Allow user creation from Blender ID webhook "user modified"

When the webhook indicates that the user has a Cloud subscription (demo,
active, or renewable), the user is immediately created.
This commit is contained in:
2017-12-20 12:56:48 +01:00
parent a2c24375e5
commit fd68f3fc8b
2 changed files with 246 additions and 12 deletions

View File

@@ -13,6 +13,7 @@ import werkzeug.exceptions as wz_exceptions
from pillar import current_app
from pillar.api.blender_cloud import subscription
from pillar.api.utils.authentication import create_new_user_document, make_unique_username
from pillar.auth import UserClass
blueprint = Blueprint('cloud-webhooks', __name__)
@@ -76,24 +77,30 @@ def score(wh_payload: dict, user: dict) -> int:
return match_on_bid * 10 + match_on_old_email + match_on_new_email * 2
def fetch_user(wh_payload: dict) -> typing.Optional[dict]:
"""Fetch the user from the DB
def insert_or_fetch_user(wh_payload: dict) -> typing.Optional[dict]:
"""Fetch the user from the DB or create it.
:returns the user document, or None when not found.
Only creates it if the webhook payload indicates they could actually use
Blender Cloud (i.e. demo or subscriber). This prevents us from creating
Cloud accounts for Blender Network users.
:returns the user document, or None when not created.
"""
users_coll = current_app.db('users')
my_log = log.getChild('insert_or_fetch_user')
bid_str = str(wh_payload['id'])
email = wh_payload['email']
# Find the user by their Blender ID, or any of their email addresses.
# We use one query to find all matching users. This is done as a
# consistency check; if more than one user is returned, we know the
# database is inconsistent with Blender ID and can emit a warning
# about this.
bid_str = str(wh_payload['id'])
query = {'$or': [
{'auth.provider': 'blender-id', 'auth.user_id': bid_str},
{'email': {'$in': [wh_payload['old_email'], wh_payload['email']]}},
{'email': {'$in': [wh_payload['old_email'], email]}},
]}
db_users = users_coll.find(query)
user_count = db_users.count()
@@ -103,21 +110,50 @@ def fetch_user(wh_payload: dict) -> typing.Optional[dict]:
calc_score = functools.partial(score, wh_payload)
best_score = max(db_users, key=calc_score)
my_log.warning('%d users found for query %s, picking %s',
user_count, query, best_score['email'])
my_log.error('%d users found for query %s, picking user %s (%s)',
user_count, query, best_score['_id'], best_score['email'])
return best_score
if user_count:
db_user = db_users[0]
my_log.debug('found user %s', db_user['email'])
return db_user
my_log.info('Received update for unknown user %r', wh_payload['old_email'])
return None
# Pretend to create the user, so that we can inspect the resulting
# capabilities. This is more future-proof than looking at the list
# of roles in the webhook payload.
username = make_unique_username(email)
user_doc = create_new_user_document(email, bid_str, username,
provider='blender-id',
full_name=wh_payload['full_name'])
user_doc['roles'] = [subscription.ROLES_BID_TO_PILLAR[r]
for r in wh_payload.get('roles', [])
if r in subscription.ROLES_BID_TO_PILLAR]
user_ob = UserClass.construct('', user_doc)
create = user_ob.has_cap('subscriber') or user_ob.has_cap('can-renew-subscription')
if not create:
my_log.info('Received update for unknown user %r without Cloud access (caps=%s)',
wh_payload['old_email'], user_ob.capabilities)
return None
# Actually create the user in the database.
r, _, _, status = current_app.post_internal('users', user_doc)
if status != 201:
my_log.error('unable to create user %s: : %r %r', email, status, r)
raise wz_exceptions.InternalServerError('unable to create user')
user_doc.update(r)
my_log.info('created user %r = %s to allow immediate Cloud access', email, user_doc['_id'])
return user_doc
@blueprint.route('/user-modified', methods=['POST'])
def user_modified():
"""Updates the local user based on the info from Blender ID.
"""Update the local user based on the info from Blender ID.
If the payload indicates the user has access to Blender Cloud (or at least
a renewable subscription), create the user if not already in our DB.
The payload we expect is a dictionary like:
{'id': 12345, # the user's ID in Blender ID
@@ -135,7 +171,7 @@ def user_modified():
my_log.info('payload: %s', payload)
# Update the user
db_user = fetch_user(payload)
db_user = insert_or_fetch_user(payload)
if not db_user:
my_log.info('Received update for unknown user %r', payload['old_email'])
return '', 204