Reworked subscription/demo role management from web to API level.
In the old situation, users had to be able to change their own roles. This is inherently insecure.
This commit is contained in:
parent
d0557445cd
commit
1a54b723aa
@ -24,7 +24,8 @@ def blender_cloud_addon_version():
|
||||
|
||||
|
||||
def setup_app(app, url_prefix):
|
||||
from . import texture_libs, home_project
|
||||
from . import texture_libs, home_project, subscription
|
||||
|
||||
texture_libs.setup_app(app, url_prefix=url_prefix)
|
||||
home_project.setup_app(app, url_prefix=url_prefix)
|
||||
subscription.setup_app(app, url_prefix=url_prefix)
|
||||
|
118
pillar/api/blender_cloud/subscription.py
Normal file
118
pillar/api/blender_cloud/subscription.py
Normal file
@ -0,0 +1,118 @@
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from flask import current_app, Blueprint
|
||||
|
||||
from pillar.api.utils import authorization
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
blueprint = Blueprint('blender_cloud.subscription', __name__)
|
||||
|
||||
|
||||
def fetch_subscription_info(email: str) -> typing.Optional[dict]:
|
||||
"""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
|
||||
}
|
||||
"""
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
external_subscriptions_server = current_app.config['EXTERNAL_SUBSCRIPTIONS_MANAGEMENT_SERVER']
|
||||
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
import urllib.parse
|
||||
|
||||
log_email = urllib.parse.quote(email)
|
||||
log.debug('Connecting to store at %s?blenderid=%s',
|
||||
external_subscriptions_server, log_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()
|
||||
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
import json
|
||||
log.debug('Received JSON from store API: %s',
|
||||
json.dumps(store_user, sort_keys=False, indent=4))
|
||||
|
||||
return store_user
|
||||
|
||||
|
||||
@blueprint.route('/update-subscription')
|
||||
@authorization.require_login()
|
||||
def update_subscription():
|
||||
"""Updates the subscription status of the current user.
|
||||
|
||||
Returns an empty HTTP response.
|
||||
"""
|
||||
|
||||
import pprint
|
||||
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()
|
||||
if not bid_user:
|
||||
my_log.warning('Logged in user %s has no BlenderID account! '
|
||||
'Unable to update subscription status.', user_id)
|
||||
return
|
||||
|
||||
# Use the Blender ID email address to check with the store. At least that reduces the
|
||||
# number of email addresses that could be out of sync to two (rather than three when we
|
||||
# use the email address from our local database).
|
||||
try:
|
||||
email = bid_user['email']
|
||||
except KeyError:
|
||||
my_log.error('Blender ID response did not include an email address, '
|
||||
'unable to update subscription status: %s',
|
||||
pprint.pformat(bid_user, compact=True))
|
||||
return
|
||||
store_user = fetch_subscription_info(email) or {}
|
||||
|
||||
# Handle the role changes via the badger service functionality.
|
||||
grant_subscriber = store_user.get('cloud_access', 0) == 1
|
||||
grant_demo = bid_user.get('roles', {}).get('cloud_demo', False)
|
||||
|
||||
is_subscriber = authorization.user_has_role('subscriber')
|
||||
is_demo = authorization.user_has_role('demo')
|
||||
|
||||
if grant_subscriber != is_subscriber:
|
||||
action = 'grant' if grant_subscriber else 'revoke'
|
||||
log.info('%sing subscriber role to user %s', action, user_id)
|
||||
service.do_badger(action, 'subscriber', user_id=user_id)
|
||||
|
||||
if grant_demo != is_demo:
|
||||
action = 'grant' if grant_demo else 'revoke'
|
||||
log.info('%sing demo role to user %s', action, user_id)
|
||||
service.do_badger(action, 'demo', user_id=user_id)
|
||||
|
||||
return '', 204
|
||||
|
||||
|
||||
def setup_app(app, url_prefix):
|
||||
log.info('Registering blueprint at %s', url_prefix)
|
||||
app.register_api_blueprint(blueprint, url_prefix=url_prefix)
|
@ -88,7 +88,8 @@ def upsert_user(db_user, blender_id_user_id):
|
||||
|
||||
if 'subscriber' in db_user.get('groups', []):
|
||||
log.error('Non-ObjectID string found in user.groups: %s', db_user)
|
||||
raise wz_exceptions.InternalServerError('Non-ObjectID string found in user.groups: %s' % db_user)
|
||||
raise wz_exceptions.InternalServerError(
|
||||
'Non-ObjectID string found in user.groups: %s' % db_user)
|
||||
|
||||
r = {}
|
||||
for retry in range(5):
|
||||
@ -237,5 +238,50 @@ def find_user_in_db(blender_id_user_id, user_info):
|
||||
return db_user
|
||||
|
||||
|
||||
def fetch_blenderid_user() -> dict:
|
||||
"""Returns the user info of the currently logged in user from BlenderID.
|
||||
|
||||
Returns an empty dict if communication fails.
|
||||
|
||||
Example dict:
|
||||
{
|
||||
"email": "some@email.example.com",
|
||||
"full_name": "dr. Sybren A. St\u00fcvel",
|
||||
"id": 5555,
|
||||
"roles": {
|
||||
"admin": true,
|
||||
"bfct_trainer": false,
|
||||
"cloud_single_member": true,
|
||||
"conference_speaker": true,
|
||||
"network_member": true
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
import urllib.parse
|
||||
import httplib2 # used by the oauth2 package
|
||||
|
||||
bid_url = urllib.parse.urljoin(blender_id_endpoint(), 'api/user')
|
||||
log.debug('Fetching user info from %s', bid_url)
|
||||
|
||||
try:
|
||||
bid_resp = current_app.oauth_blender_id.get(bid_url)
|
||||
except httplib2.HttpLib2Error:
|
||||
log.exception('Error getting %s from BlenderID', bid_url)
|
||||
return {}
|
||||
|
||||
if bid_resp.status != 200:
|
||||
log.warning('Error %i from BlenderID %s: %s', bid_resp.status, bid_url, bid_resp.data)
|
||||
return {}
|
||||
|
||||
if not bid_resp.data:
|
||||
log.warning('Empty data returned from BlenderID %s', bid_url)
|
||||
return {}
|
||||
|
||||
log.debug('BlenderID returned %s', bid_resp.data)
|
||||
return bid_resp.data
|
||||
|
||||
|
||||
def setup_app(app, url_prefix):
|
||||
app.register_api_blueprint(blender_id, url_prefix=url_prefix)
|
||||
|
@ -3,11 +3,14 @@
|
||||
import logging
|
||||
|
||||
import blinker
|
||||
import bson
|
||||
|
||||
from flask import Blueprint, current_app, request
|
||||
from werkzeug import exceptions as wz_exceptions
|
||||
|
||||
from pillar.api import local_auth
|
||||
from pillar.api.utils import mongo
|
||||
from pillar.api.utils import authorization, authentication, str2id, jsonify
|
||||
from werkzeug import exceptions as wz_exceptions
|
||||
|
||||
blueprint = Blueprint('service', __name__)
|
||||
log = logging.getLogger(__name__)
|
||||
@ -70,16 +73,19 @@ def badger():
|
||||
action, user_email, role, action, role)
|
||||
return 'Role not allowed', 403
|
||||
|
||||
return do_badger(action, user_email, role)
|
||||
return do_badger(action, role, user_email=user_email)
|
||||
|
||||
|
||||
def do_badger(action, user_email, role):
|
||||
"""Performs a badger action, returning a HTTP response."""
|
||||
def do_badger(action: str, role: str, *, user_email: str='', user_id: bson.ObjectId=None):
|
||||
"""Performs a badger action, returning a HTTP response.
|
||||
|
||||
Either user_email or user_id must be given.
|
||||
"""
|
||||
|
||||
if action not in {'grant', 'revoke'}:
|
||||
raise wz_exceptions.BadRequest('Action %r not supported' % action)
|
||||
|
||||
if not user_email:
|
||||
if not user_email and user_id is None:
|
||||
raise wz_exceptions.BadRequest('User email not given')
|
||||
|
||||
if not role:
|
||||
@ -88,9 +94,14 @@ def do_badger(action, user_email, role):
|
||||
users_coll = current_app.data.driver.db['users']
|
||||
|
||||
# Fetch the user
|
||||
db_user = users_coll.find_one({'email': user_email}, projection={'roles': 1, 'groups': 1})
|
||||
if user_email:
|
||||
query = {'email': user_email}
|
||||
else:
|
||||
query = user_id
|
||||
db_user = users_coll.find_one(query, projection={'roles': 1, 'groups': 1})
|
||||
if db_user is None:
|
||||
log.warning('badger(%s, %s, %s): user not found', action, user_email, role)
|
||||
log.warning('badger(%s, %s, user_email=%s, user_id=%s): user not found',
|
||||
action, role, user_email, user_id)
|
||||
return 'User not found', 404
|
||||
|
||||
# Apply the action
|
||||
|
@ -93,7 +93,10 @@ def get_blender_id_oauth_token():
|
||||
def config_oauth_login(app):
|
||||
config = app.config
|
||||
if not config.get('SOCIAL_BLENDER_ID'):
|
||||
log.info('OAuth Blender-ID login not setup.')
|
||||
log.info('OAuth Blender-ID login not set up, no app config SOCIAL_BLENDER_ID.')
|
||||
return None
|
||||
if not config.get('BLENDER_ID_OAUTH_URL'):
|
||||
log.error('Unable to use Blender ID, missing configuration BLENDER_ID_OAUTH_URL.')
|
||||
return None
|
||||
|
||||
oauth = flask_oauthlib.client.OAuth(app)
|
||||
|
@ -1,51 +0,0 @@
|
||||
"""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
|
||||
|
@ -306,7 +306,7 @@ def badger(action, user_email, role):
|
||||
|
||||
with current_app.app_context():
|
||||
service.fetch_role_to_group_id_map()
|
||||
response, status = service.do_badger(action, user_email, role)
|
||||
response, status = service.do_badger(action, role, user_email=user_email)
|
||||
|
||||
if status == 204:
|
||||
log.info('Done.')
|
||||
|
@ -12,3 +12,5 @@ ROLES_FOR_UNLIMITED_UPLOADS = {'subscriber', 'demo', 'admin'}
|
||||
|
||||
GCLOUD_APP_CREDENTIALS = 'invalid-file-because-gcloud-storage-should-be-mocked-in-tests'
|
||||
STORAGE_BACKEND = 'local'
|
||||
|
||||
EXTERNAL_SUBSCRIPTIONS_MANAGEMENT_SERVER = "http://store.localhost/api"
|
||||
|
@ -1,8 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
import httplib2 # used by the oauth2 package
|
||||
import requests
|
||||
import urllib.parse
|
||||
|
||||
from flask import (abort, Blueprint, current_app, flash, redirect,
|
||||
render_template, request, session, url_for)
|
||||
@ -10,8 +8,8 @@ from flask_login import login_required, logout_user, current_user
|
||||
from flask_oauthlib.client import OAuthException
|
||||
from werkzeug import exceptions as wz_exceptions
|
||||
|
||||
import pillar.api.blender_cloud.subscription
|
||||
import pillar.auth
|
||||
from pillar.auth import subscriptions
|
||||
from pillar.web import system_util
|
||||
from .forms import UserProfileForm
|
||||
from .forms import UserSettingsEmailsForm
|
||||
@ -46,6 +44,8 @@ def login():
|
||||
|
||||
@blueprint.route('/oauth/blender-id/authorized')
|
||||
def blender_id_authorized():
|
||||
from pillar.api.blender_cloud import subscription
|
||||
|
||||
check_oauth_provider(current_app.oauth_blender_id)
|
||||
try:
|
||||
oauth_resp = current_app.oauth_blender_id.authorized_response()
|
||||
@ -70,7 +70,7 @@ def blender_id_authorized():
|
||||
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(current_user.objectid)
|
||||
subscription.update_subscription()
|
||||
|
||||
next_after_login = session.get('next_after_login')
|
||||
if next_after_login:
|
||||
@ -188,7 +188,7 @@ def settings_billing():
|
||||
group = Group.find(group_id, api=api)
|
||||
groups.append(group.name)
|
||||
|
||||
store_user = subscriptions.fetch_user(user.email)
|
||||
store_user = pillar.api.blender_cloud.subscription.fetch_subscription_info(user.email)
|
||||
|
||||
return render_template(
|
||||
'users/settings/billing.html',
|
||||
@ -247,91 +247,3 @@ 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):
|
||||
"""Update the user's roles based on the store subscription status and BlenderID roles."""
|
||||
|
||||
api = system_util.pillar_api()
|
||||
group_subscriber = Group.find_one({'where': {'name': 'subscriber'}}, api=api)
|
||||
group_demo = Group.find_one({'where': {'name': 'demo'}}, api=api)
|
||||
|
||||
# Fetch the user once outside the loop, because we only need to get the
|
||||
# subscription status once.
|
||||
user = User.me(api=api)
|
||||
|
||||
# Fetch user info from different sources.
|
||||
store_user = subscriptions.fetch_user(user.email) or {}
|
||||
bid_user = fetch_blenderid_user()
|
||||
|
||||
grant_subscriber = store_user.get('cloud_access', 0) == 1
|
||||
grant_demo = bid_user.get('roles', {}).get('cloud_demo', False)
|
||||
|
||||
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 grant_subscriber:
|
||||
roles.add('subscriber')
|
||||
groups.add(group_subscriber._id)
|
||||
elif 'admin' not in roles:
|
||||
# Don't take away roles from admins.
|
||||
roles.discard('subscriber')
|
||||
groups.discard(group_subscriber._id)
|
||||
|
||||
if grant_demo:
|
||||
roles.add('demo')
|
||||
groups.add(group_demo._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)
|
||||
|
||||
|
||||
def fetch_blenderid_user():
|
||||
"""Returns the user info from BlenderID.
|
||||
|
||||
Returns an empty dict if communication fails.
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
bid_url = urllib.parse.urljoin(current_app.config['BLENDER_ID_ENDPOINT'], 'api/user')
|
||||
log.debug('Fetching user info from %s', bid_url)
|
||||
|
||||
try:
|
||||
bid_resp = current_app.oauth_blender_id.get(bid_url)
|
||||
except httplib2.HttpLib2Error:
|
||||
log.exception('Error getting %s from BlenderID', bid_url)
|
||||
return {}
|
||||
|
||||
if bid_resp.status != 200:
|
||||
log.warning('Error %i from BlenderID %s: %s', bid_resp.status, bid_url, bid_resp.data)
|
||||
return {}
|
||||
|
||||
if not bid_resp.data:
|
||||
log.warning('Empty data returned from BlenderID %s', bid_url)
|
||||
return {}
|
||||
|
||||
log.debug('BlenderID returned %s', bid_resp.data)
|
||||
return bid_resp.data
|
||||
|
110
tests/test_api/test_subscriptions.py
Normal file
110
tests/test_api/test_subscriptions.py
Normal file
@ -0,0 +1,110 @@
|
||||
from unittest import mock
|
||||
|
||||
import responses
|
||||
|
||||
from pillar.tests import AbstractPillarTest, TEST_EMAIL_ADDRESS
|
||||
|
||||
|
||||
# The OAuth lib doesn't use requests, so we can't use responses to mock it.
|
||||
# Instead, we use unittest.mock for that.
|
||||
|
||||
|
||||
class RoleUpdatingTest(AbstractPillarTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
with self.app.test_request_context():
|
||||
self.create_standard_groups()
|
||||
|
||||
def _setup_testcase(self, mocked_fetch_blenderid_user, *,
|
||||
store_says_cloud_access: bool,
|
||||
bid_says_cloud_demo: bool):
|
||||
import urllib.parse
|
||||
url = '%s?blenderid=%s' % (self.app.config['EXTERNAL_SUBSCRIPTIONS_MANAGEMENT_SERVER'],
|
||||
urllib.parse.quote(TEST_EMAIL_ADDRESS))
|
||||
responses.add('GET', url,
|
||||
json={'shop_id': 58432,
|
||||
'cloud_access': 1 if store_says_cloud_access else 0,
|
||||
'paid_balance': 0,
|
||||
'balance_currency': 'EUR',
|
||||
'start_date': '2017-05-04 12:07:49',
|
||||
'expiration_date': '2017-08-04 10:07:49',
|
||||
'subscription_status': 'wc-active'
|
||||
},
|
||||
status=200,
|
||||
match_querystring=True)
|
||||
self.mock_blenderid_validate_happy()
|
||||
mocked_fetch_blenderid_user.return_value = {
|
||||
'email': TEST_EMAIL_ADDRESS,
|
||||
'full_name': 'dr. Sybren A. St\u00fcvel',
|
||||
'id': 5555,
|
||||
'roles': {
|
||||
'admin': True,
|
||||
'bfct_trainer': False,
|
||||
'conference_speaker': True,
|
||||
'network_member': True
|
||||
}
|
||||
}
|
||||
if bid_says_cloud_demo:
|
||||
mocked_fetch_blenderid_user.return_value['roles']['cloud_demo'] = True
|
||||
|
||||
@responses.activate
|
||||
@mock.patch('pillar.api.blender_id.fetch_blenderid_user')
|
||||
def test_store_api_role_grant_subscriber(self, mocked_fetch_blenderid_user):
|
||||
self._setup_testcase(mocked_fetch_blenderid_user,
|
||||
store_says_cloud_access=True,
|
||||
bid_says_cloud_demo=False)
|
||||
|
||||
self.get('/api/bcloud/update-subscription', auth_token='my-happy-token',
|
||||
expected_status=204)
|
||||
user_info = self.get('/api/users/me', auth_token='my-happy-token').json()
|
||||
self.assertEqual(['subscriber'], user_info['roles'])
|
||||
|
||||
@responses.activate
|
||||
@mock.patch('pillar.api.blender_id.fetch_blenderid_user')
|
||||
def test_store_api_role_revoke_subscriber(self, mocked_fetch_blenderid_user):
|
||||
self._setup_testcase(mocked_fetch_blenderid_user,
|
||||
store_says_cloud_access=False,
|
||||
bid_says_cloud_demo=False)
|
||||
|
||||
# Make sure this user is currently known as a subcriber.
|
||||
self.create_user(roles={'subscriber'}, token='my-happy-token')
|
||||
user_info = self.get('/api/users/me', auth_token='my-happy-token').json()
|
||||
self.assertEqual(['subscriber'], user_info['roles'])
|
||||
|
||||
# And after updating, it shouldn't be.
|
||||
self.get('/api/bcloud/update-subscription', auth_token='my-happy-token',
|
||||
expected_status=204)
|
||||
user_info = self.get('/api/users/me', auth_token='my-happy-token').json()
|
||||
self.assertEqual([], user_info['roles'])
|
||||
|
||||
@responses.activate
|
||||
@mock.patch('pillar.api.blender_id.fetch_blenderid_user')
|
||||
def test_bid_api_grant_demo(self, mocked_fetch_blenderid_user):
|
||||
self._setup_testcase(mocked_fetch_blenderid_user,
|
||||
store_says_cloud_access=False,
|
||||
bid_says_cloud_demo=True)
|
||||
|
||||
self.get('/api/bcloud/update-subscription', auth_token='my-happy-token',
|
||||
expected_status=204)
|
||||
|
||||
user_info = self.get('/api/users/me', auth_token='my-happy-token').json()
|
||||
self.assertEqual(['demo'], user_info['roles'])
|
||||
|
||||
@responses.activate
|
||||
@mock.patch('pillar.api.blender_id.fetch_blenderid_user')
|
||||
def test_bid_api_role_revoke_subscriber(self, mocked_fetch_blenderid_user):
|
||||
self._setup_testcase(mocked_fetch_blenderid_user,
|
||||
store_says_cloud_access=False,
|
||||
bid_says_cloud_demo=False)
|
||||
|
||||
# Make sure this user is currently known as demo user.
|
||||
self.create_user(roles={'demo'}, token='my-happy-token')
|
||||
user_info = self.get('/api/users/me', auth_token='my-happy-token').json()
|
||||
self.assertEqual(['demo'], user_info['roles'])
|
||||
|
||||
# And after updating, it shouldn't be.
|
||||
self.get('/api/bcloud/update-subscription', auth_token='my-happy-token',
|
||||
expected_status=204)
|
||||
user_info = self.get('/api/users/me', auth_token='my-happy-token').json()
|
||||
self.assertEqual([], user_info['roles'])
|
Loading…
x
Reference in New Issue
Block a user