From d37d2c4aaacafc122fba697f4bee52e3726b2029 Mon Sep 17 00:00:00 2001 From: Aleksey Lim Date: Mon, 16 Apr 2012 18:53:59 +0000 Subject: Test authorization --- diff --git a/tests/__init__.py b/tests/__init__.py index 8dfdfa4..e770e94 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -3,6 +3,7 @@ import os import sys import time +import json import signal import shutil import hashlib @@ -10,12 +11,13 @@ import logging import unittest from os.path import dirname, join, exists, abspath -import restkit +import requests from M2Crypto import DSA import active_document as ad +import active_document as rd -from active_document import env as _env +from restful_document import env as _env root = abspath(dirname(__file__)) @@ -52,12 +54,13 @@ class Test(unittest.TestCase): logging.getLogger().removeHandler(handler) logging.basicConfig(level=logging.DEBUG, filename=logfile) - _env.data_root.value = tmpdir - _env.index_flush_timeout.value = 0 - _env.index_flush_threshold.value = 1 - _env.find_limit.value = 1024 - _env.index_write_queue.value = 10 - _env.LAYOUT_VERSION = 1 + ad.data_root.value = tmpdir + ad.index_flush_timeout.value = 0 + ad.index_flush_threshold.value = 1 + ad.find_limit.value = 1024 + ad.index_write_queue.value = 10 + ad.LAYOUT_VERSION = 1 + _env.request.environ = {} ad.data_root.value = tmpdir + '/db' ad.index_flush_timeout.value = 0 @@ -137,35 +140,51 @@ class Test(unittest.TestCase): os.waitpid(pid, 0) -class Resource(restkit.Resource): +class Resource(object): + + def __init__(self, url, uid=None, privkey=None, pubkey=None): + self.url = url + self.uid = uid or UID + self.privkey = privkey or PRIVKEY + + self.post('/user', {'uid': self.uid, 'pubkey': pubkey or PUBKEY}) + + def get(self, path, **kwargs): + return self._request('GET', path, **kwargs) - def get(self, path=None, headers=None, **kwargs): - if headers is None: - headers = {} - if 'Content-Type' not in headers: - headers['Content-Type'] = 'application/json' - return restkit.Resource.get(self, path, headers=headers, **kwargs) + def put(self, path, data, **kwargs): + return self._request('PUT', path, data, **kwargs) - def put(self, path=None, headers=None, **kwargs): - if headers is None: - headers = {} - if 'Content-Type' not in headers: - headers['Content-Type'] = 'application/json' - return restkit.Resource.put(self, path, headers=headers, **kwargs) + def post(self, path, data, **kwargs): + return self._request('POST', path, data, **kwargs) - def post(self, path=None, headers=None, **kwargs): - if headers is None: - headers = {} - if 'Content-Type' not in headers: - headers['Content-Type'] = 'application/json' - return restkit.Resource.post(self, path, headers=headers, **kwargs) + def delete(self, path, **kwargs): + return self._request('DELETE', path, **kwargs) - def delete(self, path=None, headers=None, **kwargs): - if headers is None: - headers = {} - if 'Content-Type' not in headers: - headers['Content-Type'] = 'application/json' - return restkit.Resource.delete(self, path, headers=headers, **kwargs) + def _request(self, method, path, data=None, headers=None, **kwargs): + if not headers: + if data: + headers = {'Content-Type': 'application/json'} + data = json.dumps(data) + else: + headers = {} + headers['SUGAR_USER'] = self.uid + headers['SUGAR_USER_SIGNATURE'] = sign(self.privkey, self.uid) + + response = requests.request(method, self.url + path, data=data, + headers=headers, config={'keep_alive': True}, params=kwargs) + reply = response.content + + if response.status_code != 200: + if reply: + raise RuntimeError(reply) + else: + response.raise_for_status() + + if response.headers.get('Content-Type') == 'application/json': + return json.loads(reply) + else: + return reply class User(ad.Document): @@ -181,32 +200,27 @@ class User(ad.Document): ['ssh-keygen', '-f', src_path, '-e', '-m', 'PKCS8']) properties['pubkey'] = pubkey_pkcs8 + guid = properties.pop('uid') doc = cls(**properties) - doc.set('guid', UID_FOR_PUBKEY, raw=True) + doc.set('guid', guid, raw=True) doc.post() return doc -def sign(user): - key_path = ad.util.TempFilePath(text=DSS_PRIVKEY) +def sign(privkey, data): + key_path = ad.util.TempFilePath(text=privkey) key = DSA.load_key(key_path) - return key.sign_asn1(hashlib.sha1(user).digest()).encode('hex') - + return key.sign_asn1(hashlib.sha1(data).digest()).encode('hex') -def creds(): - return {'SUGAR_USER': UID_FOR_PUBKEY, - 'SUGAR_USER_SIGNATURE': sign(UID_FOR_PUBKEY), - } - -VALID_DSS_PUBKEY = """\ +PUBKEY = """\ ssh-dss AAAAB3NzaC1kc3MAAACBANuYoFH3uvJGoQFMeW6M3CCJQlPrSv6sqd9dGQlwnnNxLBrq6KgY63e10ULtyYzq9UjiIUowqbtheGrtPCtL5w7qmFcCnq1cFzAk6Xxfe6ytJDx1fql5Y1wKqa+zxOKF6SGNnglyxvf78mZXt2G6wx22AjW+1fEhAOr+g8kRiUbBAAAAFQDA/W3LfD5NBB4vlZFcT10jU4B8QwAAAIBHh1U2B71memu/TsatwOo9+CyUyvF0FHHsXwQDkeRjqY3dcfeV38YoU/EbOZtHIQgdfGrzy7m5osnpBwUtHLunZJuwCt5tBNrpU8CAF7nEXOJ4n2FnoNiWO1IsbWdhkh9Hd7+TBM9hLGmOqlqTIx3TmUG0e4F2X33VVJ8UsrJ3mwAAAIEAm29WVw9zkRbv6CTFhPlLjJ71l/2GE9XFbdznJFRmPNBBWF2J452okRWywzeDMIIoi/z0wmNSr2B6P9wduxSxp8eIWQhKVQa4V4lJyqX/A2tE5SQtFULtw3yiYOUaCjvB2s46ZM6/9K3r8o7FSKHDpYlqAbBKURNCot5zDAu6RgE= """ -INVALID_DSS_PUBKEY = """\ +INVALID_PUBKEY = """\ ssh-dss ____B3NzaC1kc3MAAACBANuYoFH3uvJGoQFMeW6M3CCJQlPrSv6sqd9dGQlwnnNxLBrq6KgY63e10ULtyYzq9UjiIUowqbtheGrtPCtL5w7qmFcCnq1cFzAk6Xxfe6ytJDx1fql5Y1wKqa+zxOKF6SGNnglyxvf78mZXt2G6wx22AjW+1fEhAOr+g8kRiUbBAAAAFQDA/W3LfD5NBB4vlZFcT10jU4B8QwAAAIBHh1U2B71memu/TsatwOo9+CyUyvF0FHHsXwQDkeRjqY3dcfeV38YoU/EbOZtHIQgdfGrzy7m5osnpBwUtHLunZJuwCt5tBNrpU8CAF7nEXOJ4n2FnoNiWO1IsbWdhkh9Hd7+TBM9hLGmOqlqTIx3TmUG0e4F2X33VVJ8UsrJ3mwAAAIEAm29WVw9zkRbv6CTFhPlLjJ71l/2GE9XFbdznJFRmPNBBWF2J452okRWywzeDMIIoi/z0wmNSr2B6P9wduxSxp8eIWQhKVQa4V4lJyqX/A2tE5SQtFULtw3yiYOUaCjvB2s46ZM6/9K3r8o7FSKHDpYlqAbBKURNCot5zDAu6RgE= """ -DSS_PRIVKEY = """\ +PRIVKEY = """\ -----BEGIN DSA PRIVATE KEY----- MIIBvAIBAAKBgQDbmKBR97ryRqEBTHlujNwgiUJT60r+rKnfXRkJcJ5zcSwa6uio GOt3tdFC7cmM6vVI4iFKMKm7YXhq7TwrS+cO6phXAp6tXBcwJOl8X3usrSQ8dX6p @@ -220,4 +234,23 @@ w3yiYOUaCjvB2s46ZM6/9K3r8o7FSKHDpYlqAbBKURNCot5zDAu6RgECFQC6wU/U 6uUSSSw8Apr+eJQlSFhA+Q== -----END DSA PRIVATE KEY----- """ -UID_FOR_PUBKEY = '25c081e29242cf7a19ae893a420ab3de56e9e989' +UID = '25c081e29242cf7a19ae893a420ab3de56e9e989' + +PUBKEY2 = """\ +ssh-dss AAAAB3NzaC1kc3MAAACBAOTS+oSz5nmXlxGLhnadTHwZDf9H124rRLqIxmLhHZy/I93LPHfG1T/hSF9n46DEKwfpLZ8EMNl2VNATvPhbst0ckcsdaB6FSblYVNMFu9C+SAwiX1+JYw8e9koFq8tIKyBz+V1zzr3VUJoUozYvT4MehIFq2YlYR4AdlnfbwQG/AAAAFQDa4fpL/eMJBgp2azVvcHPXoAN1dQAAAIAM41xtZbZ2GvOyiMB49gPFta/SWsie84agasvDVaUljj4RLgIHAOe75V3vh8Myjz7WxBMqS09IRKO8EM9Xv/BeRdLQfXRFvOY3kG4C5EJPIoZykDKCag9fEtw3PMSSf50wvnO0zz1FlJOKsf0tNYfeO98KY3fUNyxoI4p7HbLAoQAAAIEAxHnjr34jnPHGL8n4lhALJDbBUJOP5SwubArF94wodPmFtDI0ia6lWV1o3aHtwpTKRIiyUocJRaTJzxArdSh3jfutxaoIs+KqPgGa3rO5jbHv07b40bpueH8nnb6Mc5Qas/NaCLwWqWoVs5F7w28v70LB88PcmGxxjP1bXxLlDKE= +""" +PRIVKEY2 = """\ +-----BEGIN DSA PRIVATE KEY----- +MIIBuwIBAAKBgQDk0vqEs+Z5l5cRi4Z2nUx8GQ3/R9duK0S6iMZi4R2cvyPdyzx3 +xtU/4UhfZ+OgxCsH6S2fBDDZdlTQE7z4W7LdHJHLHWgehUm5WFTTBbvQvkgMIl9f +iWMPHvZKBavLSCsgc/ldc8691VCaFKM2L0+DHoSBatmJWEeAHZZ328EBvwIVANrh ++kv94wkGCnZrNW9wc9egA3V1AoGADONcbWW2dhrzsojAePYDxbWv0lrInvOGoGrL +w1WlJY4+ES4CBwDnu+Vd74fDMo8+1sQTKktPSESjvBDPV7/wXkXS0H10RbzmN5Bu +AuRCTyKGcpAygmoPXxLcNzzEkn+dML5ztM89RZSTirH9LTWH3jvfCmN31DcsaCOK +ex2ywKECgYEAxHnjr34jnPHGL8n4lhALJDbBUJOP5SwubArF94wodPmFtDI0ia6l +WV1o3aHtwpTKRIiyUocJRaTJzxArdSh3jfutxaoIs+KqPgGa3rO5jbHv07b40bpu +eH8nnb6Mc5Qas/NaCLwWqWoVs5F7w28v70LB88PcmGxxjP1bXxLlDKECFFHbJZ6Y +D+YxdWZ851uNEXjVIvza +-----END DSA PRIVATE KEY----- +""" +UID2 = 'd87dc9fde73fa1cf86c1e7ce86129eaf88985828' diff --git a/tests/units/__main__.py b/tests/units/__main__.py index 83f6854..467f93b 100644 --- a/tests/units/__main__.py +++ b/tests/units/__main__.py @@ -2,7 +2,6 @@ from __init__ import tests -from document import * -from user import * +from router import * tests.main() diff --git a/tests/units/document.py b/tests/units/router.py index 89ef15a..f4fbe8d 100755 --- a/tests/units/document.py +++ b/tests/units/router.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # sugar-lint: disable -import json import time import hashlib @@ -10,28 +9,19 @@ from tests import Resource import active_document as ad +from restful_document import env +from restful_document.router import Router -class DocumentTest(tests.Test): + +class RouterTest(tests.Test): def test_Walkthrough(self): self.httpd(8000, [tests.User, Document]) rest = Resource('http://localhost:8000') - props = { - 'pubkey': tests.VALID_DSS_PUBKEY, - } - rest.post('/user', payload=json.dumps(props), headers=tests.creds()) - - guid_1 = json.loads( - rest.post('/document', headers=tests.creds(), - payload=json.dumps({ - 'term': 'term', - 'stored': 'stored', - }) - ).body_string() - )['guid'] - - reply = json.loads(rest.get('/document/' + guid_1, headers=tests.creds()).body_string()) + guid_1 = rest.post('/document', {'term': 'term', 'stored': 'stored'})['guid'] + + reply = rest.get('/document/' + guid_1) del reply['ctime'] del reply['mtime'] self.assertEqual( @@ -42,20 +32,13 @@ class DocumentTest(tests.Test): 'counter': 0, 'guid': guid_1, 'layers': ['public'], - 'author': [tests.UID_FOR_PUBKEY], + 'author': [rest.uid], }, reply) - guid_2 = json.loads( - rest.post('/document', headers=tests.creds(), - payload=json.dumps({ - 'term': 'term2', - 'stored': 'stored2', - }) - ).body_string() - )['guid'] + guid_2 = rest.post('/document', {'term': 'term2', 'stored': 'stored2'})['guid'] - reply = json.loads(rest.get('/document/' + guid_2, headers=tests.creds()).body_string()) + reply = rest.get('/document/' + guid_2) del reply['ctime'] del reply['mtime'] self.assertEqual( @@ -66,11 +49,11 @@ class DocumentTest(tests.Test): 'counter': 0, 'guid': guid_2, 'layers': ['public'], - 'author': [tests.UID_FOR_PUBKEY], + 'author': [rest.uid], }, reply) - reply = json.loads(rest.get('/document', reply='guid,stored,term,vote,counter', headers=tests.creds()).body_string()) + reply = rest.get('/document', reply='guid,stored,term,vote,counter') self.assertEqual(2, reply['total']) self.assertEqual( sorted([ @@ -79,18 +62,12 @@ class DocumentTest(tests.Test): ]), sorted(reply['result'])) - rest.put('/document/' + guid_2, headers=tests.creds(), - payload=json.dumps({ - 'vote': True, - 'stored': 'stored3', - 'term': 'term3', - }) - ) + rest.put('/document/' + guid_2, {'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, headers=tests.creds()).body_string()) + reply = rest.get('/document/' + guid_2) del reply['ctime'] del reply['mtime'] self.assertEqual( @@ -101,7 +78,7 @@ class DocumentTest(tests.Test): 'counter': 1, 'guid': guid_2, 'layers': ['public'], - 'author': [tests.UID_FOR_PUBKEY], + 'author': [rest.uid], }, reply) @@ -111,9 +88,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', headers=tests.creds()).body_string())) + rest.get('/document', reply='guid,stored,term,vote,counter')) - rest.delete('/document/' + guid_1, headers=tests.creds()) + rest.delete('/document/' + guid_1) # Let server process commit and change `counter` property time.sleep(3) @@ -123,28 +100,26 @@ 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', headers=tests.creds()).body_string())) + rest.get('/document', reply='guid,stored,term,vote,counter')) self.assertEqual( 'term3', - 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()) + rest.get('/document/' + guid_2 + '/term')) + rest.put('/document/' + guid_2 + '/term', 'term4') self.assertEqual( 'term4', - json.loads(rest.get('/document/' + guid_2 + '/term', headers=tests.creds()).body_string())) + rest.get('/document/' + guid_2 + '/term')) payload = 'blob' - headers = tests.creds() - headers['Content-Type'] = 'application/octet-stream' - rest.put('/document/' + guid_2 + '/blob', payload=payload, headers=headers) + rest.put('/document/' + guid_2 + '/blob', payload, headers={'Content-Type': 'application/octet-stream'}) self.assertEqual( payload, - rest.get('/document/' + guid_2 + '/blob', headers=tests.creds()).body_string()) + rest.get('/document/' + guid_2 + '/blob')) self.assertEqual( {'size': len(payload), 'sha1sum': hashlib.sha1(payload).hexdigest()}, - json.loads(rest.get('/document/' + guid_2 + '/blob', headers=tests.creds(), cmd='stat-blob').body_string())) + rest.get('/document/' + guid_2 + '/blob', cmd='stat-blob')) - rest.delete('/document/' + guid_2, headers=tests.creds()) + rest.delete('/document/' + guid_2) # Let server process commit and change `counter` property time.sleep(3) @@ -152,37 +127,16 @@ class DocumentTest(tests.Test): self.assertEqual( {'total': 0, 'result': sorted([])}, - json.loads(rest.get('/document', reply='guid,stored,term,vote,counter', headers=tests.creds()).body_string())) + rest.get('/document', reply='guid,stored,term,vote,counter')) def test_ServerCrash(self): self.httpd(8000, [tests.User, Document]) rest = Resource('http://localhost:8000') - props = { - 'pubkey': tests.VALID_DSS_PUBKEY, - } - rest.post('/user', payload=json.dumps(props), headers=tests.creds()) - - guid_1 = json.loads( - rest.post('/document', headers=tests.creds(), - payload=json.dumps({ - 'term': 'term', - 'stored': 'stored', - }) - ).body_string() - )['guid'] - guid_2 = json.loads( - 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', headers=tests.creds()).body_string()) + guid_1 = rest.post('/document', {'term': 'term', 'stored': 'stored'})['guid'] + guid_2 = rest.post('/document', {'term': 'term2', 'stored': 'stored2', 'vote': '1'})['guid'] + + reply = rest.get('/document', reply='guid,stored,term,vote') self.assertEqual(2, reply['total']) self.assertEqual( sorted([{'guid': guid_1, 'stored': 'stored', 'term': 'term', 'vote': False}, @@ -194,7 +148,7 @@ class DocumentTest(tests.Test): self.httpd(8001, [tests.User, Document]) rest = Resource('http://localhost:8001') - reply = json.loads(rest.get('/document', reply='guid,stored,term,vote,counter', headers=tests.creds()).body_string()) + reply = rest.get('/document', reply='guid,stored,term,vote,counter') self.assertEqual(2, reply['total']) self.assertEqual( sorted([{'guid': guid_1, 'stored': 'stored', 'term': 'term', 'vote': False, 'counter': 0}, @@ -202,6 +156,52 @@ class DocumentTest(tests.Test): ]), sorted(reply['result'])) + def test_Register(self): + self.httpd(8000, [tests.User]) + + self.assertRaises(RuntimeError, Resource, 'http://localhost:8000', + uid=tests.UID, privkey=tests.PRIVKEY, + pubkey=tests.INVALID_PUBKEY) + + rest = Resource('http://localhost:8000', + uid=tests.UID, privkey=tests.PRIVKEY, pubkey=tests.PUBKEY) + self.assertEqual( + {'total': 1, + 'result': sorted([ + {'guid': tests.UID}, + ]), + }, + rest.get('/user')) + + def test_Authenticate(self): + self.httpd(8888, [tests.User]) + rest = Resource('http://localhost:8888') + self.httpdown(8888) + + with ad.Master([tests.User]) as node: + router = Router(node) + ad.principal.user = None + + env.request.environ['HTTP_SUGAR_USER'] = 'foo' + env.request.environ['HTTP_SUGAR_USER_SIGNATURE'] = tests.sign(tests.PRIVKEY, 'foo') + self.assertRaises(env.Unauthorized, router._authenticate) + self.assertEqual(None, ad.principal.user) + + env.request.environ['HTTP_SUGAR_USER'] = tests.UID + env.request.environ['HTTP_SUGAR_USER_SIGNATURE'] = tests.sign(tests.PRIVKEY, tests.UID) + router._authenticate() + self.assertEqual(tests.UID, ad.principal.user) + + def test_Authorization(self): + self.httpd(8000, [tests.User, Document]) + + rest_1 = Resource('http://localhost:8000') + guid = rest_1.post('/document', {'term': '', 'stored': ''})['guid'] + + rest_2 = Resource('http://localhost:8000', tests.UID2, tests.PRIVKEY2, tests.PUBKEY2) + self.assertRaises(RuntimeError, rest_2.put, '/document/' + guid, {'term': 'new'}) + self.assertRaises(RuntimeError, rest_2.delete, '/document/' + guid) + class Vote(ad.AggregatorProperty): diff --git a/tests/units/user.py b/tests/units/user.py deleted file mode 100755 index 829717e..0000000 --- a/tests/units/user.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python -# sugar-lint: disable - -import json - -import restkit - -from __init__ import tests -from tests import Resource - -import active_document as ad - -from restful_document import env -from restful_document.router import Router - - -class UserTest(tests.Test): - - def setUp(self): - tests.Test.setUp(self) - env.request.environ = {} - - def test_Register(self): - self.httpd(8000, [tests.User]) - rest = Resource('http://localhost:8000') - - props = {} - props['pubkey'] = tests.INVALID_DSS_PUBKEY - self.assertRaises(restkit.RequestFailed, rest.post, '/user', payload=json.dumps(props), headers=tests.creds()) - - 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): - env.request.environ['HTTP_SUGAR_USER'] = user - env.request.environ['HTTP_SUGAR_USER_SIGNATURE'] = tests.sign(user) - - props = { - 'pubkey': tests.VALID_DSS_PUBKEY, - } - self.httpd(8888, [tests.User]) - rest = Resource('http://localhost:8888') - rest.post('/user', payload=json.dumps(props), headers=tests.creds()) - self.httpdown(8888) - - with ad.Master([tests.User]) as node: - router = Router(node) - ad.principal.user = None - - set_headers('foo') - self.assertRaises(env.Unauthorized, router._authenticate) - self.assertEqual(None, ad.principal.user) - - set_headers(tests.UID_FOR_PUBKEY) - router._authenticate() - self.assertEqual(tests.UID_FOR_PUBKEY, ad.principal.user) - - -if __name__ == '__main__': - tests.main() -- cgit v0.9.1