From 07691db874267923eda3feb0895f39a9c543e876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Tue, 23 Aug 2016 16:09:47 +0200 Subject: [PATCH] Check subscription status on login. --- pillar/__init__.py | 2 +- pillar/auth/subscriptions.py | 51 ++++++++++++++++++++++++++++ pillar/config.py | 5 +++ pillar/web/users/routes.py | 64 +++++++++++++++++++++++++++++++----- 4 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 pillar/auth/subscriptions.py diff --git a/pillar/__init__.py b/pillar/__init__.py index ebced276..c13b916d 100644 --- a/pillar/__init__.py +++ b/pillar/__init__.py @@ -64,7 +64,7 @@ class PillarServer(Eve): self.load_config() # Configure authentication - self._login_manager = auth.config_login_manager(self) + self.login_manager = auth.config_login_manager(self) self.oauth_blender_id = auth.config_oauth_login(self) self._config_caching() diff --git a/pillar/auth/subscriptions.py b/pillar/auth/subscriptions.py new file mode 100644 index 00000000..70c748ec --- /dev/null +++ b/pillar/auth/subscriptions.py @@ -0,0 +1,51 @@ +"""Cloud subscription info. + +Connects to the external subscription server to obtain user info. +""" + +import logging + +from flask import current_app +import requests +from requests.adapters import HTTPAdapter + +log = logging.getLogger(__name__) + + +def fetch_user(email): + """Returns the user info dict from the external subscriptions management server. + + :returns: the store user info, or None if the user can't be found or there + was an error communicating. A dict like this is returned: + { + "shop_id": 700, + "cloud_access": 1, + "paid_balance": 314.75, + "balance_currency": "EUR", + "start_date": "2014-08-25 17:05:46", + "expiration_date": "2016-08-24 13:38:45", + "subscription_status": "wc-active", + "expiration_date_approximate": true + } + :rtype: dict + """ + + external_subscriptions_server = current_app.config['EXTERNAL_SUBSCRIPTIONS_MANAGEMENT_SERVER'] + + log.debug('Connecting to store at %s?blenderid=%s', external_subscriptions_server, email) + + # Retry a few times when contacting the store. + s = requests.Session() + s.mount(external_subscriptions_server, HTTPAdapter(max_retries=5)) + r = s.get(external_subscriptions_server, params={'blenderid': email}, + verify=current_app.config['TLS_CERT_FILE']) + + if r.status_code != 200: + log.warning("Error communicating with %s, code=%i, unable to check " + "subscription status of user %s", + external_subscriptions_server, r.status_code, email) + return None + + store_user = r.json() + return store_user + diff --git a/pillar/config.py b/pillar/config.py index 973036e3..8344a3af 100644 --- a/pillar/config.py +++ b/pillar/config.py @@ -2,6 +2,8 @@ import os.path from os import getenv from collections import defaultdict +import requests.certs + RFC1123_DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' PILLAR_SERVER_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -137,3 +139,6 @@ URLER_SERVICE_AUTH_TOKEN = None BLENDER_CLOUD_ADDON_VERSION = '1.4' EXTERNAL_SUBSCRIPTIONS_MANAGEMENT_SERVER = 'https://store.blender.org/api/' + +# Certificate file for communication with other systems. +TLS_CERT_FILE = requests.certs.where() diff --git a/pillar/web/users/routes.py b/pillar/web/users/routes.py index 62a30d72..ded7a0c7 100644 --- a/pillar/web/users/routes.py +++ b/pillar/web/users/routes.py @@ -6,7 +6,7 @@ from flask import (abort, Blueprint, current_app, flash, redirect, render_template, request, session, url_for) from flask_login import login_required, login_user, logout_user, current_user from flask_oauthlib.client import OAuthException -from pillar.auth import UserClass +from pillar.auth import UserClass, subscriptions from pillar.web import system_util from .forms import UserProfileForm from .forms import UserSettingsEmailsForm @@ -55,13 +55,12 @@ def blender_id_authorized(): user = UserClass(oauth_resp['access_token']) login_user(user) - # user = load_user(current_user.id) + current_app.login_manager.reload_user() # This ensures that flask_login.current_user is set. - if user is not None: - pass + if current_user is not None: # Check with the store for user roles. If the user has an active # subscription, we apply the 'subscriber' role - # user_roles_update(user.objectid) + user_roles_update(current_user.objectid) next_after_login = session.get('next_after_login') if next_after_login: @@ -179,9 +178,9 @@ def settings_billing(): for group_id in user.groups: group = Group.find(group_id, api=api) groups.append(group.name) - external_subscriptions_server = current_app.config['EXTERNAL_SUBSCRIPTIONS_MANAGEMENT_SERVER'] - r = requests.get(external_subscriptions_server, params={'blenderid': user.email}) - store_user = r.json() + + store_user = subscriptions.fetch_user(user.email) + return render_template( 'users/settings/billing.html', store_user=store_user, groups=groups, title='billing') @@ -237,3 +236,52 @@ def users_index(): if not current_user.has_role('admin'): return abort(403) return render_template('users/index.html') + + +def user_roles_update(user_id): + api = system_util.pillar_api() + group_subscriber = Group.find_one({'where': {'name': 'subscriber'}}, api=api) + + # Fetch the user once outside the loop, because we only need to get the + # subscription status once. + user = User.me(api=api) + + store_user = subscriptions.fetch_user(user.email) + if store_user is None: + return + + max_retry = 5 + for retry_count in range(max_retry): + # Update the user's role & groups for their subscription status. + roles = set(user.roles or []) + groups = set(user.groups or []) + + if store_user['cloud_access'] == 1: + roles.add(u'subscriber') + groups.add(group_subscriber._id) + + elif u'admin' not in roles: + roles.discard(u'subscriber') + groups.discard(group_subscriber._id) + + # Only send an API request when the user has actually changed + if set(user.roles or []) == roles and set(user.groups or []) == groups: + break + + user.roles = list(roles) + user.groups = list(groups) + + try: + user.update(api=api) + except sdk_exceptions.PreconditionFailed: + log.warning('User etag changed while updating roles, retrying.') + else: + # Successful update, so we can stop the loop. + break + + # Fetch the user for the next iteration. + if retry_count < max_retry - 1: + user = User.me(api=api) + else: + log.warning('Tried %i times to update user %s, and failed each time. Giving up.', + max_retry, user_id)