TODO: also store expiry timestamp TODO: allow multiple subclient-specific tokens per user
213 lines
6.9 KiB
Python
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)
|