Files
pillar/pillar/application/utils/authentication.py
Sybren A. Stüvel e898fe0315 Use Blender ID subclient-specific token to find the user.
TODO: also store expiry timestamp
TODO: allow multiple subclient-specific tokens per user
2016-04-12 16:53:27 +02:00

213 lines
6.9 KiB
Python

import logging
import requests
from datetime import datetime
from datetime import timedelta
from flask import g
from flask import request
from eve.methods.post import post_internal
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
from it.
When the token is successfully validated, sets `g.current_user` to contain
the user information, otherwise it is set to None.
@returns True iff the user is logged in with a valid Blender ID token.
"""
# Default to no user at all.
g.current_user = None
if not request.authorization:
# If no authorization headers are provided, we are getting a request
# from a non logged in user. Proceed accordingly.
log.debug('No authentication headers, so not logged in.')
return False
# 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
# 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)
if not db_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
# 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
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'])
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
return True
def create_new_user(email, username, user_id):
"""Creates a new user in our local database.
@param email: the user's email
@param username: the username, which is also used as full name.
@param user_id: the user ID from the Blender ID server.
@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,
'email': email,
'auth': [{
'provider': 'blender-id',
'user_id': str(user_id),
'token': token}],
'settings': {
'email_communications': 1
}
}
return user_data
def make_unique_username(email):
"""Creates a unique username from the email address.
@param email: the email address
@returns: the new username
@rtype: str
"""
username = email.split('@')[0]
# Check for min length of username (otherwise validation fails)
username = "___{0}".format(username) if len(username) < 3 else username
users = app.data.driver.db['users']
user_from_username = users.find_one({'username': username})
if not user_from_username:
return username
# Username exists, make it unique by adding some number after it.
suffix = 1
while True:
unique_name = '%s%i' % (username, suffix)
user_from_username = users.find_one({'username': unique_name})
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)