diff --git a/conductor/blueprints/authentication/blueprint.py b/conductor/blueprints/authentication/blueprint.py index 898e3dd..d1cd3fd 100644 --- a/conductor/blueprints/authentication/blueprint.py +++ b/conductor/blueprints/authentication/blueprint.py @@ -1,6 +1,11 @@ -from flask import Blueprint +import os +from flask import Blueprint, request, url_for +from flask.ext.jsonpify import jsonpify + from . import controllers +os_conductor = os.environ.get('OS_EXTERNAL_ADDRESS') + def create(): """Create blueprint. @@ -9,11 +14,25 @@ def create(): # Create instance blueprint = Blueprint('authentication', 'authentication') + # Controller Proxies + check_controller = controllers.Check() + callback_controller = controllers.Callback() + + def check(): + token = request.values.get('jwt') + next_url = request.args.get('next', None) + callback_url = 'http://'+os_conductor+url_for('.callback') + return jsonpify(check_controller(token, next_url, callback_url)) + + def callback(): + state = request.args.get('state') + return callback_controller(state) + # Register routes blueprint.add_url_rule( - 'check', 'check', controllers.Check(), methods=['GET']) + 'check', 'check', check, methods=['GET']) blueprint.add_url_rule( - 'google_callback', 'callback', controllers.Callback(), methods=['GET']) + 'google_callback', 'callback', callback, methods=['GET']) # Return blueprint return blueprint diff --git a/conductor/blueprints/authentication/controllers.py b/conductor/blueprints/authentication/controllers.py index 8b5c6ce..57812bb 100644 --- a/conductor/blueprints/authentication/controllers.py +++ b/conductor/blueprints/authentication/controllers.py @@ -1,6 +1,6 @@ -import os import requests import datetime +import logging try: import urllib.parse as urlparse @@ -8,8 +8,7 @@ import urlparse import jwt -from flask import request, url_for, redirect -from flask.ext.jsonpify import jsonpify +from flask import redirect from flask_oauthlib.client import OAuth, OAuthException from .models import create_or_get_user, get_user @@ -20,7 +19,6 @@ def readfile_or_default(filename, default): return open(filename).read().strip() except IOError: return default -os_conductor = os.environ.get('OS_EXTERNAL_ADDRESS') PRIVATE_KEY = readfile_or_default('/secrets/private.pem', 'private key stub') GOOGLE_KEY = readfile_or_default('/secrets/google.key', 'google consumer key') @@ -65,8 +63,7 @@ class Check: # Public - def __call__(self): - token = request.values.get('jwt') + def __call__(self, token, next, callback_url): if token is not None: try: token = jwt.decode(token, PRIVATE_KEY) @@ -85,11 +82,9 @@ def __call__(self): 'avatar_url': user.avatar_url } } - return jsonpify(ret) + return ret # Otherwise - not authenticated - callback = 'http://'+os_conductor+url_for('.callback') - next = request.args.get('next', None) provider = 'google' state = { 'next': next, @@ -99,7 +94,7 @@ def __call__(self): } state = jwt.encode(state, PRIVATE_KEY) google_login_url = google_remote_app()\ - .authorize(callback=callback, state=state).headers['Location'] + .authorize(callback=callback_url, state=state).headers['Location'] ret = { 'authenticated': False, @@ -110,7 +105,7 @@ def __call__(self): } } - return jsonpify(ret) + return ret class Callback: @@ -119,18 +114,19 @@ class Callback: # Public - def __call__(self): + def __call__(self, state): resp = google_remote_app().authorized_response() if isinstance(resp, OAuthException): + logging.log(logging.WARN, "OAuthException: %r" % resp) resp = None - state = request.args.get('state') try: state = jwt.decode(state, PRIVATE_KEY) except jwt.InvalidTokenError: state = None next_url = '/' + provider = None if state is not None: provider = state.get('provider') next_url = state.get('next', next_url) diff --git a/conductor/blueprints/authorization/blueprint.py b/conductor/blueprints/authorization/blueprint.py index f8a2d44..149c02f 100644 --- a/conductor/blueprints/authorization/blueprint.py +++ b/conductor/blueprints/authorization/blueprint.py @@ -1,4 +1,5 @@ -from flask import Blueprint +from flask import Blueprint, request +from flask.ext.jsonpify import jsonpify from . import controllers @@ -9,9 +10,17 @@ def create(): # Create instance blueprint = Blueprint('authorization', 'authorization') + # Controller Proxies + check_controller = controllers.Check() + + def check(): + token = request.values.get('jwt') + service = request.values.get('service') + return jsonpify(check_controller(token, service)) + # Register routes blueprint.add_url_rule( - 'check', 'check', controllers.Check(), methods=['GET']) + 'check', 'check', check, methods=['GET']) blueprint.add_url_rule( 'public-key', 'public-key', controllers.PublicKey(), methods=['GET']) blueprint.add_url_rule( diff --git a/conductor/blueprints/authorization/controllers.py b/conductor/blueprints/authorization/controllers.py index 843321a..fc27833 100644 --- a/conductor/blueprints/authorization/controllers.py +++ b/conductor/blueprints/authorization/controllers.py @@ -6,8 +6,7 @@ import urlparse # silence pyflakes import jwt -from flask import request, make_response -from flask.ext.jsonpify import jsonpify +from flask import make_response from .models import get_permission @@ -19,8 +18,42 @@ def readfile_or_default(filename, default): return default os_conductor = os.environ.get('OS_EXTERNAL_ADDRESS') -PUBLIC_KEY = readfile_or_default('/secrets/public.pem', 'public key stub') -PRIVATE_KEY = readfile_or_default('/secrets/private.pem', 'private key stub') +PUBLIC_KEY = readfile_or_default('/secrets/public.pem', '''-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzSrV/SxRNKufc6f0GQIu +YMASgBCOiJW5fvCnGtVMIrWvBQoCFAp9QwRHrbQrQJiPg6YqqnTvGhWssL5LMMvR +8jXXOpFUKzYaSgYaQt1LNMCwtqMB0FGSDjBrbmEmnDSo6g0Naxhi+SJX3BMcce1W +TgKRybv3N3F+gJ9d8wPkyx9xhd3H4200lHk4T5XK5+LyAPSnP7FNUYTdJRRxKFWg +ZFuII+Ex6mtUKU9LZsg9xeAC6033dmSYe5yWfdrFehmQvPBUVH4HLtL1fXTNyXuz +ZwtO1v61Qc1u/j7gMsrHXW+4csjS3lDwiiPIg6q1hTA7QJdB1M+rja2MG+owL0U9 +owIDAQAB +-----END PUBLIC KEY-----''') +PRIVATE_KEY = readfile_or_default('/secrets/private.pem', '''-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAzSrV/SxRNKufc6f0GQIuYMASgBCOiJW5fvCnGtVMIrWvBQoC +FAp9QwRHrbQrQJiPg6YqqnTvGhWssL5LMMvR8jXXOpFUKzYaSgYaQt1LNMCwtqMB +0FGSDjBrbmEmnDSo6g0Naxhi+SJX3BMcce1WTgKRybv3N3F+gJ9d8wPkyx9xhd3H +4200lHk4T5XK5+LyAPSnP7FNUYTdJRRxKFWgZFuII+Ex6mtUKU9LZsg9xeAC6033 +dmSYe5yWfdrFehmQvPBUVH4HLtL1fXTNyXuzZwtO1v61Qc1u/j7gMsrHXW+4csjS +3lDwiiPIg6q1hTA7QJdB1M+rja2MG+owL0U9owIDAQABAoIBAHgA7ytniZQSMnDW +szsRgIkMr4WCqawQT3CFWGikjCTdOiLraK3KONxDG53pfUcKNR9eySPsw5HxTZIP +rDE9dm6CuYJDUQT5X0Ue7qtffsa7UmFxVPVBUPnFroDgiFHjp01HFysmF3X7dYJ/ +Fys4FDwK2rUxoXcnhkO7c5taErAPhpmv+QncVBkouQ3bB78av6cHdQfo+7PcvYRP +x6iDPAjMpz1wF1Fkd9mSHadjuqlC3FubbwEK5nTuSl4nPULK7KaCv9NjxyzTUi23 +DWk9QCv+peIK/1h75cbB9eVvZayHlFlVNtD7Mrx5rediWABSqvNLRv/aZ0/o5+FM +1cxiYPECgYEA9AEr60CPlW9vBOacCImnWHWEH/UEwi4aNTBxpZEWRuN0HnmB+4Rt +1b+7LoX6olVBN1y8YIwzkDOCVblFaT+THBNiE7ABwB87c0jYd2ULQszqrebjXPoz +8q7MqghD+4iDfvP2QmivpadfeGGzYFI49b7W5c/Iv4w0oWgutib+hDsCgYEA10Dk +hMwg61q6YVAeTIqnV7zujfzTIif9AkePAfNLolLdn0Bx5LS6oPxeRUxyy4mImwrf +p6yZGOX/7ocy7rQ3X/F6fuxwuGa74PNZPwlLuD7UUPr//OPuQihoDKvL+52XWA5U +Q09sXK+KlvuH4DJ5UsHC9kgATyuGNUOeXYBHHbkCgYEA78Zq8x2ZOz6quQUolZc3 +dEzezkyHJY4KQPRe6VUesAB5riy3F4M2L5LejMQp2/WtRYsCrll3nh+P109dryRD +GpbNjQ0rWzEVyZ7u4LzRiQ43GzbFfCt+et9czUWcEIRAu7Ne7jlTSZSk03Ymv+Ns +h8jGAkTiP6C2Y1oudN7ywtsCgYBAWIa3Z+oDUQjcJD4adWxW3wSU71oSINASSV/n +nloiuRDFFVe2nYwYqbhokNTUIVXzuwlmr0LI3aBnJoVENB1FkgMjQ/ziMtvBAB3S +qS24cxe26YFykJRdtIR+HTEKE271hLsNsAVdo6ATSDey/oOkCIYGZzmocQNaks8Z +dkpMCQKBgQCfZ75r1l/Hzphb78Ygf9tOz1YUFqw/xY9jfufW4C/5SgV2q2t/AZok +LixyPP8SzJcH20iKdc9kS7weiQA0ldT2SYv6VT7IqgQ3i/qYdOmaggjBGaIuIB/B +QZOJBnaSMVJFf/ZO1/1ilGVGfZZ3TMOA1TJlcTZisk56tRTbkivL9Q== +-----END RSA PRIVATE KEY-----''') LIBJS = readfile_or_default(os.path.join(os.path.dirname(__file__), 'lib', 'lib.js'), @@ -33,9 +66,7 @@ class Check: # Public - def __call__(self): - token = request.values.get('jwt') - service = request.values.get('service') + def __call__(self, token, service): if token is not None and service is not None: try: token = jwt.decode(token, PRIVATE_KEY) @@ -58,12 +89,12 @@ def __call__(self): token = jwt.encode(ret, PRIVATE_KEY, algorithm='RS256')\ .decode('ascii') ret['token'] = token - return jsonpify(ret) + return ret ret = { 'permissions': {} } - return jsonpify(ret) + return ret class PublicKey: diff --git a/conductor/blueprints/datastore/blueprint.py b/conductor/blueprints/datastore/blueprint.py index 3614eac..8fdcb4e 100644 --- a/conductor/blueprints/datastore/blueprint.py +++ b/conductor/blueprints/datastore/blueprint.py @@ -1,4 +1,6 @@ -from flask import Blueprint +import json + +from flask import Blueprint, request, Response from . import controllers @@ -9,9 +11,20 @@ def create(): # Create instase blueprint = Blueprint('datastore', 'datastore') + # Controller proxies + authorize_controller = controllers.Authorize() + + def authorize(): + auth_token = request.headers.get('Auth-Token') + try: + req_payload = json.loads(request.data.decode()) + return authorize_controller(auth_token, req_payload) + except json.JSONDecodeError: + return Response(status=400) + # Register routes blueprint.add_url_rule( - '/', 'authorize', controllers.Authorize(), methods=['POST']) + '/', 'authorize', authorize, methods=['POST']) # Return blueprint return blueprint diff --git a/conductor/blueprints/datastore/controllers.py b/conductor/blueprints/datastore/controllers.py index dbad6ac..88aae26 100644 --- a/conductor/blueprints/datastore/controllers.py +++ b/conductor/blueprints/datastore/controllers.py @@ -30,14 +30,11 @@ def __init__(self): self.__bucket = self.__connection.get_bucket( config.OPENSPENDING_STORAGE_BUCKET_NAME) - def __call__(self): - + def __call__(self, auth_token, req_payload): # Verify client, deny access if not verified - auth_token = request.headers.get('Auth-Token') try: # Get request payload - req_payload = json.loads(request.data.decode()) owner = req_payload.get('metadata', {}).get('owner') dataset_name = req_payload.get('metadata', {}).get('name') if owner is None or dataset_name is None: @@ -79,6 +76,7 @@ def __call__(self): except Exception as exception: + raise # TODO: use logger # Log bad request exception print('Bad request: {0}'.format(exception)) diff --git a/tests/module/blueprints/authentication/__init__.py b/tests/module/blueprints/authentication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/module/blueprints/authentication/test_blueprint.py b/tests/module/blueprints/authentication/test_blueprint.py new file mode 100644 index 0000000..2e61339 --- /dev/null +++ b/tests/module/blueprints/authentication/test_blueprint.py @@ -0,0 +1,21 @@ +import unittest +try: + from unittest.mock import Mock, patch +except ImportError: + from mock import Mock, patch +from importlib import import_module +module = import_module('conductor.blueprints.authentication.blueprint') + + +class createTest(unittest.TestCase): + + # Actions + + def setUp(self): + self.addCleanup(patch.stopall) + self.controllers = patch.object(module, 'controllers').start() + + # Tests + + def test(self): + self.assertTrue(module.create()) diff --git a/tests/module/blueprints/authentication/test_controllers.py b/tests/module/blueprints/authentication/test_controllers.py new file mode 100644 index 0000000..372406b --- /dev/null +++ b/tests/module/blueprints/authentication/test_controllers.py @@ -0,0 +1,130 @@ +import unittest + +import flask +import jwt +import datetime +from collections import namedtuple + +try: + from unittest.mock import Mock, patch +except ImportError: + from mock import Mock, patch +from importlib import import_module +module = import_module('conductor.blueprints.authentication.controllers') + + +class AuthenticationTest(unittest.TestCase): + + # Actions + + def setUp(self): + + # Cleanup + self.addCleanup(patch.stopall) + + self.goog_provider = namedtuple("resp",['headers'])({'Location':'google'}) + self.oauth_response = { + 'access_token': 'access_token' + } + module.google_remote_app = Mock( + return_value=namedtuple('google_remote_app', + ['authorize', 'authorized_response']) + (authorize=lambda **kwargs: self.goog_provider, + authorized_response=lambda **kwargs: self.oauth_response) + ) + module.get_user = Mock( + return_value=namedtuple('User', + ['name','email','avatar_url']) + ('moshe','email@moshe.com','http://google.com') + ) + module.get_user_profile = Mock( + return_value={ + 'id': 'userid', + 'name': 'Moshe', + 'email': 'email@moshe.com', + 'picture': 'http://moshe.com/picture' + } + ) + + # Tests + + def test___check___no_jwt(self): + ret = module.Check()(None, 'next', 'callback') + self.assertFalse(ret.get('authenticated')) + self.assertIsNotNone(ret.get('providers',{}).get('google')) + + def test___check___bad_jwt(self): + ret = module.Check()('bla', 'next', 'callback') + self.assertFalse(ret.get('authenticated')) + self.assertIsNotNone(ret.get('providers',{}).get('google')) + + def test___check___good_jwt_no_such_user(self): + module.get_user = Mock( + return_value=None + ) + token = { + 'userid': 'userid', + 'exp': (datetime.datetime.utcnow() + + datetime.timedelta(days=14)) + } + client_token = jwt.encode(token, 'private key stub') + ret = module.Check()(client_token, 'next', 'callback') + self.assertFalse(ret.get('authenticated')) + self.assertIsNotNone(ret.get('providers',{}).get('google')) + + def test___check___expired_jwt(self): + token = { + 'userid': 'userid', + 'exp': (datetime.datetime.utcnow() - + datetime.timedelta(days=1)) + } + client_token = jwt.encode(token, 'private key stub') + ret = module.Check()(client_token, 'next', 'callback') + self.assertFalse(ret.get('authenticated')) + self.assertIsNotNone(ret.get('providers',{}).get('google')) + + def test___check___good_jwt(self): + token = { + 'userid': 'userid', + 'exp': (datetime.datetime.utcnow() + + datetime.timedelta(days=14)) + } + client_token = jwt.encode(token, 'private key stub') + ret = module.Check()(client_token, 'next', 'callback') + self.assertTrue(ret.get('authenticated')) + self.assertIsNotNone(ret.get('profile')) + self.assertEquals(ret['profile']['email'],'email@moshe.com') + self.assertEquals(ret['profile']['avatar_url'],'http://google.com') + self.assertEquals(ret['profile']['name'],'moshe') + + def test___callback___good_response(self): + token = { + 'next': 'http://next.com/', + 'provider': 'dummy', + 'exp': (datetime.datetime.utcnow() + + datetime.timedelta(days=14)) + } + state = jwt.encode(token, 'private key stub') + ret = module.Callback()(state) + self.assertEqual(ret.status_code, 302) + print(ret.headers['Location']) + self.assertTrue('jwt' in ret.headers['Location']) + + def test___callback___bad_response(self): + self.oauth_response = None + token = { + 'next': 'http://next.com/', + 'provider': 'dummy', + 'exp': (datetime.datetime.utcnow() + + datetime.timedelta(days=14)) + } + state = jwt.encode(token, 'private key stub') + ret = module.Callback()(state) + self.assertEqual(ret.status_code, 302) + self.assertFalse('jwt' in ret.headers['Location']) + + def test___callback___bad_state(self): + ret = module.Callback()("das") + self.assertEqual(ret.status_code, 302) + self.assertFalse('jwt' in ret.headers['Location']) + diff --git a/tests/module/blueprints/authorization/__init__.py b/tests/module/blueprints/authorization/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/module/blueprints/authorization/test_blueprint.py b/tests/module/blueprints/authorization/test_blueprint.py new file mode 100644 index 0000000..cbf1a3d --- /dev/null +++ b/tests/module/blueprints/authorization/test_blueprint.py @@ -0,0 +1,21 @@ +import unittest +try: + from unittest.mock import Mock, patch +except ImportError: + from mock import Mock, patch +from importlib import import_module +module = import_module('conductor.blueprints.authorization.blueprint') + + +class createTest(unittest.TestCase): + + # Actions + + def setUp(self): + self.addCleanup(patch.stopall) + self.controllers = patch.object(module, 'controllers').start() + + # Tests + + def test(self): + self.assertTrue(module.create()) diff --git a/tests/module/blueprints/authorization/test_controllers.py b/tests/module/blueprints/authorization/test_controllers.py new file mode 100644 index 0000000..c286cb7 --- /dev/null +++ b/tests/module/blueprints/authorization/test_controllers.py @@ -0,0 +1,55 @@ +import unittest + +import jwt +from collections import namedtuple + +try: + from unittest.mock import Mock, patch +except ImportError: + from mock import Mock, patch +from importlib import import_module +module = import_module('conductor.blueprints.authorization.controllers') + + +class AuthorizationTest(unittest.TestCase): + + # Actions + + def setUp(self): + + # Cleanup + self.addCleanup(patch.stopall) + + goog_provider = namedtuple("resp",['headers'])({'Location':'google'}) + oauth_response = { + 'access_token': 'access_token' + } + module.google_remote_app = Mock( + return_value=namedtuple('google_remote_app', + ['authorize', 'authorized_response']) + (authorize=lambda **kwargs:goog_provider, + authorized_response=lambda **kwargs:oauth_response) + ) + + # Tests + + def test___check___no_token(self): + ret = module.Check()(None, 'service') + self.assertEquals(len(ret.get('permissions')), 0) + + def test___check___no_service(self): + ret = module.Check()('token', 'service') + self.assertEquals(len(ret.get('permissions')), 0) + + def test___check___bad_token(self): + ret = module.Check()('token', 'service') + self.assertEquals(len(ret.get('permissions')), 0) + + def test___check___good_token(self): + token = { + 'userid': 'userid', + } + client_token = jwt.encode(token, module.PRIVATE_KEY) + ret = module.Check()(client_token, 'os.datastore') + self.assertEquals(ret.get('service'), 'os.datastore') + self.assertGreater(len(ret.get('permissions',{})), 0) diff --git a/tests/module/blueprints/datastore/test_controllers.py b/tests/module/blueprints/datastore/test_controllers.py index 4bbc853..b398712 100644 --- a/tests/module/blueprints/datastore/test_controllers.py +++ b/tests/module/blueprints/datastore/test_controllers.py @@ -7,8 +7,23 @@ from importlib import import_module module = import_module('conductor.blueprints.datastore.controllers') +AUTH_TOKEN = "token" +PAYLOAD = { + 'metadata': { + 'owner': 'owner', + 'name': 'name', + }, + 'filedata': { + 'data/file1': { + 'name': 'file1', + 'length': 100, + 'md5': 'aaa', + }, + }, +} -class AuthorizeTest(unittest.TestCase): + +class DataStoreTest(unittest.TestCase): # Actions @@ -19,19 +34,6 @@ def setUp(self): # Request patch self.request = patch.object(module, 'request').start() - self.request.data.decode = Mock(return_value=json.dumps({ - 'metadata': { - 'owner': 'owner', - 'name': 'name', - }, - 'filedata': { - 'data/file1': { - 'name': 'file1', - 'length': 100, - 'md5': 'aaa', - }, - }, - })) # Various patches self.services = patch.object(module, 'services').start() @@ -39,25 +41,27 @@ def setUp(self): self.boto = patch.object(module, 'boto').start() self.bucket = self.boto.connect_s3().get_bucket() self.bucket.new_key().generate_url = Mock( - return_value='http://test.com?key=value') + return_value='http://test.com?key=value') # Tests def test___call___not_authorized(self): authorize = module.Authorize() self.services.verify = Mock(return_value=False) - self.assertEqual(authorize().status, '401 UNAUTHORIZED') + self.assertEqual(authorize(AUTH_TOKEN, PAYLOAD).status, '401 UNAUTHORIZED') def test___call___bad_request(self): authorize = module.Authorize() - self.request.data.decode = Mock(return_value=json.dumps({ + self.assertEqual(authorize(AUTH_TOKEN, { 'bad': 'data', - })) - self.assertEqual(authorize().status, '400 BAD REQUEST') + }).status, '400 BAD REQUEST') def test___call___good_request(self): + self.services.verify = Mock(return_value=True) authorize = module.Authorize() - self.assertEqual(json.loads(authorize()), { + ret = authorize(AUTH_TOKEN, PAYLOAD) + self.assertIs(type(ret),str) + self.assertEqual(json.loads(ret), { 'filedata': { 'data/file1': { 'name': 'file1',