diff --git a/pillar/api/blender_cloud/subscription.py b/pillar/api/blender_cloud/subscription.py index 63edbe47..9e5aacb9 100644 --- a/pillar/api/blender_cloud/subscription.py +++ b/pillar/api/blender_cloud/subscription.py @@ -83,13 +83,19 @@ def update_subscription(): """ import pprint + from pillar import auth from pillar.api import blender_id, service from pillar.api.utils import authentication my_log: logging.Logger = log.getChild('update_subscription') user_id = authentication.current_user_id() - bid_user = blender_id.fetch_blenderid_user() + try: + bid_user = blender_id.fetch_blenderid_user() + except blender_id.LogoutUser: + auth.logout_user() + return '', 204 + if not bid_user: my_log.warning('Logged in user %s has no BlenderID account! ' 'Unable to update subscription status.', user_id) diff --git a/pillar/api/blender_id.py b/pillar/api/blender_id.py index fa4e0dce..06c8cae2 100644 --- a/pillar/api/blender_id.py +++ b/pillar/api/blender_id.py @@ -20,6 +20,13 @@ blender_id = Blueprint('blender_id', __name__) log = logging.getLogger(__name__) +class LogoutUser(Exception): + """Raised when Blender ID tells us the current user token is invalid. + + This indicates the user should be immediately logged out. + """ + + @blender_id.route('/store_scst', methods=['POST']) def store_subclient_token(): """Verifies & stores a user's subclient-specific token.""" @@ -180,6 +187,8 @@ def fetch_blenderid_user() -> dict: } } + :raises LogoutUser: when Blender ID tells us the current token is + invalid, and the user should be logged out. """ import httplib2 # used by the oauth2 package @@ -201,6 +210,10 @@ def fetch_blenderid_user() -> dict: log.exception('Error getting %s from BlenderID', bid_url) return {} + if bid_resp.status_code == 403: + log.warning('Error %i from BlenderID %s, logging out user', bid_resp.status_code, bid_url) + raise LogoutUser() + if bid_resp.status_code != 200: log.warning('Error %i from BlenderID %s: %s', bid_resp.status_code, bid_url, bid_resp.text) return {} diff --git a/pillar/api/utils/authentication.py b/pillar/api/utils/authentication.py index b84bf468..65a50b79 100644 --- a/pillar/api/utils/authentication.py +++ b/pillar/api/utils/authentication.py @@ -177,11 +177,23 @@ def validate_this_token(token, oauth_subclient=None): return db_user +def remove_token(token: str): + """Removes the token from the database.""" + + + tokens_coll = current_app.db('tokens') + token_hashed = hash_auth_token(token) + + # TODO: remove matching on unhashed tokens once all tokens have been hashed. + lookup = {'$or': [{'token': token}, {'token_hashed': token_hashed}]} + del_res = tokens_coll.delete_many(lookup) + log.debug('Removed token %r, matched %d documents', token, del_res.deleted_count) + + def find_token(token, is_subclient_token=False, **extra_filters): """Returns the token document, or None if it doesn't exist (or is expired).""" - tokens_collection = current_app.data.driver.db['tokens'] - + tokens_coll = current_app.db('tokens') token_hashed = hash_auth_token(token) # TODO: remove matching on unhashed tokens once all tokens have been hashed. @@ -190,7 +202,7 @@ def find_token(token, is_subclient_token=False, **extra_filters): 'expire_time': {"$gt": datetime.datetime.now(tz=tz_util.utc)}} lookup.update(extra_filters) - db_token = tokens_collection.find_one(lookup) + db_token = tokens_coll.find_one(lookup) return db_token diff --git a/pillar/auth/__init__.py b/pillar/auth/__init__.py index 5ae6197a..9f9e3168 100644 --- a/pillar/auth/__init__.py +++ b/pillar/auth/__init__.py @@ -202,8 +202,6 @@ def config_login_manager(app): def login_user(oauth_token: str, *, load_from_db=False): """Log in the user identified by the given token.""" - from flask import g - if load_from_db: user = _load_user(oauth_token) else: @@ -212,6 +210,20 @@ def login_user(oauth_token: str, *, load_from_db=False): g.current_user = user +def logout_user(): + """Forces a logout of the current user.""" + + from ..api.utils import authentication + + token = get_blender_id_oauth_token() + if token: + authentication.remove_token(token) + + session.clear() + flask_login.logout_user() + g.current_user = AnonymousUser() + + def get_blender_id_oauth_token() -> str: """Returns the Blender ID auth token, or an empty string if there is none.""" @@ -231,6 +243,9 @@ def get_blender_id_oauth_token() -> str: if request.authorization and request.authorization.username: return request.authorization.username + if current_user.is_authenticated and current_user.id: + return current_user.id + return '' diff --git a/pillar/web/users/routes.py b/pillar/web/users/routes.py index 3b506b8c..85aa0462 100644 --- a/pillar/web/users/routes.py +++ b/pillar/web/users/routes.py @@ -2,7 +2,7 @@ import logging from flask import abort, Blueprint, redirect, render_template, request, session, \ url_for -from flask_login import login_required, logout_user +from flask_login import login_required from werkzeug import exceptions as wz_exceptions import pillar.api.blender_cloud.subscription @@ -108,8 +108,7 @@ def login_local(): @blueprint.route('/logout') def logout(): - logout_user() - session.clear() + pillar.auth.logout_user() return redirect('/')