diff options
author | Aleksey Lim <alsroot@sugarlabs.org> | 2012-03-09 13:40:06 (GMT) |
---|---|---|
committer | Aleksey Lim <alsroot@sugarlabs.org> | 2012-03-09 15:37:16 (GMT) |
commit | c25b7f068ea5330bea8da998e98ef65f883293ac (patch) | |
tree | cbc12c2e6db117f22fc98164d13453cdeac65ab5 | |
parent | 7d12d64605cb73b228af7dde83ec6e0f35dd2d17 (diff) |
Simplify authentication sceme by cheking creds on every request
-rw-r--r-- | restful_document/env.py | 26 | ||||
-rw-r--r-- | restful_document/router.py | 26 | ||||
-rw-r--r-- | tests/__init__.py | 37 | ||||
-rwxr-xr-x | tests/units/document.py | 113 | ||||
-rwxr-xr-x | tests/units/user.py | 66 |
5 files changed, 158 insertions, 110 deletions
diff --git a/restful_document/env.py b/restful_document/env.py index 306c218..bdab624 100644 --- a/restful_document/env.py +++ b/restful_document/env.py @@ -52,6 +52,10 @@ rundir = util.Option( _('path to the directory to place pid files'), default='/var/run/sugar-network') +auth = util.Option( + _('authenticate requests using Sugar credentials'), + default=True, type_cast=util.Option.bool_cast, action='store_true') + trust_users = util.Option( _('switch off user credentials check; disabling this option will ' \ 'require OpenSSH-5.6 or later.'), @@ -213,26 +217,8 @@ class Responce(threading.local): class Principal(threading.local): """Authenticated user.""" - _cache = {} - _user = None - - @property - def user(self): - self.authenticate() - return self._user - - def authenticate(self): - if self._user is not None: - return - enforce('sugar_user' in request and 'sugar_user_signature' in request, - Unauthorized, _('Sugar user credentials were not specified')) - signature = request['sugar_user_signature'] - user = self._cache.get(signature) - if user is None: - from restful_document.user import User - User.verify(request['sugar_user'], signature) - user = self._cache[signature] = request['sugar_user'] - self._user = user + user = None + authenticated = set() request = Request() diff --git a/restful_document/router.py b/restful_document/router.py index ab95f26..99dd24f 100644 --- a/restful_document/router.py +++ b/restful_document/router.py @@ -42,16 +42,17 @@ class Router(object): env.request.set(environ) env.responce.set() - _logger.debug('Processing request %s: %s', - env.request.url, env.request.content) + _logger.debug('Processing %s request %s: %s', + env.request.method, env.request.url, + env.request.content or '(no sent data)') try: + _authenticate() + method = self._metadata.get_method() enforce(method is not None and \ method.method == env.request.method, env.BadRequest, _('No way to handle the request')) - if method.method != 'GET': - env.principal.authenticate() result = method() except Exception, error: @@ -179,3 +180,20 @@ def _list_methods(classes): else: raise RuntimeError(_('Incorrect RESTful method for %r') % attr) yield method_cls(cls, slot, attr, **attr.restful_cls_kwargs) + + +def _authenticate(): + enforce('sugar_user' in env.request and \ + 'sugar_user_signature' in env.request, env.Unauthorized, + _('Sugar user credentials were not specified')) + + user = env.request['sugar_user'] + signature = env.request['sugar_user_signature'] + + if env.auth.value and signature not in env.principal.authenticated and \ + (env.request.path != ['user'] or env.request.method != 'POST'): + from restful_document.user import User + User.verify(user, signature) + env.principal.authenticated.add(signature) + + env.principal.user = user diff --git a/tests/__init__.py b/tests/__init__.py index eb78f41..07e1a70 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -5,11 +5,13 @@ import sys import time import signal import shutil +import hashlib import logging import unittest from os.path import dirname, join, exists, abspath import restkit +from M2Crypto import DSA import active_document as ad @@ -164,3 +166,38 @@ class Resource(restkit.Resource): if 'Content-Type' not in headers: headers['Content-Type'] = 'application/json' return restkit.Resource.delete(self, path, headers=headers, **kwargs) + + +def sign(user): + key_path = ad.util.TempFilePath(text=DSS_PRIVKEY) + key = DSA.load_key(key_path) + return key.sign_asn1(hashlib.sha1(user).digest()).encode('hex') + + +def creds(): + return {'SUGAR_USER': UID_FOR_PUBKEY, + 'SUGAR_USER_SIGNATURE': sign(UID_FOR_PUBKEY), + } + + +VALID_DSS_PUBKEY = """\ +ssh-dss AAAAB3NzaC1kc3MAAACBANuYoFH3uvJGoQFMeW6M3CCJQlPrSv6sqd9dGQlwnnNxLBrq6KgY63e10ULtyYzq9UjiIUowqbtheGrtPCtL5w7qmFcCnq1cFzAk6Xxfe6ytJDx1fql5Y1wKqa+zxOKF6SGNnglyxvf78mZXt2G6wx22AjW+1fEhAOr+g8kRiUbBAAAAFQDA/W3LfD5NBB4vlZFcT10jU4B8QwAAAIBHh1U2B71memu/TsatwOo9+CyUyvF0FHHsXwQDkeRjqY3dcfeV38YoU/EbOZtHIQgdfGrzy7m5osnpBwUtHLunZJuwCt5tBNrpU8CAF7nEXOJ4n2FnoNiWO1IsbWdhkh9Hd7+TBM9hLGmOqlqTIx3TmUG0e4F2X33VVJ8UsrJ3mwAAAIEAm29WVw9zkRbv6CTFhPlLjJ71l/2GE9XFbdznJFRmPNBBWF2J452okRWywzeDMIIoi/z0wmNSr2B6P9wduxSxp8eIWQhKVQa4V4lJyqX/A2tE5SQtFULtw3yiYOUaCjvB2s46ZM6/9K3r8o7FSKHDpYlqAbBKURNCot5zDAu6RgE= +""" +INVALID_DSS_PUBKEY = """\ +ssh-dss ____B3NzaC1kc3MAAACBANuYoFH3uvJGoQFMeW6M3CCJQlPrSv6sqd9dGQlwnnNxLBrq6KgY63e10ULtyYzq9UjiIUowqbtheGrtPCtL5w7qmFcCnq1cFzAk6Xxfe6ytJDx1fql5Y1wKqa+zxOKF6SGNnglyxvf78mZXt2G6wx22AjW+1fEhAOr+g8kRiUbBAAAAFQDA/W3LfD5NBB4vlZFcT10jU4B8QwAAAIBHh1U2B71memu/TsatwOo9+CyUyvF0FHHsXwQDkeRjqY3dcfeV38YoU/EbOZtHIQgdfGrzy7m5osnpBwUtHLunZJuwCt5tBNrpU8CAF7nEXOJ4n2FnoNiWO1IsbWdhkh9Hd7+TBM9hLGmOqlqTIx3TmUG0e4F2X33VVJ8UsrJ3mwAAAIEAm29WVw9zkRbv6CTFhPlLjJ71l/2GE9XFbdznJFRmPNBBWF2J452okRWywzeDMIIoi/z0wmNSr2B6P9wduxSxp8eIWQhKVQa4V4lJyqX/A2tE5SQtFULtw3yiYOUaCjvB2s46ZM6/9K3r8o7FSKHDpYlqAbBKURNCot5zDAu6RgE= +""" +DSS_PRIVKEY = """\ +-----BEGIN DSA PRIVATE KEY----- +MIIBvAIBAAKBgQDbmKBR97ryRqEBTHlujNwgiUJT60r+rKnfXRkJcJ5zcSwa6uio +GOt3tdFC7cmM6vVI4iFKMKm7YXhq7TwrS+cO6phXAp6tXBcwJOl8X3usrSQ8dX6p +eWNcCqmvs8TihekhjZ4Jcsb3+/JmV7dhusMdtgI1vtXxIQDq/oPJEYlGwQIVAMD9 +bct8Pk0EHi+VkVxPXSNTgHxDAoGAR4dVNge9Znprv07GrcDqPfgslMrxdBRx7F8E +A5HkY6mN3XH3ld/GKFPxGzmbRyEIHXxq88u5uaLJ6QcFLRy7p2SbsArebQTa6VPA +gBe5xFzieJ9hZ6DYljtSLG1nYZIfR3e/kwTPYSxpjqpakyMd05lBtHuBdl991VSf +FLKyd5sCgYEAm29WVw9zkRbv6CTFhPlLjJ71l/2GE9XFbdznJFRmPNBBWF2J452o +kRWywzeDMIIoi/z0wmNSr2B6P9wduxSxp8eIWQhKVQa4V4lJyqX/A2tE5SQtFULt +w3yiYOUaCjvB2s46ZM6/9K3r8o7FSKHDpYlqAbBKURNCot5zDAu6RgECFQC6wU/U +6uUSSSw8Apr+eJQlSFhA+Q== +-----END DSA PRIVATE KEY----- +""" +UID_FOR_PUBKEY = '25c081e29242cf7a19ae893a420ab3de56e9e989' diff --git a/tests/units/document.py b/tests/units/document.py index 77a8d4e..348d8c7 100755 --- a/tests/units/document.py +++ b/tests/units/document.py @@ -44,17 +44,28 @@ class Document(rd.Document): class DocumentTest(tests.Test): def test_Walkthrough(self): - self.httpd(8000, [Document]) + self.httpd(8000, [rd.User, Document]) rest = Resource('http://localhost:8000') + props = { + 'nickname': 'foo', + 'pubkey': tests.VALID_DSS_PUBKEY, + 'color': '', + 'machine_sn': 'machine_sn', + 'machine_uuid': 'machine_uuid', + } + rest.post('/user', payload=json.dumps(props), headers=tests.creds()) + guid_1 = json.loads( - rest.post('/document', payload=json.dumps({ - 'term': 'term', - 'stored': 'stored', - })).body_string() + rest.post('/document', headers=tests.creds(), + payload=json.dumps({ + 'term': 'term', + 'stored': 'stored', + }) + ).body_string() )['guid'] - reply = json.loads(rest.get('/document/' + guid_1).body_string()) + reply = json.loads(rest.get('/document/' + guid_1, headers=tests.creds()).body_string()) del reply['ctime'] del reply['mtime'] self.assertEqual( @@ -67,13 +78,15 @@ class DocumentTest(tests.Test): reply) guid_2 = json.loads( - rest.post('/document', payload=json.dumps({ - 'term': 'term2', - 'stored': 'stored2', - })).body_string() + rest.post('/document', headers=tests.creds(), + payload=json.dumps({ + 'term': 'term2', + 'stored': 'stored2', + }) + ).body_string() )['guid'] - reply = json.loads(rest.get('/document/' + guid_2).body_string()) + reply = json.loads(rest.get('/document/' + guid_2, headers=tests.creds()).body_string()) del reply['ctime'] del reply['mtime'] self.assertEqual( @@ -85,7 +98,7 @@ class DocumentTest(tests.Test): }, reply) - reply = json.loads(rest.get('/document', reply='guid,stored,term,vote,counter').body_string()) + reply = json.loads(rest.get('/document', reply='guid,stored,term,vote,counter', headers=tests.creds()).body_string()) self.assertEqual(2, reply['total']) self.assertEqual( sorted([ @@ -94,16 +107,18 @@ class DocumentTest(tests.Test): ]), sorted(reply['result'])) - rest.put('/document/' + guid_2, payload=json.dumps({ - 'vote': True, - 'stored': 'stored3', - 'term': 'term3', - })) + rest.put('/document/' + guid_2, headers=tests.creds(), + payload=json.dumps({ + 'vote': True, + 'stored': 'stored3', + 'term': 'term3', + }) + ) # Let server process commit and change `counter` property time.sleep(3) - reply = json.loads(rest.get('/document/' + guid_2).body_string()) + reply = json.loads(rest.get('/document/' + guid_2, headers=tests.creds()).body_string()) del reply['ctime'] del reply['mtime'] self.assertEqual( @@ -121,9 +136,9 @@ class DocumentTest(tests.Test): {'guid': guid_1, 'stored': 'stored', 'term': 'term', 'vote': False, 'counter': 0}, {'guid': guid_2, 'stored': 'stored3', 'term': 'term3', 'vote': True, 'counter': 1}, ])}, - json.loads(rest.get('/document', reply='guid,stored,term,vote,counter').body_string())) + json.loads(rest.get('/document', reply='guid,stored,term,vote,counter', headers=tests.creds()).body_string())) - rest.delete('/document/' + guid_1) + rest.delete('/document/' + guid_1, headers=tests.creds()) # Let server process commit and change `counter` property time.sleep(3) @@ -133,23 +148,24 @@ class DocumentTest(tests.Test): 'result': sorted([ {'guid': guid_2, 'stored': 'stored3', 'term': 'term3', 'vote': True, 'counter': 1}, ])}, - json.loads(rest.get('/document', reply='guid,stored,term,vote,counter').body_string())) + json.loads(rest.get('/document', reply='guid,stored,term,vote,counter', headers=tests.creds()).body_string())) self.assertEqual( 'term3', - json.loads(rest.get('/document/' + guid_2 + '/term').body_string())) - rest.put('/document/' + guid_2 + '/term', payload=json.dumps('term4')) + json.loads(rest.get('/document/' + guid_2 + '/term', headers=tests.creds()).body_string())) + rest.put('/document/' + guid_2 + '/term', payload=json.dumps('term4'), headers=tests.creds()) self.assertEqual( 'term4', - json.loads(rest.get('/document/' + guid_2 + '/term').body_string())) + json.loads(rest.get('/document/' + guid_2 + '/term', headers=tests.creds()).body_string())) - rest.put('/document/' + guid_2 + '/blob', payload='blob', - headers={'Content-Type': 'application/octet-stream'}) + headers = tests.creds() + headers['Content-Type'] = 'application/octet-stream' + rest.put('/document/' + guid_2 + '/blob', payload='blob', headers=headers) self.assertEqual( 'blob', - rest.get('/document/' + guid_2 + '/blob').body_string()) + rest.get('/document/' + guid_2 + '/blob', headers=tests.creds()).body_string()) - rest.delete('/document/' + guid_2) + rest.delete('/document/' + guid_2, headers=tests.creds()) # Let server process commit and change `counter` property time.sleep(3) @@ -157,28 +173,41 @@ class DocumentTest(tests.Test): self.assertEqual( {'total': 0, 'result': sorted([])}, - json.loads(rest.get('/document', reply='guid,stored,term,vote,counter').body_string())) + json.loads(rest.get('/document', reply='guid,stored,term,vote,counter', headers=tests.creds()).body_string())) def test_ServerCrash(self): - self.httpd(8000, [Document]) + self.httpd(8000, [rd.User, Document]) rest = Resource('http://localhost:8000') + props = { + 'nickname': 'foo', + 'color': '', + 'machine_sn': 'machine_sn', + 'machine_uuid': 'machine_uuid', + 'pubkey': tests.VALID_DSS_PUBKEY, + } + rest.post('/user', payload=json.dumps(props), headers=tests.creds()) + guid_1 = json.loads( - rest.post('/document', payload=json.dumps({ - 'term': 'term', - 'stored': 'stored', - })).body_string() + rest.post('/document', headers=tests.creds(), + payload=json.dumps({ + 'term': 'term', + 'stored': 'stored', + }) + ).body_string() )['guid'] guid_2 = json.loads( - rest.post('/document', payload=json.dumps({ - 'term': 'term2', - 'stored': 'stored2', - 'vote': '1', - })).body_string() + rest.post('/document', headers=tests.creds(), + payload=json.dumps({ + 'term': 'term2', + 'stored': 'stored2', + 'vote': '1', + }) + ).body_string() )['guid'] - reply = json.loads(rest.get('/document', reply='guid,stored,term,vote').body_string()) + reply = json.loads(rest.get('/document', reply='guid,stored,term,vote', headers=tests.creds()).body_string()) self.assertEqual(2, reply['total']) self.assertEqual( sorted([{'guid': guid_1, 'stored': 'stored', 'term': 'term', 'vote': False}, @@ -187,10 +216,10 @@ class DocumentTest(tests.Test): sorted(reply['result'])) self.httpdown(8000) - self.httpd(8001, [Document]) + self.httpd(8001, [rd.User, Document]) rest = Resource('http://localhost:8001') - reply = json.loads(rest.get('/document', reply='guid,stored,term,vote,counter').body_string()) + reply = json.loads(rest.get('/document', reply='guid,stored,term,vote,counter', headers=tests.creds()).body_string()) self.assertEqual(2, reply['total']) self.assertEqual( sorted([{'guid': guid_1, 'stored': 'stored', 'term': 'term', 'vote': False, 'counter': 0}, diff --git a/tests/units/user.py b/tests/units/user.py index d41d1d8..c576000 100755 --- a/tests/units/user.py +++ b/tests/units/user.py @@ -2,26 +2,23 @@ # sugar-lint: disable import json -import hashlib import restkit -from M2Crypto import DSA from __init__ import tests from tests import Resource import restful_document as rd import active_document as ad -util = ad.util -from restful_document import env +from restful_document import env, router class UserTest(tests.Test): def setUp(self): tests.Test.setUp(self) - env.principal._cache.clear() + env.principal.authenticated.clear() env.request.environ = {} def test_Register(self): @@ -35,64 +32,45 @@ class UserTest(tests.Test): 'machine_uuid': 'machine_uuid', } - props['pubkey'] = INVALID_DSS_PUBKEY - self.assertRaises(restkit.Unauthorized, rest.post, '/user', payload=json.dumps(props)) + props['pubkey'] = tests.INVALID_DSS_PUBKEY + self.assertRaises(restkit.Unauthorized, rest.post, '/user', payload=json.dumps(props), headers=tests.creds()) - props['pubkey'] = VALID_DSS_PUBKEY - reply = json.loads( - rest.post('/user', payload=json.dumps(props)).body_string()) - self.assertEqual(UID_FOR_PUBKEY, reply['guid']) + props['pubkey'] = tests.VALID_DSS_PUBKEY + reply = json.loads(rest.post('/user', payload=json.dumps(props), headers=tests.creds()).body_string()) + self.assertEqual(tests.UID_FOR_PUBKEY, reply['guid']) def test_Authenticate(self): def set_headers(user): - key_path = util.TempFilePath(text=DSS_PRIVKEY) - key = DSA.load_key(key_path) - signature = key.sign_asn1(hashlib.sha1(user).digest()).encode('hex') env.request.environ['HTTP_SUGAR_USER'] = user - env.request.environ['HTTP_SUGAR_USER_SIGNATURE'] = signature + env.request.environ['HTTP_SUGAR_USER_SIGNATURE'] = tests.sign(user) props = { 'nickname': 'foo', 'color': '', 'machine_sn': 'machine_sn', 'machine_uuid': 'machine_uuid', - 'pubkey': VALID_DSS_PUBKEY, + 'pubkey': tests.VALID_DSS_PUBKEY, } self.httpd(8888, [rd.User]) rest = Resource('http://localhost:8888') - rest.post('/user', payload=json.dumps(props)) + rest.post('/user', payload=json.dumps(props), headers=tests.creds()) self.httpdown(8888) with ad.Master([rd.User]): + env.principal.user = None + set_headers('foo') - self.assertRaises(env.Unauthorized, lambda: env.principal.user) - - set_headers(UID_FOR_PUBKEY) - self.assertEqual(UID_FOR_PUBKEY, env.principal.user) - - -VALID_DSS_PUBKEY = """\ -ssh-dss AAAAB3NzaC1kc3MAAACBANuYoFH3uvJGoQFMeW6M3CCJQlPrSv6sqd9dGQlwnnNxLBrq6KgY63e10ULtyYzq9UjiIUowqbtheGrtPCtL5w7qmFcCnq1cFzAk6Xxfe6ytJDx1fql5Y1wKqa+zxOKF6SGNnglyxvf78mZXt2G6wx22AjW+1fEhAOr+g8kRiUbBAAAAFQDA/W3LfD5NBB4vlZFcT10jU4B8QwAAAIBHh1U2B71memu/TsatwOo9+CyUyvF0FHHsXwQDkeRjqY3dcfeV38YoU/EbOZtHIQgdfGrzy7m5osnpBwUtHLunZJuwCt5tBNrpU8CAF7nEXOJ4n2FnoNiWO1IsbWdhkh9Hd7+TBM9hLGmOqlqTIx3TmUG0e4F2X33VVJ8UsrJ3mwAAAIEAm29WVw9zkRbv6CTFhPlLjJ71l/2GE9XFbdznJFRmPNBBWF2J452okRWywzeDMIIoi/z0wmNSr2B6P9wduxSxp8eIWQhKVQa4V4lJyqX/A2tE5SQtFULtw3yiYOUaCjvB2s46ZM6/9K3r8o7FSKHDpYlqAbBKURNCot5zDAu6RgE= -""" -INVALID_DSS_PUBKEY = """\ -ssh-dss ____B3NzaC1kc3MAAACBANuYoFH3uvJGoQFMeW6M3CCJQlPrSv6sqd9dGQlwnnNxLBrq6KgY63e10ULtyYzq9UjiIUowqbtheGrtPCtL5w7qmFcCnq1cFzAk6Xxfe6ytJDx1fql5Y1wKqa+zxOKF6SGNnglyxvf78mZXt2G6wx22AjW+1fEhAOr+g8kRiUbBAAAAFQDA/W3LfD5NBB4vlZFcT10jU4B8QwAAAIBHh1U2B71memu/TsatwOo9+CyUyvF0FHHsXwQDkeRjqY3dcfeV38YoU/EbOZtHIQgdfGrzy7m5osnpBwUtHLunZJuwCt5tBNrpU8CAF7nEXOJ4n2FnoNiWO1IsbWdhkh9Hd7+TBM9hLGmOqlqTIx3TmUG0e4F2X33VVJ8UsrJ3mwAAAIEAm29WVw9zkRbv6CTFhPlLjJ71l/2GE9XFbdznJFRmPNBBWF2J452okRWywzeDMIIoi/z0wmNSr2B6P9wduxSxp8eIWQhKVQa4V4lJyqX/A2tE5SQtFULtw3yiYOUaCjvB2s46ZM6/9K3r8o7FSKHDpYlqAbBKURNCot5zDAu6RgE= -""" -DSS_PRIVKEY = """\ ------BEGIN DSA PRIVATE KEY----- -MIIBvAIBAAKBgQDbmKBR97ryRqEBTHlujNwgiUJT60r+rKnfXRkJcJ5zcSwa6uio -GOt3tdFC7cmM6vVI4iFKMKm7YXhq7TwrS+cO6phXAp6tXBcwJOl8X3usrSQ8dX6p -eWNcCqmvs8TihekhjZ4Jcsb3+/JmV7dhusMdtgI1vtXxIQDq/oPJEYlGwQIVAMD9 -bct8Pk0EHi+VkVxPXSNTgHxDAoGAR4dVNge9Znprv07GrcDqPfgslMrxdBRx7F8E -A5HkY6mN3XH3ld/GKFPxGzmbRyEIHXxq88u5uaLJ6QcFLRy7p2SbsArebQTa6VPA -gBe5xFzieJ9hZ6DYljtSLG1nYZIfR3e/kwTPYSxpjqpakyMd05lBtHuBdl991VSf -FLKyd5sCgYEAm29WVw9zkRbv6CTFhPlLjJ71l/2GE9XFbdznJFRmPNBBWF2J452o -kRWywzeDMIIoi/z0wmNSr2B6P9wduxSxp8eIWQhKVQa4V4lJyqX/A2tE5SQtFULt -w3yiYOUaCjvB2s46ZM6/9K3r8o7FSKHDpYlqAbBKURNCot5zDAu6RgECFQC6wU/U -6uUSSSw8Apr+eJQlSFhA+Q== ------END DSA PRIVATE KEY----- -""" -UID_FOR_PUBKEY = '25c081e29242cf7a19ae893a420ab3de56e9e989' + self.assertRaises(env.Unauthorized, router._authenticate) + self.assertEqual(None, env.principal.user) + + set_headers(tests.UID_FOR_PUBKEY) + router._authenticate() + self.assertEqual(tests.UID_FOR_PUBKEY, env.principal.user) + + + + if __name__ == '__main__': |