Initial work to support multiple OAuth clients

This commit is contained in:
2017-07-25 17:50:22 +02:00
parent d48a308cc6
commit c827dc4ed2
5 changed files with 232 additions and 111 deletions

View File

@@ -4,15 +4,16 @@ Also contains functionality for other parts of Pillar to perform communication
with Blender ID.
"""
import datetime
import logging
import datetime
import requests
from bson import tz_util
from flask import Blueprint, request, current_app, jsonify
from pillar.api.utils import authentication, remove_private_keys
from requests.adapters import HTTPAdapter
from werkzeug import exceptions as wz_exceptions
from pillar.api.utils import authentication
from pillar.api.utils.authentication import find_user_in_db, upsert_user
blender_id = Blueprint('blender_id', __name__)
log = logging.getLogger(__name__)
@@ -64,11 +65,10 @@ def validate_create_user(blender_id_user_id, token, oauth_subclient_id):
# Blender ID can be queried without user ID, and will always include the
# correct user ID in its response.
log.debug('Obtained user info from Blender ID: %s', user_info)
blender_id_user_id = user_info['id']
# Store the user info in MongoDB.
db_user = find_user_in_db(blender_id_user_id, user_info)
db_id, status = upsert_user(db_user, blender_id_user_id)
db_user = find_user_in_db(user_info)
db_id, status = upsert_user(db_user)
# Store the token in MongoDB.
authentication.store_token(db_id, token, token_expiry, oauth_subclient_id)
@@ -76,67 +76,6 @@ def validate_create_user(blender_id_user_id, token, oauth_subclient_id):
return db_user, status
def upsert_user(db_user, blender_id_user_id):
"""Inserts/updates the user in MongoDB.
Retries a few times when there are uniqueness issues in the username.
:returns: the user's database ID and the status of the PUT/POST.
The status is 201 on insert, and 200 on update.
:type: (ObjectId, int)
"""
if 'subscriber' in db_user.get('groups', []):
log.error('Non-ObjectID string found in user.groups: %s', db_user)
raise wz_exceptions.InternalServerError(
'Non-ObjectID string found in user.groups: %s' % db_user)
r = {}
for retry in range(5):
if '_id' in db_user:
# Update the existing user
attempted_eve_method = 'PUT'
db_id = db_user['_id']
r, _, _, status = current_app.put_internal('users', remove_private_keys(db_user),
_id=db_id)
if status == 422:
log.error('Status %i trying to PUT user %s with values %s, should not happen! %s',
status, db_id, remove_private_keys(db_user), r)
else:
# Create a new user, retry for non-unique usernames.
attempted_eve_method = 'POST'
r, _, _, status = current_app.post_internal('users', db_user)
if status not in {200, 201}:
log.error('Status %i trying to create user for BlenderID %s with values %s: %s',
status, blender_id_user_id, db_user, r)
raise wz_exceptions.InternalServerError()
db_id = r['_id']
db_user.update(r) # update with database/eve-generated fields.
if status == 422:
# Probably non-unique username, so retry a few times with different usernames.
log.info('Error creating new user: %s', r)
username_issue = r.get('_issues', {}).get('username', '')
if 'not unique' in username_issue:
# Retry
db_user['username'] = authentication.make_unique_username(db_user['email'])
continue
# Saving was successful, or at least didn't break on a non-unique username.
break
else:
log.error('Unable to create new user %s: %s', db_user, r)
raise wz_exceptions.InternalServerError()
if status not in (200, 201):
log.error('internal response from %s to Eve: %r %r', attempted_eve_method, status, r)
raise wz_exceptions.InternalServerError()
return db_id, status
def validate_token(user_id, token, oauth_subclient_id):
"""Verifies a subclient token with Blender ID.
@@ -211,36 +150,6 @@ def _compute_token_expiry(token_expires_string):
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': str(blender_id_user_id),
'provider': 'blender-id'}}}
log.debug('Querying: %s', query)
db_user = users.find_one(query)
if db_user:
log.debug('User blender_id_user_id=%r already in our database, '
'updating with info from Blender ID.', blender_id_user_id)
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(
email=user_info['email'],
user_id=blender_id_user_id,
username=user_info['full_name'])
db_user['username'] = authentication.make_unique_username(user_info['email'])
if not db_user['full_name']:
db_user['full_name'] = db_user['username']
return db_user
def fetch_blenderid_user() -> dict:
"""Returns the user info of the currently logged in user from BlenderID.

View File

@@ -88,8 +88,8 @@ users_schema = {
}
},
'auth': {
# Storage of authentication credentials (one will be able to auth with
# multiple providers on the same account)
# Storage of authentication credentials (one will be able to auth with multiple providers on
# the same account)
'type': 'list',
'required': True,
'schema': {
@@ -97,13 +97,12 @@ users_schema = {
'schema': {
'provider': {
'type': 'string',
'allowed': ["blender-id", "local"],
'allowed': ['blender-id', 'local', 'facebook'],
},
'user_id': {
'type': 'string'
},
# A token is considered a "password" in case the provider is
# "local".
# A token is considered a "password" in case the provider is "local".
'token': {
'type': 'string'
}

View File

@@ -11,9 +11,12 @@ import typing
import bson
from bson import tz_util
from flask import g
from flask import g, current_app
from flask import request
from flask import current_app
from werkzeug import exceptions as wz_exceptions
from pillar.api.utils import remove_private_keys
log = logging.getLogger(__name__)
@@ -49,6 +52,56 @@ def force_cli_user():
g.current_user = CLI_USER
def find_user_in_db(user_info: dict, provider='blender-id'):
"""Find the user in our database, creating/updating the returned document where needed.
First, search for the user using its id from the provider, then try to look the user up via the
email address.
Does NOT update the user in the database.
:param user_info: Information (id, email and full_name) from the auth provider
:param provider: One of the supported providers
"""
users = current_app.data.driver.db['users']
query = {'$or': [
{'auth': {'$elemMatch': {
'user_id': str(user_info['id']),
'provider': provider}}},
{'email': user_info['email']},
]}
log.debug('Querying: %s', query)
db_user = users.find_one(query)
if db_user:
log.debug('User with {provider} id {user_id} already in our database, '
'updating with info from {provider}.'.format(
provider=provider, user_id=user_info['id']))
db_user['email'] = user_info['email']
# Find out if an auth entry for the current provider already exists
provider_entry = [element for element in db_user['auth'] if element['provider'] == provider]
if not provider_entry:
db_user['auth'].append({
'provider': provider,
'user_id': str(user_info['id']),
'token': ''})
else:
log.debug('User %r not yet in our database, create a new one.', user_info['id'])
db_user = create_new_user_document(
email=user_info['email'],
user_id=user_info['id'],
username=user_info['full_name'],
provider=provider)
db_user['username'] = make_unique_username(user_info['email'])
if not db_user['full_name']:
db_user['full_name'] = db_user['username']
return db_user
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
@@ -270,3 +323,64 @@ def setup_app(app):
def validate_token_at_each_request():
validate_token()
return None
def upsert_user(db_user):
"""Inserts/updates the user in MongoDB.
Retries a few times when there are uniqueness issues in the username.
:returns: the user's database ID and the status of the PUT/POST.
The status is 201 on insert, and 200 on update.
:type: (ObjectId, int)
"""
if 'subscriber' in db_user.get('groups', []):
log.error('Non-ObjectID string found in user.groups: %s', db_user)
raise wz_exceptions.InternalServerError(
'Non-ObjectID string found in user.groups: %s' % db_user)
r = {}
for retry in range(5):
if '_id' in db_user:
# Update the existing user
attempted_eve_method = 'PUT'
db_id = db_user['_id']
r, _, _, status = current_app.put_internal('users', remove_private_keys(db_user),
_id=db_id)
if status == 422:
log.error('Status %i trying to PUT user %s with values %s, should not happen! %s',
status, db_id, remove_private_keys(db_user), r)
else:
# Create a new user, retry for non-unique usernames.
attempted_eve_method = 'POST'
r, _, _, status = current_app.post_internal('users', db_user)
if status not in {200, 201}:
log.error('Status %i trying to create user with values %s: %s',
status, db_user, r)
raise wz_exceptions.InternalServerError()
db_id = r['_id']
db_user.update(r) # update with database/eve-generated fields.
if status == 422:
# Probably non-unique username, so retry a few times with different usernames.
log.info('Error creating new user: %s', r)
username_issue = r.get('_issues', {}).get('username', '')
if 'not unique' in username_issue:
# Retry
db_user['username'] = make_unique_username(db_user['email'])
continue
# Saving was successful, or at least didn't break on a non-unique username.
break
else:
log.error('Unable to create new user %s: %s', db_user, r)
raise wz_exceptions.InternalServerError()
if status not in (200, 201):
log.error('internal response from %s to Eve: %r %r', attempted_eve_method, status, r)
raise wz_exceptions.InternalServerError()
return db_id, status