Unify tokens and subclient tokens

SCST tokens are now stored in the 'tokens' table.
This unifies old token handling and new subclient-specific tokens.
Also ensures the BlenderID expiry of the token is taken into account.

Removes use of httpretty, in favour of responses.
This commit is contained in:
2016-04-13 15:33:54 +02:00
parent 0f6eeef32b
commit 66eeb25529
6 changed files with 250 additions and 190 deletions

View File

@@ -1,8 +1,13 @@
"""Blender ID subclient endpoint."""
"""Blender ID subclient endpoint.
Also contains functionality for other parts of Pillar to perform communication
with Blender ID.
"""
import logging
from pprint import pformat
import datetime
from bson import tz_util
import requests
from flask import Blueprint, request, current_app, abort, jsonify
from eve.methods.post import post_internal
@@ -19,20 +24,51 @@ def store_subclient_token():
"""Verifies & stores a user's subclient-specific token."""
user_id = request.form['user_id'] # User ID at BlenderID
scst = request.form['scst']
subclient_id = request.form['subclient_id']
scst = request.form['token']
# Verify with Blender ID
log.debug('Storing SCST for BlenderID user %s', user_id)
user_info = validate_subclient_token(user_id, scst)
db_user, status = validate_create_user(user_id, scst, subclient_id)
if user_info is None:
if db_user is None:
log.warning('Unable to verify subclient token with Blender ID.')
return jsonify({'status': 'fail',
'error': 'BLENDER ID ERROR'}), 403
# Store the user info in MongoDB.
return jsonify({'status': 'success',
'subclient_user_id': str(db_user['_id'])}), status
def blender_id_endpoint():
"""Gets the endpoint for the authentication API. If the env variable
is defined, it's possible to override the (default) production address.
"""
return current_app.config['BLENDER_ID_ENDPOINT'].rstrip('/')
def validate_create_user(blender_id_user_id, token, oauth_subclient_id):
"""Validates a user against Blender ID, creating the user in our database.
:param blender_id_user_id: the user ID at the BlenderID server.
:param token: the OAuth access token.
:param oauth_subclient_id: the subclient ID, or empty string if not a subclient.
:returns: (user in MongoDB, HTTP status 200 or 201)
"""
# Verify with Blender ID
log.debug('Storing token for BlenderID user %s', blender_id_user_id)
user_info, token_expiry = validate_token(blender_id_user_id, token, oauth_subclient_id)
if user_info is None:
log.warning('Unable to verify token with Blender ID.')
return None, None
# Blender ID can be queried without user ID, and will always include the
# correct user ID in its response.
log.info('Obtained user info from Blender ID: %s', user_info)
db_user = find_user_in_db(user_id, scst, **user_info)
blender_id_user_id = user_info['user_id']
# Store the user info in MongoDB.
db_user = find_user_in_db(blender_id_user_id, user_info)
if '_id' in db_user:
# Update the existing user
@@ -42,32 +78,43 @@ def store_subclient_token():
# Create a new user
r, _, _, status = post_internal('users', db_user)
db_id = r['_id']
db_user.update(r) # update with database/eve-generated fields.
if status not in (200, 201):
log.error('internal response: %r %r', status, r)
return abort(500)
return jsonify({'status': 'success',
'subclient_user_id': str(db_id)}), status
# Store the token in MongoDB.
authentication.store_token(db_id, token, token_expiry)
return db_user, status
def validate_subclient_token(user_id, scst):
def validate_token(user_id, token, oauth_subclient_id):
"""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.
:returns: (user info, token expiry) on success, or (None, None) on failure.
The user information from Blender ID is returned as dict
{'email': 'a@b', 'full_name': 'AB'}, token expiry as a datime.datetime.
:rtype: dict
"""
client_id = current_app.config['BLENDER_ID_CLIENT_ID']
subclient_id = current_app.config['BLENDER_ID_SUBCLIENT_ID']
our_subclient_id = current_app.config['BLENDER_ID_SUBCLIENT_ID']
log.debug('Validating subclient token for Blender ID user %s', 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())
# Check that IF there is a subclient ID given, it is the correct one.
if oauth_subclient_id and our_subclient_id != oauth_subclient_id:
log.warning('validate_token(): BlenderID user %s is trying to use the wrong subclient '
'ID %r; treating as invalid login.', user_id, oauth_subclient_id)
return None, None
# Validate against BlenderID.
log.debug('Validating subclient token for BlenderID user %s', user_id)
payload = {'user_id': user_id,
'token': token}
if oauth_subclient_id:
payload['subclient_id'] = oauth_subclient_id
url = '{0}/subclients/validate_token'.format(blender_id_endpoint())
log.debug('POSTing to %r', url)
# POST to Blender ID, handling errors as negative verification results.
@@ -75,40 +122,60 @@ def validate_subclient_token(user_id, scst):
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
return None, None
if r.status_code != 200:
log.info('Token invalid, HTTP status %i returned', r.status_code)
return None
return None, None
resp = r.json()
if resp['status'] != 'success':
log.warning('Failed response from %s: %s', url, resp)
return None
return None, None
return resp['user']
expires = _compute_token_expiry(resp['token_expires'])
return resp['user'], expires
def find_user_in_db(user_id, scst, email, full_name):
"""Find the user in our database, creating/updating it where needed."""
def _compute_token_expiry(token_expires_string):
"""Computes token expiry based on current time and BlenderID expiry.
Expires our side of the token when either the BlenderID token expires,
or in one hour. The latter case is to ensure we periodically verify
the token.
"""
date_format = current_app.config['RFC1123_DATE_FORMAT']
blid_expiry = datetime.datetime.strptime(token_expires_string, date_format)
blid_expiry = blid_expiry.replace(tzinfo=tz_util.utc)
our_expiry = datetime.datetime.now(tz=tz_util.utc) + datetime.timedelta(hours=1)
return min(blid_expiry, our_expiry)
def find_user_in_db(blender_id_user_id, user_info):
"""Find the user in our database, creating/updating the returned document where needed.
Does NOT update the user in the database.
"""
users = current_app.data.driver.db['users']
query = {'auth': {'$elemMatch': {'user_id': user_id, 'provider': 'blender-id'}}}
query = {'auth': {'$elemMatch': {'user_id': str(blender_id_user_id),
'provider': 'blender-id'}}}
log.debug('Querying: %s', query)
db_user = users.find_one(query)
# TODO: include token expiry in database.
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
log.debug('User blender_id_user_id=%r already in our database, '
'updating with info from Blender ID.', blender_id_user_id)
db_user['full_name'] = user_info['full_name']
db_user['email'] = user_info['email']
else:
log.debug('User %r not yet in our database, create a new one.', blender_id_user_id)
db_user = authentication.create_new_user_document(user_info['email'], blender_id_user_id,
user_info['full_name'])
db_user['username'] = authentication.make_unique_username(user_info['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