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:
@@ -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
|
import logging
|
||||||
from pprint import pformat
|
import datetime
|
||||||
|
|
||||||
|
from bson import tz_util
|
||||||
import requests
|
import requests
|
||||||
from flask import Blueprint, request, current_app, abort, jsonify
|
from flask import Blueprint, request, current_app, abort, jsonify
|
||||||
from eve.methods.post import post_internal
|
from eve.methods.post import post_internal
|
||||||
@@ -19,20 +24,51 @@ def store_subclient_token():
|
|||||||
"""Verifies & stores a user's subclient-specific token."""
|
"""Verifies & stores a user's subclient-specific token."""
|
||||||
|
|
||||||
user_id = request.form['user_id'] # User ID at BlenderID
|
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
|
db_user, status = validate_create_user(user_id, scst, subclient_id)
|
||||||
log.debug('Storing SCST for BlenderID user %s', user_id)
|
|
||||||
user_info = validate_subclient_token(user_id, scst)
|
|
||||||
|
|
||||||
if user_info is None:
|
if db_user is None:
|
||||||
log.warning('Unable to verify subclient token with Blender ID.')
|
log.warning('Unable to verify subclient token with Blender ID.')
|
||||||
return jsonify({'status': 'fail',
|
return jsonify({'status': 'fail',
|
||||||
'error': 'BLENDER ID ERROR'}), 403
|
'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)
|
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:
|
if '_id' in db_user:
|
||||||
# Update the existing user
|
# Update the existing user
|
||||||
@@ -42,32 +78,43 @@ def store_subclient_token():
|
|||||||
# Create a new user
|
# Create a new user
|
||||||
r, _, _, status = post_internal('users', db_user)
|
r, _, _, status = post_internal('users', db_user)
|
||||||
db_id = r['_id']
|
db_id = r['_id']
|
||||||
|
db_user.update(r) # update with database/eve-generated fields.
|
||||||
|
|
||||||
if status not in (200, 201):
|
if status not in (200, 201):
|
||||||
log.error('internal response: %r %r', status, r)
|
log.error('internal response: %r %r', status, r)
|
||||||
return abort(500)
|
return abort(500)
|
||||||
|
|
||||||
return jsonify({'status': 'success',
|
# Store the token in MongoDB.
|
||||||
'subclient_user_id': str(db_id)}), status
|
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.
|
"""Verifies a subclient token with Blender ID.
|
||||||
|
|
||||||
:returns: the user information from Blender ID on success, in a dict
|
:returns: (user info, token expiry) on success, or (None, None) on failure.
|
||||||
{'email': 'a@b', 'full_name': 'AB'}, or 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
|
:rtype: dict
|
||||||
"""
|
"""
|
||||||
|
|
||||||
client_id = current_app.config['BLENDER_ID_CLIENT_ID']
|
our_subclient_id = current_app.config['BLENDER_ID_SUBCLIENT_ID']
|
||||||
subclient_id = current_app.config['BLENDER_ID_SUBCLIENT_ID']
|
|
||||||
|
|
||||||
log.debug('Validating subclient token for Blender ID user %s', user_id)
|
# Check that IF there is a subclient ID given, it is the correct one.
|
||||||
payload = {'client_id': client_id,
|
if oauth_subclient_id and our_subclient_id != oauth_subclient_id:
|
||||||
'subclient_id': subclient_id,
|
log.warning('validate_token(): BlenderID user %s is trying to use the wrong subclient '
|
||||||
'user_id': user_id,
|
'ID %r; treating as invalid login.', user_id, oauth_subclient_id)
|
||||||
'scst': scst}
|
return None, None
|
||||||
url = '{0}/subclients/validate_token'.format(authentication.blender_id_endpoint())
|
|
||||||
|
# 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)
|
log.debug('POSTing to %r', url)
|
||||||
|
|
||||||
# POST to Blender ID, handling errors as negative verification results.
|
# 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)
|
r = requests.post(url, data=payload)
|
||||||
except requests.exceptions.ConnectionError as e:
|
except requests.exceptions.ConnectionError as e:
|
||||||
log.error('Connection error trying to POST to %s, handling as invalid token.', url)
|
log.error('Connection error trying to POST to %s, handling as invalid token.', url)
|
||||||
return None
|
return None, None
|
||||||
|
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
log.info('Token invalid, HTTP status %i returned', r.status_code)
|
log.info('Token invalid, HTTP status %i returned', r.status_code)
|
||||||
return None
|
return None, None
|
||||||
|
|
||||||
resp = r.json()
|
resp = r.json()
|
||||||
if resp['status'] != 'success':
|
if resp['status'] != 'success':
|
||||||
log.warning('Failed response from %s: %s', url, resp)
|
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):
|
def _compute_token_expiry(token_expires_string):
|
||||||
"""Find the user in our database, creating/updating it where needed."""
|
"""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']
|
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)
|
log.debug('Querying: %s', query)
|
||||||
db_user = users.find_one(query)
|
db_user = users.find_one(query)
|
||||||
|
|
||||||
# TODO: include token expiry in database.
|
|
||||||
if db_user:
|
if db_user:
|
||||||
log.debug('User %r already in our database, updating with info from Blender ID.', user_id)
|
log.debug('User blender_id_user_id=%r already in our database, '
|
||||||
db_user['full_name'] = full_name
|
'updating with info from Blender ID.', blender_id_user_id)
|
||||||
db_user['email'] = email
|
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
|
return db_user
|
||||||
|
@@ -1,9 +1,14 @@
|
|||||||
|
"""Generic authentication.
|
||||||
|
|
||||||
|
Contains functionality to validate tokens, create users and tokens, and make
|
||||||
|
unique usernames from emails. Calls out to the application.modules.blender_id
|
||||||
|
module for Blender ID communication.
|
||||||
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import requests
|
from bson import tz_util
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timedelta
|
|
||||||
from flask import g
|
from flask import g
|
||||||
from flask import request
|
from flask import request
|
||||||
from eve.methods.post import post_internal
|
from eve.methods.post import post_internal
|
||||||
@@ -13,41 +18,6 @@ from application import app
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
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 app.config['BLENDER_ID_ENDPOINT'].rstrip('/')
|
|
||||||
|
|
||||||
|
|
||||||
def validate(token):
|
|
||||||
"""Validate a token against the Blender ID server. This simple lookup
|
|
||||||
returns a dictionary with the following keys:
|
|
||||||
|
|
||||||
- message: a success message
|
|
||||||
- valid: a boolean, stating if the token is valid
|
|
||||||
- user: a dictionary with information regarding the user
|
|
||||||
"""
|
|
||||||
|
|
||||||
log.debug("Validating token %s", token)
|
|
||||||
payload = dict(
|
|
||||||
token=token)
|
|
||||||
url = "{0}/u/validate_token".format(blender_id_endpoint())
|
|
||||||
|
|
||||||
try:
|
|
||||||
log.debug('POSTing to %r', url)
|
|
||||||
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('HTTP error %i validating token: %s', r.status_code, r.content)
|
|
||||||
return None
|
|
||||||
|
|
||||||
return r.json()
|
|
||||||
|
|
||||||
|
|
||||||
def validate_token():
|
def validate_token():
|
||||||
"""Validate the token provided in the request and populate the current_user
|
"""Validate the token provided in the request and populate the current_user
|
||||||
flask.g object, so that permissions and access to a resource can be defined
|
flask.g object, so that permissions and access to a resource can be defined
|
||||||
@@ -70,79 +40,68 @@ def validate_token():
|
|||||||
|
|
||||||
# Check the users to see if there is one with this Blender ID token.
|
# Check the users to see if there is one with this Blender ID token.
|
||||||
token = request.authorization.username
|
token = request.authorization.username
|
||||||
db_user = find_user_by_token(token)
|
oauth_subclient = request.authorization.password
|
||||||
if db_user is not None:
|
|
||||||
log.debug(u'Token for %s found as locally stored blender-id subclient token.',
|
|
||||||
db_user['full_name'])
|
|
||||||
current_user = dict(
|
|
||||||
user_id=db_user['_id'],
|
|
||||||
token=token,
|
|
||||||
groups=db_user['groups'],
|
|
||||||
token_expire_time=datetime.now() + timedelta(hours=1) # TODO: get from Blender ID
|
|
||||||
)
|
|
||||||
g.current_user = current_user
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Fall back to deprecated behaviour.
|
db_token = find_token(token)
|
||||||
log.debug('Token not found as locally stored blender-id subclient token; '
|
|
||||||
'falling back on deprecated behaviour.')
|
|
||||||
|
|
||||||
tokens_collection = app.data.driver.db['tokens']
|
|
||||||
|
|
||||||
lookup = {'token': token, 'expire_time': {"$gt": datetime.now()}}
|
|
||||||
db_token = tokens_collection.find_one(lookup)
|
|
||||||
if not db_token:
|
if not db_token:
|
||||||
|
log.debug('Token %s not found in our local database.', token)
|
||||||
|
|
||||||
# If no valid token is found in our local database, we issue a new
|
# If no valid token is found in our local database, we issue a new
|
||||||
# request to the Blender ID server to verify the validity of the token
|
# request to the Blender ID server to verify the validity of the token
|
||||||
# passed via the HTTP header. We will get basic user info if the user
|
# passed via the HTTP header. We will get basic user info if the user
|
||||||
# is authorized, and we will store the token in our local database.
|
# is authorized, and we will store the token in our local database.
|
||||||
validation = validate(token)
|
from application.modules import blender_id
|
||||||
if validation is None or validation.get('status', '') != 'success':
|
|
||||||
log.debug('Validation failed, result is %r', validation)
|
|
||||||
return False
|
|
||||||
|
|
||||||
users = app.data.driver.db['users']
|
db_user, status = blender_id.validate_create_user('', token, oauth_subclient)
|
||||||
email = validation['data']['user']['email']
|
|
||||||
db_user = users.find_one({'email': email})
|
|
||||||
username = make_unique_username(email)
|
|
||||||
|
|
||||||
if not db_user:
|
|
||||||
# We don't even know this user; create it on the fly.
|
|
||||||
log.debug('Validation success, creating new user in our database.')
|
|
||||||
user_id = create_new_user(
|
|
||||||
email, username, validation['data']['user']['id'])
|
|
||||||
groups = None
|
|
||||||
else:
|
|
||||||
log.debug('Validation success, user is already in our database.')
|
|
||||||
user_id = db_user['_id']
|
|
||||||
groups = db_user['groups']
|
|
||||||
|
|
||||||
token_data = {
|
|
||||||
'user': user_id,
|
|
||||||
'token': token,
|
|
||||||
'expire_time': datetime.now() + timedelta(hours=1)
|
|
||||||
}
|
|
||||||
post_internal('tokens', token_data)
|
|
||||||
current_user = dict(
|
|
||||||
user_id=user_id,
|
|
||||||
token=token,
|
|
||||||
groups=groups,
|
|
||||||
token_expire_time=token_data['expire_time'])
|
|
||||||
else:
|
else:
|
||||||
log.debug("User is already in our database and token hasn't expired yet.")
|
log.debug("User is already in our database and token hasn't expired yet.")
|
||||||
users = app.data.driver.db['users']
|
users = app.data.driver.db['users']
|
||||||
db_user = users.find_one(db_token['user'])
|
db_user = users.find_one(db_token['user'])
|
||||||
current_user = dict(
|
|
||||||
user_id=db_token['user'],
|
|
||||||
token=db_token['token'],
|
|
||||||
groups=db_user['groups'],
|
|
||||||
token_expire_time=db_token['expire_time'])
|
|
||||||
|
|
||||||
g.current_user = current_user
|
if db_user is None:
|
||||||
|
log.debug('Validation failed, user not logged in')
|
||||||
|
return False
|
||||||
|
|
||||||
|
g.current_user = {'user_id': db_user['_id'],
|
||||||
|
'groups': db_user['groups']}
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def find_token(token, **extra_filters):
|
||||||
|
"""Returns the token document, or None if it doesn't exist (or is expired)."""
|
||||||
|
|
||||||
|
tokens_collection = app.data.driver.db['tokens']
|
||||||
|
|
||||||
|
# TODO: remove expired tokens from collection.
|
||||||
|
lookup = {'token': token,
|
||||||
|
'expire_time': {"$gt": datetime.now(tz=tz_util.utc)}}
|
||||||
|
lookup.update(extra_filters)
|
||||||
|
|
||||||
|
db_token = tokens_collection.find_one(lookup)
|
||||||
|
return db_token
|
||||||
|
|
||||||
|
|
||||||
|
def store_token(user_id, token, token_expiry):
|
||||||
|
"""Stores an authentication token.
|
||||||
|
|
||||||
|
:returns: the token document from MongoDB
|
||||||
|
"""
|
||||||
|
|
||||||
|
token_data = {
|
||||||
|
'user': user_id,
|
||||||
|
'token': token,
|
||||||
|
'expire_time': token_expiry,
|
||||||
|
}
|
||||||
|
r, _, _, status = post_internal('tokens', token_data)
|
||||||
|
|
||||||
|
if status not in {200, 201}:
|
||||||
|
log.error('Unable to store authentication token: %s', r)
|
||||||
|
raise RuntimeError('Unable to store authentication token.')
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def create_new_user(email, username, user_id):
|
def create_new_user(email, username, user_id):
|
||||||
"""Creates a new user in our local database.
|
"""Creates a new user in our local database.
|
||||||
|
|
||||||
@@ -158,7 +117,7 @@ def create_new_user(email, username, user_id):
|
|||||||
return user_id
|
return user_id
|
||||||
|
|
||||||
|
|
||||||
def create_new_user_document(email, user_id, username, token=''):
|
def create_new_user_document(email, user_id, username):
|
||||||
"""Creates a new user document, without storing it in MongoDB."""
|
"""Creates a new user document, without storing it in MongoDB."""
|
||||||
|
|
||||||
user_data = {
|
user_data = {
|
||||||
@@ -168,10 +127,11 @@ def create_new_user_document(email, user_id, username, token=''):
|
|||||||
'auth': [{
|
'auth': [{
|
||||||
'provider': 'blender-id',
|
'provider': 'blender-id',
|
||||||
'user_id': str(user_id),
|
'user_id': str(user_id),
|
||||||
'token': token}],
|
'token': ''}], # TODO: remove 'token' field altogether.
|
||||||
'settings': {
|
'settings': {
|
||||||
'email_communications': 1
|
'email_communications': 1
|
||||||
}
|
},
|
||||||
|
'groups': [],
|
||||||
}
|
}
|
||||||
return user_data
|
return user_data
|
||||||
|
|
||||||
@@ -202,11 +162,3 @@ def make_unique_username(email):
|
|||||||
if user_from_username is None:
|
if user_from_username is None:
|
||||||
return unique_name
|
return unique_name
|
||||||
suffix += 1
|
suffix += 1
|
||||||
|
|
||||||
|
|
||||||
def find_user_by_token(scst):
|
|
||||||
users = app.data.driver.db['users']
|
|
||||||
|
|
||||||
query = {'auth': {'$elemMatch': {'provider': 'blender-id',
|
|
||||||
'token': scst}}}
|
|
||||||
return users.find_one(query)
|
|
||||||
|
@@ -24,6 +24,5 @@ wheel==0.24.0
|
|||||||
zencoder==0.6.5
|
zencoder==0.6.5
|
||||||
|
|
||||||
# development requirements
|
# development requirements
|
||||||
httpretty==0.8.14
|
|
||||||
pytest==2.9.1
|
pytest==2.9.1
|
||||||
responses==0.5.1
|
responses==0.5.1
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import copy
|
import copy
|
||||||
import sys
|
import sys
|
||||||
@@ -9,7 +11,7 @@ from bson import ObjectId
|
|||||||
from eve.tests import TestMinimal
|
from eve.tests import TestMinimal
|
||||||
import pymongo.collection
|
import pymongo.collection
|
||||||
from flask.testing import FlaskClient
|
from flask.testing import FlaskClient
|
||||||
import httpretty
|
import responses
|
||||||
|
|
||||||
from common_test_data import EXAMPLE_PROJECT, EXAMPLE_FILE
|
from common_test_data import EXAMPLE_PROJECT, EXAMPLE_FILE
|
||||||
|
|
||||||
@@ -17,6 +19,14 @@ MY_PATH = os.path.dirname(os.path.abspath(__file__))
|
|||||||
|
|
||||||
TEST_EMAIL_USER = 'koro'
|
TEST_EMAIL_USER = 'koro'
|
||||||
TEST_EMAIL_ADDRESS = '%s@testing.blender.org' % TEST_EMAIL_USER
|
TEST_EMAIL_ADDRESS = '%s@testing.blender.org' % TEST_EMAIL_USER
|
||||||
|
TEST_FULL_NAME = u'врач Сергей'
|
||||||
|
TEST_SUBCLIENT_TOKEN = 'my-subclient-token-for-pillar'
|
||||||
|
BLENDER_ID_TEST_USERID = 1896
|
||||||
|
BLENDER_ID_USER_RESPONSE = {'status': 'success',
|
||||||
|
'user': {'email': TEST_EMAIL_ADDRESS,
|
||||||
|
'full_name': TEST_FULL_NAME,
|
||||||
|
'user_id': BLENDER_ID_TEST_USERID},
|
||||||
|
'token_expires': 'Mon, 1 Jan 2018 01:02:03 GMT'}
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.DEBUG,
|
level=logging.DEBUG,
|
||||||
@@ -83,26 +93,23 @@ class AbstractPillarTest(TestMinimal):
|
|||||||
|
|
||||||
return found['_id'], found
|
return found['_id'], found
|
||||||
|
|
||||||
def htp_blenderid_validate_unhappy(self):
|
def mock_blenderid_validate_unhappy(self):
|
||||||
"""Sets up HTTPretty to mock unhappy validation flow."""
|
"""Sets up Responses to mock unhappy validation flow."""
|
||||||
|
|
||||||
httpretty.register_uri(httpretty.POST,
|
responses.add(responses.POST,
|
||||||
'%s/u/validate_token' % self.app.config['BLENDER_ID_ENDPOINT'],
|
'%s/subclients/validate_token' % self.app.config['BLENDER_ID_ENDPOINT'],
|
||||||
body=json.dumps(
|
json={'status': 'fail'},
|
||||||
{'data': {'token': 'Token is invalid'}, 'status': 'fail'}),
|
status=404)
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
def htp_blenderid_validate_happy(self):
|
def mock_blenderid_validate_happy(self):
|
||||||
"""Sets up HTTPretty to mock happy validation flow."""
|
"""Sets up Responses to mock happy validation flow."""
|
||||||
|
|
||||||
httpretty.register_uri(httpretty.POST,
|
responses.add(responses.POST,
|
||||||
'%s/u/validate_token' % self.app.config['BLENDER_ID_ENDPOINT'],
|
'%s/subclients/validate_token' % self.app.config['BLENDER_ID_ENDPOINT'],
|
||||||
body=json.dumps(
|
json=BLENDER_ID_USER_RESPONSE,
|
||||||
{'data': {'user': {'email': TEST_EMAIL_ADDRESS, 'id': 5123}},
|
status=200)
|
||||||
'status': 'success'}),
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
def make_header(self, username, password=''):
|
def make_header(self, username, subclient_id=''):
|
||||||
"""Returns a Basic HTTP Authentication header value."""
|
"""Returns a Basic HTTP Authentication header value."""
|
||||||
|
|
||||||
return 'basic ' + base64.b64encode('%s:%s' % (username, password))
|
return 'basic ' + base64.b64encode('%s:%s' % (username, subclient_id))
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import httpretty
|
import responses
|
||||||
|
|
||||||
from common_test_class import AbstractPillarTest, TEST_EMAIL_USER, TEST_EMAIL_ADDRESS
|
from common_test_class import AbstractPillarTest, TEST_EMAIL_USER, TEST_EMAIL_ADDRESS
|
||||||
|
|
||||||
@@ -15,31 +15,31 @@ class AuthenticationTests(AbstractPillarTest):
|
|||||||
auth.create_new_user(TEST_EMAIL_ADDRESS, TEST_EMAIL_USER, 'test1234')
|
auth.create_new_user(TEST_EMAIL_ADDRESS, TEST_EMAIL_USER, 'test1234')
|
||||||
self.assertEqual('%s1' % TEST_EMAIL_USER, auth.make_unique_username(TEST_EMAIL_ADDRESS))
|
self.assertEqual('%s1' % TEST_EMAIL_USER, auth.make_unique_username(TEST_EMAIL_ADDRESS))
|
||||||
|
|
||||||
@httpretty.activate
|
@responses.activate
|
||||||
def test_validate_token__not_logged_in(self):
|
def test_validate_token__not_logged_in(self):
|
||||||
from application.utils import authentication as auth
|
from application.utils import authentication as auth
|
||||||
|
|
||||||
with self.app.test_request_context():
|
with self.app.test_request_context():
|
||||||
self.assertFalse(auth.validate_token())
|
self.assertFalse(auth.validate_token())
|
||||||
|
|
||||||
@httpretty.activate
|
@responses.activate
|
||||||
def test_validate_token__unknown_token(self):
|
def test_validate_token__unknown_token(self):
|
||||||
"""Test validating of invalid token, unknown both to us and Blender ID."""
|
"""Test validating of invalid token, unknown both to us and Blender ID."""
|
||||||
|
|
||||||
from application.utils import authentication as auth
|
from application.utils import authentication as auth
|
||||||
|
|
||||||
self.htp_blenderid_validate_unhappy()
|
self.mock_blenderid_validate_unhappy()
|
||||||
with self.app.test_request_context(
|
with self.app.test_request_context(
|
||||||
headers={'Authorization': self.make_header('unknowntoken')}):
|
headers={'Authorization': self.make_header('unknowntoken')}):
|
||||||
self.assertFalse(auth.validate_token())
|
self.assertFalse(auth.validate_token())
|
||||||
|
|
||||||
@httpretty.activate
|
@responses.activate
|
||||||
def test_validate_token__unknown_but_valid_token(self):
|
def test_validate_token__unknown_but_valid_token(self):
|
||||||
"""Test validating of valid token, unknown to us but known to Blender ID."""
|
"""Test validating of valid token, unknown to us but known to Blender ID."""
|
||||||
|
|
||||||
from application.utils import authentication as auth
|
from application.utils import authentication as auth
|
||||||
|
|
||||||
self.htp_blenderid_validate_happy()
|
self.mock_blenderid_validate_happy()
|
||||||
with self.app.test_request_context(
|
with self.app.test_request_context(
|
||||||
headers={'Authorization': self.make_header('knowntoken')}):
|
headers={'Authorization': self.make_header('knowntoken')}):
|
||||||
self.assertTrue(auth.validate_token())
|
self.assertTrue(auth.validate_token())
|
||||||
|
@@ -4,15 +4,10 @@ import responses
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from bson import ObjectId
|
from bson import ObjectId
|
||||||
|
from flask import g
|
||||||
|
|
||||||
from common_test_class import AbstractPillarTest
|
from common_test_class import (AbstractPillarTest, TEST_EMAIL_ADDRESS, BLENDER_ID_TEST_USERID,
|
||||||
|
TEST_SUBCLIENT_TOKEN, BLENDER_ID_USER_RESPONSE, TEST_FULL_NAME)
|
||||||
TEST_FULL_NAME = u'врач Сергей'
|
|
||||||
TEST_EMAIL = 'jemoeder@example.com'
|
|
||||||
TEST_SUBCLIENT_TOKEN = 'my-subclient-token-for-pillar'
|
|
||||||
BLENDER_ID_TEST_USERID = 1896
|
|
||||||
BLENDER_ID_USER_RESPONSE = {'status': 'success',
|
|
||||||
'user': {'email': TEST_EMAIL, 'full_name': TEST_FULL_NAME}}
|
|
||||||
|
|
||||||
|
|
||||||
class BlenderIdSubclientTest(AbstractPillarTest):
|
class BlenderIdSubclientTest(AbstractPillarTest):
|
||||||
@@ -25,32 +20,72 @@ class BlenderIdSubclientTest(AbstractPillarTest):
|
|||||||
# Make sure the user exists in our database.
|
# Make sure the user exists in our database.
|
||||||
from application.utils.authentication import create_new_user
|
from application.utils.authentication import create_new_user
|
||||||
with self.app.test_request_context():
|
with self.app.test_request_context():
|
||||||
create_new_user(TEST_EMAIL, 'apekoppie', BLENDER_ID_TEST_USERID)
|
create_new_user(TEST_EMAIL_ADDRESS, 'apekoppie', BLENDER_ID_TEST_USERID)
|
||||||
|
|
||||||
self._common_user_test(200)
|
self._common_user_test(200)
|
||||||
|
|
||||||
def _common_user_test(self, expected_status_code):
|
@responses.activate
|
||||||
responses.add(responses.POST,
|
def test_store_multiple_tokens(self):
|
||||||
'%s/subclients/validate_token' % self.app.config['BLENDER_ID_ENDPOINT'],
|
scst1 = '%s-1' % TEST_SUBCLIENT_TOKEN
|
||||||
json=BLENDER_ID_USER_RESPONSE,
|
scst2 = '%s-2' % TEST_SUBCLIENT_TOKEN
|
||||||
status=200)
|
db_user1 = self._common_user_test(201, scst=scst1)
|
||||||
|
db_user2 = self._common_user_test(200, scst=scst2)
|
||||||
|
self.assertEqual(db_user1['_id'], db_user2['_id'])
|
||||||
|
|
||||||
|
# Now there should be two tokens.
|
||||||
|
with self.app.test_request_context():
|
||||||
|
tokens = self.app.data.driver.db['tokens']
|
||||||
|
self.assertIsNotNone(tokens.find_one({'user': db_user1['_id'], 'token': scst1}))
|
||||||
|
self.assertIsNotNone(tokens.find_one({'user': db_user1['_id'], 'token': scst2}))
|
||||||
|
|
||||||
|
# There should still be only one auth element for blender-id in the user doc.
|
||||||
|
self.assertEqual(1, len(db_user1['auth']))
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_authenticate_with_scst(self):
|
||||||
|
# Make sure there is a user and SCST.
|
||||||
|
db_user = self._common_user_test(201)
|
||||||
|
|
||||||
|
# Make a call that's authenticated with the SCST
|
||||||
|
from application.utils import authentication as auth
|
||||||
|
|
||||||
|
subclient_id = self.app.config['BLENDER_ID_SUBCLIENT_ID']
|
||||||
|
auth_header = self.make_header(TEST_SUBCLIENT_TOKEN, subclient_id)
|
||||||
|
|
||||||
|
with self.app.test_request_context(headers={'Authorization': auth_header}):
|
||||||
|
self.assertTrue(auth.validate_token())
|
||||||
|
self.assertIsNotNone(g.current_user)
|
||||||
|
self.assertEqual(db_user['_id'], g.current_user['user_id'])
|
||||||
|
|
||||||
|
def _common_user_test(self, expected_status_code, scst=TEST_SUBCLIENT_TOKEN):
|
||||||
|
self.mock_blenderid_validate_happy()
|
||||||
|
|
||||||
|
subclient_id = self.app.config['BLENDER_ID_SUBCLIENT_ID']
|
||||||
resp = self.client.post('/blender_id/store_scst',
|
resp = self.client.post('/blender_id/store_scst',
|
||||||
data={'user_id': BLENDER_ID_TEST_USERID,
|
data={'user_id': BLENDER_ID_TEST_USERID,
|
||||||
'scst': TEST_SUBCLIENT_TOKEN})
|
'subclient_id': subclient_id,
|
||||||
|
'token': scst})
|
||||||
self.assertEqual(expected_status_code, resp.status_code)
|
self.assertEqual(expected_status_code, resp.status_code)
|
||||||
|
|
||||||
user_info = json.loads(resp.data) # {'status': 'success', 'subclient_user_id': '...'}
|
user_info = json.loads(resp.data) # {'status': 'success', 'subclient_user_id': '...'}
|
||||||
self.assertEqual('success', user_info['status'])
|
self.assertEqual('success', user_info['status'])
|
||||||
|
|
||||||
# Check that the user was correctly updated
|
|
||||||
with self.app.test_request_context():
|
with self.app.test_request_context():
|
||||||
|
# Check that the user was correctly updated
|
||||||
users = self.app.data.driver.db['users']
|
users = self.app.data.driver.db['users']
|
||||||
db_user = users.find_one(ObjectId(user_info['subclient_user_id']))
|
db_user = users.find_one(ObjectId(user_info['subclient_user_id']))
|
||||||
self.assertIsNotNone(db_user, 'user %r not found' % user_info['subclient_user_id'])
|
self.assertIsNotNone(db_user, 'user %r not found' % user_info['subclient_user_id'])
|
||||||
|
|
||||||
self.assertEqual(TEST_EMAIL, db_user['email'])
|
self.assertEqual(TEST_EMAIL_ADDRESS, db_user['email'])
|
||||||
self.assertEqual(TEST_FULL_NAME, db_user['full_name'])
|
self.assertEqual(TEST_FULL_NAME, db_user['full_name'])
|
||||||
self.assertEqual(TEST_SUBCLIENT_TOKEN, db_user['auth'][0]['token'])
|
# self.assertEqual(TEST_SUBCLIENT_TOKEN, db_user['auth'][0]['token'])
|
||||||
self.assertEqual(str(BLENDER_ID_TEST_USERID), db_user['auth'][0]['user_id'])
|
self.assertEqual(str(BLENDER_ID_TEST_USERID), db_user['auth'][0]['user_id'])
|
||||||
self.assertEqual('blender-id', db_user['auth'][0]['provider'])
|
self.assertEqual('blender-id', db_user['auth'][0]['provider'])
|
||||||
|
|
||||||
|
# Check that the token was succesfully stored.
|
||||||
|
tokens = self.app.data.driver.db['tokens']
|
||||||
|
db_token = tokens.find_one({'user': db_user['_id'],
|
||||||
|
'token': scst})
|
||||||
|
self.assertIsNotNone(db_token)
|
||||||
|
|
||||||
|
return db_user
|
||||||
|
Reference in New Issue
Block a user