From 87fe1887e890198bb3a0ac672ce59eaf48fed5d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Tue, 5 Dec 2017 11:45:42 +0100 Subject: [PATCH] Added "Update from Blender ID" button Added this button in the /u/ user/embed view, so that admins can easily force a re-check from Blender ID without requiring the user themselves to perform any actions. --- pillar/api/blender_cloud/subscription.py | 71 +++++++++++++++++++++--- pillar/api/blender_id.py | 18 ++++++ pillar/config.py | 5 +- src/templates/users/edit_embed_base.pug | 13 +++++ 4 files changed, 97 insertions(+), 10 deletions(-) diff --git a/pillar/api/blender_cloud/subscription.py b/pillar/api/blender_cloud/subscription.py index d523aa75..e6fe6fe9 100644 --- a/pillar/api/blender_cloud/subscription.py +++ b/pillar/api/blender_cloud/subscription.py @@ -1,11 +1,14 @@ -import collections import logging import typing -from flask import Blueprint +from flask import Blueprint, Response +import requests +from requests.adapters import HTTPAdapter -from pillar.auth import UserClass -from pillar.api.utils import authorization +from pillar import auth, current_app +from pillar.api import blender_id +from pillar.api.utils import authorization, jsonify +from pillar.auth import current_user log = logging.getLogger(__name__) blueprint = Blueprint('blender_cloud.subscription', __name__) @@ -27,9 +30,6 @@ def update_subscription() -> typing.Tuple[str, int]: Returns an empty HTTP response. """ - from pillar import auth - from pillar.api import blender_id - my_log: logging.Logger = log.getChild('update_subscription') current_user = auth.get_current_user() @@ -48,7 +48,58 @@ def update_subscription() -> typing.Tuple[str, int]: return '', 204 -def do_update_subscription(local_user: UserClass, bid_user: dict): +@blueprint.route('/update-subscription-for/', methods=['POST']) +@authorization.require_login(require_cap='admin') +def update_subscription_for(user_id: str): + """Updates the user based on their info at Blender ID.""" + + from urllib.parse import urljoin + + from pillar.api.utils import str2id + + my_log = log.getChild('update_subscription_for') + + bid_session = requests.Session() + bid_session.mount('https://', HTTPAdapter(max_retries=5)) + bid_session.mount('http://', HTTPAdapter(max_retries=5)) + + users_coll = current_app.db('users') + db_user = users_coll.find_one({'_id': str2id(user_id)}) + if not db_user: + my_log.warning('User %s not found in database', user_id) + return Response(f'User {user_id} not found in our database', status=404) + + log.info('Updating user %s from Blender ID on behalf of %s', + db_user['email'], current_user.email) + + bid_user_id = blender_id.get_user_blenderid(db_user) + if not bid_user_id: + my_log.info('User %s has no Blender ID', user_id) + return Response('User has no Blender ID', status=404) + + # Get the user info from Blender ID, and handle errors. + api_url = current_app.config['BLENDER_ID_USER_INFO_API'] + api_token = current_app.config['BLENDER_ID_USER_INFO_TOKEN'] + url = urljoin(api_url, bid_user_id) + resp = bid_session.get(url, headers={'Authorization': f'Bearer {api_token}'}) + if resp.status_code == 404: + my_log.info('User %s has a Blender ID %s but Blender ID itself does not find it', + user_id, bid_user_id) + return Response(f'User {bid_user_id} does not exist at Blender ID', status=404) + if resp.status_code != 200: + my_log.info('Error code %s getting user %s from Blender ID (resp = %s)', + resp.status_code, user_id, resp.text) + return Response(f'Error code {resp.status_code} from Blender ID', status=resp.status_code) + + # Update the user in our database. + local_user = auth.UserClass.construct('', db_user) + bid_user = resp.json() + do_update_subscription(local_user, bid_user) + + return '', 204 + + +def do_update_subscription(local_user: auth.UserClass, bid_user: dict): """Updates the subscription status of the user given the Blender ID user info. Uses the badger service to update the user's roles from Blender ID. @@ -106,6 +157,10 @@ def do_update_subscription(local_user: UserClass, bid_user: dict): user_id, email, ', '.join(sorted(revoke_roles))) service.do_badger('revoke', roles=revoke_roles, user_id=user_id) + # Re-index the user in the search database. + from pillar.api.users import hooks + hooks.push_updated_user_to_algolia({'_id': user_id}, {}) + def setup_app(app, url_prefix): log.info('Registering blueprint at %s', url_prefix) diff --git a/pillar/api/blender_id.py b/pillar/api/blender_id.py index 8d025243..eb9e7537 100644 --- a/pillar/api/blender_id.py +++ b/pillar/api/blender_id.py @@ -168,6 +168,24 @@ def _compute_token_expiry(token_expires_string): return min(blid_expiry, our_expiry) +def get_user_blenderid(db_user: dict) -> str: + """Returns the Blender ID user ID for this Pillar user. + + Takes the string from 'auth.*.user_id' for the '*' where 'provider' + is 'blender-id'. + + :returns the user ID, or the empty string when the user has none. + """ + + bid_user_ids = [auth['user_id'] + for auth in db_user['auth'] + if auth['provider'] == 'blender-id'] + try: + return bid_user_ids[0] + except IndexError: + return '' + + def fetch_blenderid_user() -> dict: """Returns the user info of the currently logged in user from BlenderID. diff --git a/pillar/config.py b/pillar/config.py index e5938499..eb168fa7 100644 --- a/pillar/config.py +++ b/pillar/config.py @@ -99,8 +99,9 @@ FULL_FILE_ACCESS_ROLES = {'admin', 'subscriber', 'demo'} BLENDER_ID_CLIENT_ID = 'SPECIAL-SNOWFLAKE-57' BLENDER_ID_SUBCLIENT_ID = 'PILLAR' -# Blender ID user info API endpoint URL and auth token, only used for -# reconciling subscribers. The token requires the 'userinfo' scope. +# Blender ID user info API endpoint URL and auth token, used for +# reconciling subscribers and updating their info from /u/. +# The token requires the 'userinfo' scope. BLENDER_ID_USER_INFO_API = 'http://blender-id:8000/api/user/' BLENDER_ID_USER_INFO_TOKEN = '-set-in-config-local-' diff --git a/src/templates/users/edit_embed_base.pug b/src/templates/users/edit_embed_base.pug index 2f616666..7ce36b08 100644 --- a/src/templates/users/edit_embed_base.pug +++ b/src/templates/users/edit_embed_base.pug @@ -72,6 +72,7 @@ | none | {% endif %} + a.btn.btn-default(href="javascript:update_from_bid()") Update from Blender ID a.btn.btn-default(href="javascript:$('#user-edit-container').html('')") Cancel input#submit_edit_user.btn.btn-default( @@ -103,4 +104,16 @@ script(type="text/javascript"). new Clipboard('.copy-to-clipboard'); + function update_from_bid() { + var url = '{{ url_for("blender_cloud.subscription.update_subscription_for", user_id=user.user_id) }}'; + $.post(url) + .done(function(data) { + toastr.info('User updated from Blender ID'); + displayUser('{{ user.user_id }}'); + }) + .fail(function(data) { + toastr.error(data.responseText); + }); + } + | {% endblock %}