From 3d9fe76271060ce570ae63f5c78a1720574b8b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 8 Apr 2016 18:45:35 +0200 Subject: [PATCH] Added subclient token verification & storage. --- pillar/application/__init__.py | 2 + pillar/application/modules/blender_id.py | 106 +++++++++++++++++++++ pillar/application/utils/authentication.py | 15 ++- pillar/config.py | 3 + 4 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 pillar/application/modules/blender_id.py diff --git a/pillar/application/__init__.py b/pillar/application/__init__.py index 3de98b41..24a84bfd 100644 --- a/pillar/application/__init__.py +++ b/pillar/application/__init__.py @@ -380,5 +380,7 @@ file_storage.setup_app(app, url_prefix='/storage') # The encoding module (receive notification and report progress) from modules.encoding import encoding +from modules.blender_id import blender_id app.register_blueprint(encoding, url_prefix='/encoding') +app.register_blueprint(blender_id, url_prefix='/blender_id') diff --git a/pillar/application/modules/blender_id.py b/pillar/application/modules/blender_id.py new file mode 100644 index 00000000..629b1e79 --- /dev/null +++ b/pillar/application/modules/blender_id.py @@ -0,0 +1,106 @@ +"""Blender ID subclient endpoint.""" + +import logging +from pprint import pformat + +import requests +from flask import Blueprint, request, current_app, abort +from eve.methods.post import post_internal +from eve.methods.put import put_internal + +from application.utils import authentication, remove_private_keys + +blender_id = Blueprint('blender_id', __name__) +log = logging.getLogger(__name__) + + +@blender_id.route('/store_scst', methods=['POST']) +def store_subclient_token(): + """Verifies & stores a user's subclient-specific token.""" + + user_id = request.form['user_id'] + scst = request.form['scst'] + + # Verify with Blender ID + log.debug('Storing SCST for BlenderID user %s', user_id) + user_info = validate_subclient_token(user_id, scst) + + if user_info is None: + log.warning('Unable to verify subclient token with Blender ID.') + return 'BLENDER ID ERROR', 403 + + # Store the user info in MongoDB. + log.info('Obtained user info from Blender ID: %s', user_info) + db_user = find_user_in_db(user_id, scst, **user_info) + log.debug('Storing updated/created user:\n%s', pformat(db_user)) + + if '_id' in db_user: + db_id = db_user['_id'] + r, _, _, status = put_internal('users', remove_private_keys(db_user), _id=db_id) + else: + r, _, _, status = post_internal('users', db_user) + if status != 200: + log.error('internal response: %r %r', status, r) + return abort(500) + + return 'OK', 200 + + +def validate_subclient_token(user_id, scst): + """Verifies a subclient token with Blender ID. + + :returns: the user information from Blender ID on success, in a dict + {'email': 'a@b', 'full_name': 'AB'}, or None on failure. + :rtype: dict + """ + + client_id = current_app.config['BLENDER_ID_CLIENT_ID'] + subclient_id = current_app.config['BLENDER_ID_SUBCLIENT_ID'] + + log.debug('Validating subclient token %s for Blender ID user %s', scst, user_id) + payload = {'client_id': client_id, + 'subclient_id': subclient_id, + 'user_id': user_id, + 'scst': scst} + url = '{0}/subclients/validate_token'.format(authentication.blender_id_endpoint()) + log.debug('POSTing to %r', url) + + # POST to Blender ID, handling errors as negative verification results. + try: + r = requests.post(url, data=payload) + except requests.exceptions.ConnectionError as e: + log.error('Connection error trying to POST to %s, handling as invalid token.', url) + return None + + if r.status_code != 200: + log.info('Token invalid, HTTP status %i returned', r.status_code) + return None + + resp = r.json() + if resp['status'] != 'success': + log.warning('Failed response from %s: %s', url, resp) + return None + + return resp['user'] + + +def find_user_in_db(user_id, scst, email, full_name): + users = current_app.data.driver.db['users'] + + query = {'auth': {'$elemMatch': {'user_id': user_id, 'provider': 'blender-id'}}} + log.debug('Querying: %s', query) + db_user = users.find_one(query) + + if db_user: + log.debug('User %r already in our database, updating with info from Blender ID.', user_id) + db_user['full_name'] = full_name + db_user['email'] = email + + auth = next(auth for auth in db_user['auth'] if auth['provider'] == 'blender-id') + auth['token'] = scst + return db_user + + log.debug('User %r not yet in our database, create a new one.', user_id) + db_user = authentication.create_new_user_document(email, user_id, full_name, token=scst) + db_user['username'] = authentication.make_unique_username(email) + return db_user diff --git a/pillar/application/utils/authentication.py b/pillar/application/utils/authentication.py index 7d676fd0..ee96fb2f 100644 --- a/pillar/application/utils/authentication.py +++ b/pillar/application/utils/authentication.py @@ -134,6 +134,15 @@ def create_new_user(email, username, user_id): @returns: the user ID from our local database. """ + user_data = create_new_user_document(email, user_id, username) + r = post_internal('users', user_data) + user_id = r[0]['_id'] + return user_id + + +def create_new_user_document(email, user_id, username, token=''): + """Creates a new user document, without storing it in MongoDB.""" + user_data = { 'full_name': username, 'username': username, @@ -141,14 +150,12 @@ def create_new_user(email, username, user_id): 'auth': [{ 'provider': 'blender-id', 'user_id': str(user_id), - 'token': ''}], + 'token': token}], 'settings': { 'email_communications': 1 } } - r = post_internal('users', user_data) - user_id = r[0]['_id'] - return user_id + return user_data def make_unique_username(email): diff --git a/pillar/config.py b/pillar/config.py index 92df469c..8216b531 100644 --- a/pillar/config.py +++ b/pillar/config.py @@ -64,3 +64,6 @@ FILE_LINK_VALIDITY = defaultdict( gcs=3600 * 23, # 23 hours for Google Cloud Storage. ) +# Client and Subclient IDs for Blender ID +BLENDER_ID_CLIENT_ID = 'SPECIAL-SNOWFLAKE-57' +BLENDER_ID_SUBCLIENT_ID = 'PILLAR'