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:
parent
0f6eeef32b
commit
66eeb25529
@ -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
|
||||
|
@ -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 requests
|
||||
|
||||
from bson import tz_util
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from flask import g
|
||||
from flask import request
|
||||
from eve.methods.post import post_internal
|
||||
@ -13,41 +18,6 @@ from application import app
|
||||
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():
|
||||
"""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
|
||||
@ -70,79 +40,68 @@ def validate_token():
|
||||
|
||||
# Check the users to see if there is one with this Blender ID token.
|
||||
token = request.authorization.username
|
||||
db_user = find_user_by_token(token)
|
||||
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
|
||||
oauth_subclient = request.authorization.password
|
||||
|
||||
# Fall back to deprecated behaviour.
|
||||
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)
|
||||
db_token = find_token(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
|
||||
# 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.
|
||||
validation = validate(token)
|
||||
if validation is None or validation.get('status', '') != 'success':
|
||||
log.debug('Validation failed, result is %r', validation)
|
||||
return False
|
||||
from application.modules import blender_id
|
||||
|
||||
users = app.data.driver.db['users']
|
||||
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'])
|
||||
db_user, status = blender_id.validate_create_user('', token, oauth_subclient)
|
||||
else:
|
||||
log.debug("User is already in our database and token hasn't expired yet.")
|
||||
users = app.data.driver.db['users']
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
"""Creates a new user in our local database.
|
||||
|
||||
@ -158,7 +117,7 @@ def create_new_user(email, username, 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."""
|
||||
|
||||
user_data = {
|
||||
@ -168,10 +127,11 @@ def create_new_user_document(email, user_id, username, token=''):
|
||||
'auth': [{
|
||||
'provider': 'blender-id',
|
||||
'user_id': str(user_id),
|
||||
'token': token}],
|
||||
'token': ''}], # TODO: remove 'token' field altogether.
|
||||
'settings': {
|
||||
'email_communications': 1
|
||||
}
|
||||
},
|
||||
'groups': [],
|
||||
}
|
||||
return user_data
|
||||
|
||||
@ -202,11 +162,3 @@ def make_unique_username(email):
|
||||
if user_from_username is None:
|
||||
return unique_name
|
||||
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
|
||||
|
||||
# development requirements
|
||||
httpretty==0.8.14
|
||||
pytest==2.9.1
|
||||
responses==0.5.1
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import copy
|
||||
import sys
|
||||
@ -9,7 +11,7 @@ from bson import ObjectId
|
||||
from eve.tests import TestMinimal
|
||||
import pymongo.collection
|
||||
from flask.testing import FlaskClient
|
||||
import httpretty
|
||||
import responses
|
||||
|
||||
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_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(
|
||||
level=logging.DEBUG,
|
||||
@ -83,26 +93,23 @@ class AbstractPillarTest(TestMinimal):
|
||||
|
||||
return found['_id'], found
|
||||
|
||||
def htp_blenderid_validate_unhappy(self):
|
||||
"""Sets up HTTPretty to mock unhappy validation flow."""
|
||||
def mock_blenderid_validate_unhappy(self):
|
||||
"""Sets up Responses to mock unhappy validation flow."""
|
||||
|
||||
httpretty.register_uri(httpretty.POST,
|
||||
'%s/u/validate_token' % self.app.config['BLENDER_ID_ENDPOINT'],
|
||||
body=json.dumps(
|
||||
{'data': {'token': 'Token is invalid'}, 'status': 'fail'}),
|
||||
content_type="application/json")
|
||||
responses.add(responses.POST,
|
||||
'%s/subclients/validate_token' % self.app.config['BLENDER_ID_ENDPOINT'],
|
||||
json={'status': 'fail'},
|
||||
status=404)
|
||||
|
||||
def htp_blenderid_validate_happy(self):
|
||||
"""Sets up HTTPretty to mock happy validation flow."""
|
||||
def mock_blenderid_validate_happy(self):
|
||||
"""Sets up Responses to mock happy validation flow."""
|
||||
|
||||
httpretty.register_uri(httpretty.POST,
|
||||
'%s/u/validate_token' % self.app.config['BLENDER_ID_ENDPOINT'],
|
||||
body=json.dumps(
|
||||
{'data': {'user': {'email': TEST_EMAIL_ADDRESS, 'id': 5123}},
|
||||
'status': 'success'}),
|
||||
content_type="application/json")
|
||||
responses.add(responses.POST,
|
||||
'%s/subclients/validate_token' % self.app.config['BLENDER_ID_ENDPOINT'],
|
||||
json=BLENDER_ID_USER_RESPONSE,
|
||||
status=200)
|
||||
|
||||
def make_header(self, username, password=''):
|
||||
def make_header(self, username, subclient_id=''):
|
||||
"""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
|
||||
|
||||
@ -15,31 +15,31 @@ class AuthenticationTests(AbstractPillarTest):
|
||||
auth.create_new_user(TEST_EMAIL_ADDRESS, TEST_EMAIL_USER, 'test1234')
|
||||
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):
|
||||
from application.utils import authentication as auth
|
||||
|
||||
with self.app.test_request_context():
|
||||
self.assertFalse(auth.validate_token())
|
||||
|
||||
@httpretty.activate
|
||||
@responses.activate
|
||||
def test_validate_token__unknown_token(self):
|
||||
"""Test validating of invalid token, unknown both to us and Blender ID."""
|
||||
|
||||
from application.utils import authentication as auth
|
||||
|
||||
self.htp_blenderid_validate_unhappy()
|
||||
self.mock_blenderid_validate_unhappy()
|
||||
with self.app.test_request_context(
|
||||
headers={'Authorization': self.make_header('unknowntoken')}):
|
||||
self.assertFalse(auth.validate_token())
|
||||
|
||||
@httpretty.activate
|
||||
@responses.activate
|
||||
def test_validate_token__unknown_but_valid_token(self):
|
||||
"""Test validating of valid token, unknown to us but known to Blender ID."""
|
||||
|
||||
from application.utils import authentication as auth
|
||||
|
||||
self.htp_blenderid_validate_happy()
|
||||
self.mock_blenderid_validate_happy()
|
||||
with self.app.test_request_context(
|
||||
headers={'Authorization': self.make_header('knowntoken')}):
|
||||
self.assertTrue(auth.validate_token())
|
||||
|
@ -4,15 +4,10 @@ import responses
|
||||
import json
|
||||
|
||||
from bson import ObjectId
|
||||
from flask import g
|
||||
|
||||
from common_test_class import AbstractPillarTest
|
||||
|
||||
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}}
|
||||
from common_test_class import (AbstractPillarTest, TEST_EMAIL_ADDRESS, BLENDER_ID_TEST_USERID,
|
||||
TEST_SUBCLIENT_TOKEN, BLENDER_ID_USER_RESPONSE, TEST_FULL_NAME)
|
||||
|
||||
|
||||
class BlenderIdSubclientTest(AbstractPillarTest):
|
||||
@ -25,32 +20,72 @@ class BlenderIdSubclientTest(AbstractPillarTest):
|
||||
# Make sure the user exists in our database.
|
||||
from application.utils.authentication import create_new_user
|
||||
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)
|
||||
|
||||
def _common_user_test(self, expected_status_code):
|
||||
responses.add(responses.POST,
|
||||
'%s/subclients/validate_token' % self.app.config['BLENDER_ID_ENDPOINT'],
|
||||
json=BLENDER_ID_USER_RESPONSE,
|
||||
status=200)
|
||||
@responses.activate
|
||||
def test_store_multiple_tokens(self):
|
||||
scst1 = '%s-1' % TEST_SUBCLIENT_TOKEN
|
||||
scst2 = '%s-2' % TEST_SUBCLIENT_TOKEN
|
||||
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',
|
||||
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)
|
||||
|
||||
user_info = json.loads(resp.data) # {'status': 'success', 'subclient_user_id': '...'}
|
||||
self.assertEqual('success', user_info['status'])
|
||||
|
||||
# Check that the user was correctly updated
|
||||
with self.app.test_request_context():
|
||||
# Check that the user was correctly updated
|
||||
users = self.app.data.driver.db['users']
|
||||
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.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_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('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
|
||||
|
Loading…
x
Reference in New Issue
Block a user