Merge branch 'master' into elastic
This commit is contained in:
commit
b7773e69c7
@ -21,6 +21,7 @@ from flask_babel import Babel, gettext as _
|
|||||||
from flask.templating import TemplateNotFound
|
from flask.templating import TemplateNotFound
|
||||||
import pymongo.collection
|
import pymongo.collection
|
||||||
import pymongo.database
|
import pymongo.database
|
||||||
|
from raven.contrib.flask import Sentry
|
||||||
from werkzeug.local import LocalProxy
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
|
|
||||||
@ -59,7 +60,17 @@ class ConfigurationMissingError(SystemExit):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class PillarServer(Eve):
|
class BlinkerCompatibleEve(Eve):
|
||||||
|
"""Workaround for https://github.com/pyeve/eve/issues/1087"""
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if name in {"im_self", "im_func"}:
|
||||||
|
raise AttributeError("type object '%s' has no attribute '%s'" %
|
||||||
|
(self.__class__.__name__, name))
|
||||||
|
return super().__getattr__(name)
|
||||||
|
|
||||||
|
|
||||||
|
class PillarServer(BlinkerCompatibleEve):
|
||||||
def __init__(self, app_root, **kwargs):
|
def __init__(self, app_root, **kwargs):
|
||||||
from .extension import PillarExtension
|
from .extension import PillarExtension
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
@ -75,7 +86,7 @@ class PillarServer(Eve):
|
|||||||
# The default roles Pillar uses. Will probably all move to extensions at some point.
|
# The default roles Pillar uses. Will probably all move to extensions at some point.
|
||||||
self._user_roles: typing.Set[str] = {
|
self._user_roles: typing.Set[str] = {
|
||||||
'demo', 'admin', 'subscriber', 'homeproject',
|
'demo', 'admin', 'subscriber', 'homeproject',
|
||||||
'protected',
|
'protected', 'org-subscriber', 'video-encoder',
|
||||||
'service', 'badger', 'svner', 'urler',
|
'service', 'badger', 'svner', 'urler',
|
||||||
}
|
}
|
||||||
self._user_roles_indexable: typing.Set[str] = {'demo', 'admin', 'subscriber'}
|
self._user_roles_indexable: typing.Set[str] = {'demo', 'admin', 'subscriber'}
|
||||||
@ -94,7 +105,9 @@ class PillarServer(Eve):
|
|||||||
self._config_auth_token_hmac_key()
|
self._config_auth_token_hmac_key()
|
||||||
self._config_tempdirs()
|
self._config_tempdirs()
|
||||||
self._config_git()
|
self._config_git()
|
||||||
self._config_bugsnag()
|
|
||||||
|
self.sentry: typing.Optional[Sentry] = None
|
||||||
|
self._config_sentry()
|
||||||
self._config_google_cloud_storage()
|
self._config_google_cloud_storage()
|
||||||
|
|
||||||
self.algolia_index_users = None
|
self.algolia_index_users = None
|
||||||
@ -187,39 +200,19 @@ class PillarServer(Eve):
|
|||||||
self.config['GIT_REVISION'] = 'unknown'
|
self.config['GIT_REVISION'] = 'unknown'
|
||||||
self.log.info('Git revision %r', self.config['GIT_REVISION'])
|
self.log.info('Git revision %r', self.config['GIT_REVISION'])
|
||||||
|
|
||||||
def _config_bugsnag(self):
|
def _config_sentry(self):
|
||||||
bugsnag_api_key = self.config.get('BUGSNAG_API_KEY')
|
sentry_dsn = self.config.get('SENTRY_CONFIG', {}).get('dsn')
|
||||||
if self.config.get('TESTING') or not bugsnag_api_key:
|
if self.config.get('TESTING') or sentry_dsn in {'', '-set-in-config-local-'}:
|
||||||
self.log.info('Bugsnag NOT configured.')
|
self.log.warning('Sentry NOT configured.')
|
||||||
|
self.sentry = None
|
||||||
return
|
return
|
||||||
|
|
||||||
import bugsnag
|
self.sentry = Sentry(self, logging=True, level=logging.WARNING,
|
||||||
from bugsnag.handlers import BugsnagHandler
|
logging_exclusions=('werkzeug',))
|
||||||
|
|
||||||
release_stage = self.config.get('BUGSNAG_RELEASE_STAGE', 'unconfigured')
|
# bugsnag.before_notify(bugsnag_extra.add_pillar_request_to_notification)
|
||||||
if self.config.get('DEBUG'):
|
# got_request_exception.connect(self.__notify_bugsnag)
|
||||||
release_stage += '-debug'
|
self.log.info('Sentry setup complete')
|
||||||
|
|
||||||
bugsnag.configure(
|
|
||||||
api_key=bugsnag_api_key,
|
|
||||||
project_root="/data/git/pillar/pillar",
|
|
||||||
release_stage=release_stage
|
|
||||||
)
|
|
||||||
|
|
||||||
bs_handler = BugsnagHandler()
|
|
||||||
bs_handler.setLevel(logging.ERROR)
|
|
||||||
self.log.addHandler(bs_handler)
|
|
||||||
|
|
||||||
# This is what bugsnag.flask.handle_exceptions also tries to do,
|
|
||||||
# but it passes the app to the connect() call, which causes an
|
|
||||||
# error. Since we only have one app, we can do without.
|
|
||||||
from flask import got_request_exception
|
|
||||||
from . import bugsnag_extra
|
|
||||||
|
|
||||||
bugsnag.before_notify(bugsnag_extra.add_pillar_request_to_notification)
|
|
||||||
got_request_exception.connect(self.__notify_bugsnag)
|
|
||||||
|
|
||||||
self.log.info('Bugsnag setup complete')
|
|
||||||
|
|
||||||
def __notify_bugsnag(self, sender, exception, **extra):
|
def __notify_bugsnag(self, sender, exception, **extra):
|
||||||
import bugsnag
|
import bugsnag
|
||||||
|
@ -1,94 +1,37 @@
|
|||||||
import logging
|
import logging
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from flask import current_app, Blueprint
|
from flask import Blueprint, Response
|
||||||
|
import requests
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
|
||||||
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__)
|
log = logging.getLogger(__name__)
|
||||||
blueprint = Blueprint('blender_cloud.subscription', __name__)
|
blueprint = Blueprint('blender_cloud.subscription', __name__)
|
||||||
|
|
||||||
|
# Mapping from roles on Blender ID to roles here in Pillar.
|
||||||
def fetch_subscription_info(email: str) -> typing.Optional[dict]:
|
# Roles not mentioned here will not be synced from Blender ID.
|
||||||
"""Returns the user info dict from the external subscriptions management server.
|
ROLES_BID_TO_PILLAR = {
|
||||||
|
'cloud_subscriber': 'subscriber',
|
||||||
:returns: the store user info, or None if the user can't be found or there
|
'cloud_demo': 'demo',
|
||||||
was an error communicating. A dict like this is returned:
|
'cloud_has_subscription': 'has_subscription',
|
||||||
{
|
}
|
||||||
"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
|
|
||||||
import requests.exceptions
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
try:
|
|
||||||
r = s.get(external_subscriptions_server,
|
|
||||||
params={'blenderid': email},
|
|
||||||
verify=current_app.config['TLS_CERT_FILE'],
|
|
||||||
timeout=current_app.config.get('EXTERNAL_SUBSCRIPTIONS_TIMEOUT_SECS', 10))
|
|
||||||
except requests.exceptions.ConnectionError as ex:
|
|
||||||
log.error('Error connecting to %s: %s', external_subscriptions_server, ex)
|
|
||||||
return None
|
|
||||||
except requests.exceptions.Timeout as ex:
|
|
||||||
log.error('Timeout communicating with %s: %s', external_subscriptions_server, ex)
|
|
||||||
return None
|
|
||||||
except requests.exceptions.RequestException as ex:
|
|
||||||
log.error('Some error communicating with %s: %s', external_subscriptions_server, ex)
|
|
||||||
return None
|
|
||||||
|
|
||||||
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')
|
@blueprint.route('/update-subscription')
|
||||||
@authorization.require_login()
|
@authorization.require_login()
|
||||||
def update_subscription():
|
def update_subscription() -> typing.Tuple[str, int]:
|
||||||
"""Updates the subscription status of the current user.
|
"""Updates the subscription status of the current user.
|
||||||
|
|
||||||
Returns an empty HTTP response.
|
Returns an empty HTTP response.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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')
|
my_log: logging.Logger = log.getChild('update_subscription')
|
||||||
user_id = authentication.current_user_id()
|
current_user = auth.get_current_user()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bid_user = blender_id.fetch_blenderid_user()
|
bid_user = blender_id.fetch_blenderid_user()
|
||||||
@ -98,46 +41,125 @@ def update_subscription():
|
|||||||
|
|
||||||
if not bid_user:
|
if not bid_user:
|
||||||
my_log.warning('Logged in user %s has no BlenderID account! '
|
my_log.warning('Logged in user %s has no BlenderID account! '
|
||||||
'Unable to update subscription status.', user_id)
|
'Unable to update subscription status.', current_user.user_id)
|
||||||
return '', 204
|
return '', 204
|
||||||
|
|
||||||
# Use the Blender ID email address to check with the store. At least that reduces the
|
do_update_subscription(current_user, bid_user)
|
||||||
# number of email addresses that could be out of sync to two (rather than three when we
|
return '', 204
|
||||||
# use the email address from our local database).
|
|
||||||
|
|
||||||
|
@blueprint.route('/update-subscription-for/<user_id>', 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.
|
||||||
|
|
||||||
|
bid_user should be a dict like:
|
||||||
|
{'id': 1234,
|
||||||
|
'full_name': 'मूंगफली मक्खन प्रेमी',
|
||||||
|
'email': 'here@example.com',
|
||||||
|
'roles': {'cloud_demo': True}}
|
||||||
|
|
||||||
|
The 'roles' key can also be an interable of role names instead of a dict.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pillar.api import service
|
||||||
|
|
||||||
|
my_log: logging.Logger = log.getChild('do_update_subscription')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
email = bid_user['email']
|
email = bid_user['email']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
my_log.error('Blender ID response did not include an email address, '
|
email = '-missing email-'
|
||||||
'unable to update subscription status: %s',
|
|
||||||
pprint.pformat(bid_user, compact=True))
|
# Transform the BID roles from a dict to a set.
|
||||||
return 'Internal error', 500
|
bidr = bid_user.get('roles', set())
|
||||||
store_user = fetch_subscription_info(email) or {}
|
if isinstance(bidr, dict):
|
||||||
|
bid_roles = {role
|
||||||
|
for role, has_role in bid_user.get('roles', {}).items()
|
||||||
|
if has_role}
|
||||||
|
else:
|
||||||
|
bid_roles = set(bidr)
|
||||||
|
|
||||||
# Handle the role changes via the badger service functionality.
|
# Handle the role changes via the badger service functionality.
|
||||||
grant_subscriber = store_user.get('cloud_access', 0) == 1
|
plr_roles = set(local_user.roles)
|
||||||
grant_demo = bid_user.get('roles', {}).get('cloud_demo', False)
|
|
||||||
|
|
||||||
is_subscriber = authorization.user_has_role('subscriber')
|
grant_roles = set()
|
||||||
is_demo = authorization.user_has_role('demo')
|
revoke_roles = set()
|
||||||
|
for bid_role, plr_role in ROLES_BID_TO_PILLAR.items():
|
||||||
|
if bid_role in bid_roles and plr_role not in plr_roles:
|
||||||
|
grant_roles.add(plr_role)
|
||||||
|
continue
|
||||||
|
if bid_role not in bid_roles and plr_role in plr_roles:
|
||||||
|
revoke_roles.add(plr_role)
|
||||||
|
|
||||||
if grant_subscriber != is_subscriber:
|
user_id = local_user.user_id
|
||||||
action = 'grant' if grant_subscriber else 'revoke'
|
|
||||||
my_log.info('%sing subscriber role to user %s (Blender ID email %s)',
|
|
||||||
action, user_id, email)
|
|
||||||
service.do_badger(action, role='subscriber', user_id=user_id)
|
|
||||||
else:
|
|
||||||
my_log.debug('Not changing subscriber role, grant=%r and is=%s',
|
|
||||||
grant_subscriber, is_subscriber)
|
|
||||||
|
|
||||||
if grant_demo != is_demo:
|
if grant_roles:
|
||||||
action = 'grant' if grant_demo else 'revoke'
|
if my_log.isEnabledFor(logging.INFO):
|
||||||
my_log.info('%sing demo role to user %s (Blender ID email %s)', action, user_id, email)
|
my_log.info('granting roles to user %s (Blender ID %s): %s',
|
||||||
service.do_badger(action, role='demo', user_id=user_id)
|
user_id, email, ', '.join(sorted(grant_roles)))
|
||||||
else:
|
service.do_badger('grant', roles=grant_roles, user_id=user_id)
|
||||||
my_log.debug('Not changing demo role, grant=%r and is=%s',
|
|
||||||
grant_demo, is_demo)
|
|
||||||
|
|
||||||
return '', 204
|
if revoke_roles:
|
||||||
|
if my_log.isEnabledFor(logging.INFO):
|
||||||
|
my_log.info('revoking roles to user %s (Blender ID %s): %s',
|
||||||
|
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):
|
def setup_app(app, url_prefix):
|
||||||
|
@ -168,6 +168,24 @@ def _compute_token_expiry(token_expires_string):
|
|||||||
return min(blid_expiry, our_expiry)
|
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:
|
def fetch_blenderid_user() -> dict:
|
||||||
"""Returns the user info of the currently logged in user from BlenderID.
|
"""Returns the user info of the currently logged in user from BlenderID.
|
||||||
|
|
||||||
@ -181,7 +199,8 @@ def fetch_blenderid_user() -> dict:
|
|||||||
"roles": {
|
"roles": {
|
||||||
"admin": true,
|
"admin": true,
|
||||||
"bfct_trainer": false,
|
"bfct_trainer": false,
|
||||||
"cloud_single_member": true,
|
"cloud_has_subscription": true,
|
||||||
|
"cloud_subscriber": true,
|
||||||
"conference_speaker": true,
|
"conference_speaker": true,
|
||||||
"network_member": true
|
"network_member": true
|
||||||
}
|
}
|
||||||
@ -218,12 +237,13 @@ def fetch_blenderid_user() -> dict:
|
|||||||
log.warning('Error %i from BlenderID %s: %s', bid_resp.status_code, bid_url, bid_resp.text)
|
log.warning('Error %i from BlenderID %s: %s', bid_resp.status_code, bid_url, bid_resp.text)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
if not bid_resp.json():
|
payload = bid_resp.json()
|
||||||
|
if not payload:
|
||||||
log.warning('Empty data returned from BlenderID %s', bid_url)
|
log.warning('Empty data returned from BlenderID %s', bid_url)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
log.debug('BlenderID returned %s', bid_resp.json())
|
log.debug('BlenderID returned %s', payload)
|
||||||
return bid_resp.json()
|
return payload
|
||||||
|
|
||||||
|
|
||||||
def setup_app(app, url_prefix):
|
def setup_app(app, url_prefix):
|
||||||
|
@ -26,7 +26,7 @@ from flask import url_for, helpers
|
|||||||
from pillar.api import utils
|
from pillar.api import utils
|
||||||
from pillar.api.file_storage_backends.gcs import GoogleCloudStorageBucket, \
|
from pillar.api.file_storage_backends.gcs import GoogleCloudStorageBucket, \
|
||||||
GoogleCloudStorageBlob
|
GoogleCloudStorageBlob
|
||||||
from pillar.api.utils import remove_private_keys, authentication
|
from pillar.api.utils import remove_private_keys
|
||||||
from pillar.api.utils.authorization import require_login, user_has_role, \
|
from pillar.api.utils.authorization import require_login, user_has_role, \
|
||||||
user_matches_roles
|
user_matches_roles
|
||||||
from pillar.api.utils.cdn import hash_file_path
|
from pillar.api.utils.cdn import hash_file_path
|
||||||
@ -291,8 +291,8 @@ def process_file(bucket: Bucket,
|
|||||||
# TODO: overrule the content type based on file extention & magic numbers.
|
# TODO: overrule the content type based on file extention & magic numbers.
|
||||||
mime_category, src_file['format'] = src_file['content_type'].split('/', 1)
|
mime_category, src_file['format'] = src_file['content_type'].split('/', 1)
|
||||||
|
|
||||||
# Prevent video handling for non-admins.
|
# Only allow video encoding when the user has the correct capability.
|
||||||
if not user_has_role('admin') and mime_category == 'video':
|
if not current_user.has_cap('encode-video') and mime_category == 'video':
|
||||||
if src_file['format'].startswith('x-'):
|
if src_file['format'].startswith('x-'):
|
||||||
xified = src_file['format']
|
xified = src_file['format']
|
||||||
else:
|
else:
|
||||||
@ -300,7 +300,7 @@ def process_file(bucket: Bucket,
|
|||||||
|
|
||||||
src_file['content_type'] = 'application/%s' % xified
|
src_file['content_type'] = 'application/%s' % xified
|
||||||
mime_category = 'application'
|
mime_category = 'application'
|
||||||
log.info('Not processing video file %s for non-admin user', file_id)
|
log.info('Not processing video file %s for non-video-encoding user', file_id)
|
||||||
|
|
||||||
# Run the required processor, based on the MIME category.
|
# Run the required processor, based on the MIME category.
|
||||||
processors: typing.Mapping[str, typing.Callable] = {
|
processors: typing.Mapping[str, typing.Callable] = {
|
||||||
|
@ -18,7 +18,7 @@ log = logging.getLogger(__name__)
|
|||||||
CAPABILITIES = collections.defaultdict(**{
|
CAPABILITIES = collections.defaultdict(**{
|
||||||
'subscriber': {'subscriber', 'home-project'},
|
'subscriber': {'subscriber', 'home-project'},
|
||||||
'demo': {'subscriber', 'home-project'},
|
'demo': {'subscriber', 'home-project'},
|
||||||
'admin': {'subscriber', 'home-project', 'video-encoding', 'admin',
|
'admin': {'video-encoding', 'admin',
|
||||||
'view-pending-nodes', 'edit-project-node-types'},
|
'view-pending-nodes', 'edit-project-node-types'},
|
||||||
}, default_factory=frozenset)
|
}, default_factory=frozenset)
|
||||||
|
|
||||||
|
@ -65,8 +65,13 @@ GOOGLE_SITE_VERIFICATION = ''
|
|||||||
|
|
||||||
ADMIN_USER_GROUP = '5596e975ea893b269af85c0e'
|
ADMIN_USER_GROUP = '5596e975ea893b269af85c0e'
|
||||||
SUBSCRIBER_USER_GROUP = '5596e975ea893b269af85c0f'
|
SUBSCRIBER_USER_GROUP = '5596e975ea893b269af85c0f'
|
||||||
BUGSNAG_API_KEY = ''
|
|
||||||
BUGSNAG_RELEASE_STAGE = 'development'
|
SENTRY_CONFIG = {
|
||||||
|
'dsn': '-set-in-config-local-',
|
||||||
|
# 'release': raven.fetch_git_sha(os.path.dirname(__file__)),
|
||||||
|
}
|
||||||
|
# See https://docs.sentry.io/clients/python/integrations/flask/#settings
|
||||||
|
SENTRY_USER_ATTRS = ['username', 'full_name', 'email', 'objectid']
|
||||||
|
|
||||||
ALGOLIA_USER = '-SECRET-'
|
ALGOLIA_USER = '-SECRET-'
|
||||||
ALGOLIA_API_KEY = '-SECRET-'
|
ALGOLIA_API_KEY = '-SECRET-'
|
||||||
@ -106,6 +111,12 @@ FULL_FILE_ACCESS_ROLES = {'admin', 'subscriber', 'demo'}
|
|||||||
BLENDER_ID_CLIENT_ID = 'SPECIAL-SNOWFLAKE-57'
|
BLENDER_ID_CLIENT_ID = 'SPECIAL-SNOWFLAKE-57'
|
||||||
BLENDER_ID_SUBCLIENT_ID = 'PILLAR'
|
BLENDER_ID_SUBCLIENT_ID = 'PILLAR'
|
||||||
|
|
||||||
|
# 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-'
|
||||||
|
|
||||||
# Collection of supported OAuth providers (Blender ID, Facebook and Google).
|
# Collection of supported OAuth providers (Blender ID, Facebook and Google).
|
||||||
# Example entry:
|
# Example entry:
|
||||||
# OAUTH_CREDENTIALS = {
|
# OAUTH_CREDENTIALS = {
|
||||||
@ -180,9 +191,6 @@ URLER_SERVICE_AUTH_TOKEN = None
|
|||||||
# front-end.
|
# front-end.
|
||||||
BLENDER_CLOUD_ADDON_VERSION = '1.4'
|
BLENDER_CLOUD_ADDON_VERSION = '1.4'
|
||||||
|
|
||||||
EXTERNAL_SUBSCRIPTIONS_MANAGEMENT_SERVER = 'https://store.blender.org/api/'
|
|
||||||
EXTERNAL_SUBSCRIPTIONS_TIMEOUT_SECS = 10
|
|
||||||
|
|
||||||
# Certificate file for communication with other systems.
|
# Certificate file for communication with other systems.
|
||||||
TLS_CERT_FILE = requests.certs.where()
|
TLS_CERT_FILE = requests.certs.where()
|
||||||
|
|
||||||
@ -204,8 +212,9 @@ CELERY_BEAT_SCHEDULE = {
|
|||||||
USER_CAPABILITIES = defaultdict(**{
|
USER_CAPABILITIES = defaultdict(**{
|
||||||
'subscriber': {'subscriber', 'home-project'},
|
'subscriber': {'subscriber', 'home-project'},
|
||||||
'demo': {'subscriber', 'home-project'},
|
'demo': {'subscriber', 'home-project'},
|
||||||
'admin': {'subscriber', 'home-project', 'video-encoding', 'admin',
|
'admin': {'encode-video', 'admin',
|
||||||
'view-pending-nodes', 'edit-project-node-types', 'create-organization'},
|
'view-pending-nodes', 'edit-project-node-types', 'create-organization'},
|
||||||
|
'video-encoder': {'encode-video'},
|
||||||
'org-subscriber': {'subscriber', 'home-project'},
|
'org-subscriber': {'subscriber', 'home-project'},
|
||||||
}, default_factory=frozenset)
|
}, default_factory=frozenset)
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ def comments_for_node(node_id):
|
|||||||
project = Project({'_id': node.project})
|
project = Project({'_id': node.project})
|
||||||
can_post_comments = project.node_type_has_method('comment', 'POST', api=api)
|
can_post_comments = project.node_type_has_method('comment', 'POST', api=api)
|
||||||
can_comment_override = request.args.get('can_comment', 'True') == 'True'
|
can_comment_override = request.args.get('can_comment', 'True') == 'True'
|
||||||
can_post_comments = can_post_comments and can_comment_override
|
can_post_comments = can_post_comments and can_comment_override and current_user.has_cap('subscriber')
|
||||||
|
|
||||||
# Query for all children, i.e. comments on the node.
|
# Query for all children, i.e. comments on the node.
|
||||||
comments = Node.all({
|
comments = Node.all({
|
||||||
|
@ -23,13 +23,10 @@ def profile():
|
|||||||
api = system_util.pillar_api()
|
api = system_util.pillar_api()
|
||||||
user = User.find(current_user.objectid, api=api)
|
user = User.find(current_user.objectid, api=api)
|
||||||
|
|
||||||
form = forms.UserProfileForm(
|
form = forms.UserProfileForm(username=user.username)
|
||||||
full_name=user.full_name,
|
|
||||||
username=user.username)
|
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
try:
|
try:
|
||||||
user.full_name = form.full_name.data
|
|
||||||
user.username = form.username.data
|
user.username = form.username.data
|
||||||
user.update(api=api)
|
user.update(api=api)
|
||||||
flash("Profile updated", 'success')
|
flash("Profile updated", 'success')
|
||||||
|
@ -24,17 +24,16 @@ class UserLoginForm(Form):
|
|||||||
|
|
||||||
|
|
||||||
class UserProfileForm(Form):
|
class UserProfileForm(Form):
|
||||||
full_name = StringField('Full Name', validators=[DataRequired(), Length(
|
|
||||||
min=3, max=128, message="Min. 3 and max. 128 chars please")])
|
|
||||||
username = StringField('Username', validators=[DataRequired(), Length(
|
username = StringField('Username', validators=[DataRequired(), Length(
|
||||||
min=3, max=128, message="Min. 3, max. 128 chars please"), Regexp(
|
min=3, max=128, message="Min. 3, max. 128 chars please"), Regexp(
|
||||||
r'^[\w.@+-]+$', message="Please do not use spaces")])
|
r'^[\w.@+-]+$', message="Please do not use spaces")])
|
||||||
|
|
||||||
def __init__(self, csrf_enabled=False, *args, **kwargs):
|
def __init__(self, csrf_enabled=False, *args, **kwargs):
|
||||||
super(UserProfileForm, self).__init__(csrf_enabled=False, *args, **kwargs)
|
super().__init__(csrf_enabled=csrf_enabled, *args, **kwargs)
|
||||||
|
self.user = None
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
rv = Form.validate(self)
|
rv = super().validate()
|
||||||
if not rv:
|
if not rv:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -42,11 +41,11 @@ class UserProfileForm(Form):
|
|||||||
user = User.find(current_user.objectid, api=api)
|
user = User.find(current_user.objectid, api=api)
|
||||||
if user.username != self.username.data:
|
if user.username != self.username.data:
|
||||||
username = User.find_first(
|
username = User.find_first(
|
||||||
{'where': '{"username": "%s"}' % self.username.data},
|
{'where': {"username": self.username.data}},
|
||||||
api=api)
|
api=api)
|
||||||
|
|
||||||
if username:
|
if username:
|
||||||
self.username.errors.append('Sorry, username already exists!')
|
self.username.errors.append('Sorry, this username is already taken.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.user = user
|
self.user = user
|
||||||
|
@ -69,8 +69,7 @@ def oauth_callback(provider):
|
|||||||
pillar.auth.login_user(token['token'], load_from_db=True)
|
pillar.auth.login_user(token['token'], load_from_db=True)
|
||||||
|
|
||||||
if provider == 'blender-id' and current_user.is_authenticated:
|
if provider == 'blender-id' and current_user.is_authenticated:
|
||||||
# Check with the store for user roles. If the user has an active subscription, we apply
|
# Check with Blender ID to update certain user roles.
|
||||||
# the 'subscriber' role
|
|
||||||
update_subscription()
|
update_subscription()
|
||||||
|
|
||||||
next_after_login = session.pop('next_after_login', None)
|
next_after_login = session.pop('next_after_login', None)
|
||||||
|
@ -5,12 +5,11 @@ attrs==16.2.0
|
|||||||
algoliasearch==1.12.0
|
algoliasearch==1.12.0
|
||||||
bcrypt==3.1.3
|
bcrypt==3.1.3
|
||||||
blinker==1.4
|
blinker==1.4
|
||||||
bugsnag[flask]==3.1.1
|
|
||||||
bleach==1.4.3
|
bleach==1.4.3
|
||||||
celery[redis]==4.0.2
|
celery[redis]==4.0.2
|
||||||
CommonMark==0.7.2
|
CommonMark==0.7.2
|
||||||
Eve==0.7.3
|
Eve==0.7.3
|
||||||
Flask==0.12.2
|
Flask==0.12
|
||||||
Flask-Babel==0.11.2
|
Flask-Babel==0.11.2
|
||||||
Flask-Cache==0.13.1
|
Flask-Cache==0.13.1
|
||||||
Flask-Script==2.0.6
|
Flask-Script==2.0.6
|
||||||
@ -19,11 +18,12 @@ Flask-WTF==0.12
|
|||||||
gcloud==0.12.0
|
gcloud==0.12.0
|
||||||
google-apitools==0.4.11
|
google-apitools==0.4.11
|
||||||
httplib2==0.9.2
|
httplib2==0.9.2
|
||||||
MarkupSafe==1.0
|
MarkupSafe==0.23
|
||||||
ndg-httpsclient==0.4.0
|
ndg-httpsclient==0.4.0
|
||||||
Pillow==4.1.1
|
Pillow==4.1.1
|
||||||
python-dateutil==2.5.3
|
python-dateutil==2.5.3
|
||||||
rauth==0.7.3
|
rauth==0.7.3
|
||||||
|
raven[flask]==6.3.0
|
||||||
redis==2.10.5
|
redis==2.10.5
|
||||||
WebOb==1.5.0
|
WebOb==1.5.0
|
||||||
wheel==0.29.0
|
wheel==0.29.0
|
||||||
@ -55,4 +55,4 @@ simplejson==3.10.0
|
|||||||
six==1.10.0
|
six==1.10.0
|
||||||
vine==1.1.3
|
vine==1.1.3
|
||||||
WTForms==2.1
|
WTForms==2.1
|
||||||
Werkzeug==0.12.2
|
Werkzeug==0.11.15
|
||||||
|
@ -40,6 +40,11 @@ li(class="dropdown")
|
|||||||
title="View subscription info")
|
title="View subscription info")
|
||||||
i.pi-heart-filled
|
i.pi-heart-filled
|
||||||
span You have a free account.
|
span You have a free account.
|
||||||
|
| {% elif current_user.has_cap('can-renew-subscription') %}
|
||||||
|
a.navbar-item(target='_blank', href="/renew", title="Renew subscription")
|
||||||
|
i.pi-heart
|
||||||
|
span.info Your subscription is not active.
|
||||||
|
span.renew Click here to renew.
|
||||||
| {% else %}
|
| {% else %}
|
||||||
a.navbar-item(
|
a.navbar-item(
|
||||||
href="https://store.blender.org/product/membership/"
|
href="https://store.blender.org/product/membership/"
|
||||||
|
@ -2,7 +2,7 @@ doctype
|
|||||||
html(lang="en")
|
html(lang="en")
|
||||||
head
|
head
|
||||||
meta(charset="utf-8")
|
meta(charset="utf-8")
|
||||||
title Error
|
title {% block title %}Error{% endblock %}
|
||||||
meta(name="viewport", content="width=device-width, initial-scale=1.0")
|
meta(name="viewport", content="width=device-width, initial-scale=1.0")
|
||||||
|
|
||||||
link(href="{{ url_for('static_pillar', filename='assets/ico/favicon.png') }}", rel="shortcut icon")
|
link(href="{{ url_for('static_pillar', filename='assets/ico/favicon.png') }}", rel="shortcut icon")
|
||||||
@ -10,7 +10,7 @@ html(lang="en")
|
|||||||
link(href="{{ url_for('static_pillar', filename='assets/css/font-pillar.css') }}", rel="stylesheet")
|
link(href="{{ url_for('static_pillar', filename='assets/css/font-pillar.css') }}", rel="stylesheet")
|
||||||
link(href="{{ url_for('static_pillar', filename='assets/css/base.css') }}", rel="stylesheet")
|
link(href="{{ url_for('static_pillar', filename='assets/css/base.css') }}", rel="stylesheet")
|
||||||
link(href='//fonts.googleapis.com/css?family=Roboto:300,400', rel='stylesheet', type='text/css')
|
link(href='//fonts.googleapis.com/css?family=Roboto:300,400', rel='stylesheet', type='text/css')
|
||||||
|
| {% block head %}{% endblock %}
|
||||||
|
|
||||||
body.error
|
body.error
|
||||||
| {% block body %}{% endblock %}
|
| {% block body %}{% endblock %}
|
||||||
|
|
||||||
|
@ -74,6 +74,15 @@
|
|||||||
| Download
|
| Download
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
|
|
||||||
|
| {% elif current_user.has_cap('can-renew-subscription') %}
|
||||||
|
li.download
|
||||||
|
a.btn.btn-success(
|
||||||
|
title="Renew your subscription to download",
|
||||||
|
target="_blank",
|
||||||
|
href="/renew")
|
||||||
|
i.pi-heart
|
||||||
|
| Renew subscription to download
|
||||||
|
|
||||||
| {% elif current_user.is_authenticated %}
|
| {% elif current_user.is_authenticated %}
|
||||||
li.download
|
li.download
|
||||||
a.btn(
|
a.btn(
|
||||||
|
@ -27,8 +27,14 @@
|
|||||||
span
|
span
|
||||||
small Support Blender and get awesome stuff!
|
small Support Blender and get awesome stuff!
|
||||||
hr
|
hr
|
||||||
a.subscribe(href="{{ url_for('cloud.join') }}") <em>Subscribe</em>
|
| {% if current_user.has_cap('can-renew-subscription') %}
|
||||||
|
a.subscribe(href="/renew") You have a subscription, it just needs to be renewed. <em>Renew your subscription now!</em>
|
||||||
|
| {% else %}
|
||||||
|
a.subscribe(href="{{ url_for('cloud.join') }}") <em>Subscribe to Blender Cloud.</em>
|
||||||
|
| {% endif %}
|
||||||
|
| {% if current_user.is_anonymous %}
|
||||||
a(href="{{ url_for('users.login') }}") Already a subscriber? Log in
|
a(href="{{ url_for('users.login') }}") Already a subscriber? Log in
|
||||||
|
| {% endif %}
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,6 +49,9 @@
|
|||||||
| {% if current_user.has_cap('subscriber') %}
|
| {% if current_user.has_cap('subscriber') %}
|
||||||
i.pi-lock
|
i.pi-lock
|
||||||
| Only project members can comment.
|
| Only project members can comment.
|
||||||
|
| {% elif current_user.has_cap('can-renew-subscription') %}
|
||||||
|
i.pi-heart
|
||||||
|
a(href='/renew', target='_blank') Renew your subscription to join the conversation!
|
||||||
| {% else %}
|
| {% else %}
|
||||||
| Join the conversation! <a href="https://store.blender.org/product/membership/">Subscribe to Blender Cloud</a> now.
|
| Join the conversation! <a href="https://store.blender.org/product/membership/">Subscribe to Blender Cloud</a> now.
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
|
@ -40,12 +40,17 @@ meta(name="twitter:image", content="{{ url_for('static', filename='assets/img/ba
|
|||||||
| {% endif %}
|
| {% endif %}
|
||||||
|
|
||||||
| {% if current_user.has_cap('subscriber') %}
|
| {% if current_user.has_cap('subscriber') %}
|
||||||
li.create(
|
li.create#project-create(
|
||||||
data-url="{{ url_for('projects.create') }}")
|
data-url="{{ url_for('projects.create') }}")
|
||||||
a.btn.btn-success#project-create(
|
a.btn.btn-success(
|
||||||
href="{{ url_for('projects.create') }}")
|
href="{{ url_for('projects.create') }}")
|
||||||
i.pi-plus
|
i.pi-plus
|
||||||
| Create Project
|
| Create Project
|
||||||
|
| {% elif current_user.has_cap('can-renew-subscription') %}
|
||||||
|
li.create
|
||||||
|
a.btn(href="/renew", target="_blank")
|
||||||
|
i.pi-heart
|
||||||
|
| Renew subscription to create a project
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
|
|
||||||
nav.nav-tabs__tab.active#own_projects
|
nav.nav-tabs__tab.active#own_projects
|
||||||
@ -83,6 +88,13 @@ meta(name="twitter:image", content="{{ url_for('static', filename='assets/img/ba
|
|||||||
.projects__list-details
|
.projects__list-details
|
||||||
a.title(href="{{ url_for('projects.create') }}")
|
a.title(href="{{ url_for('projects.create') }}")
|
||||||
| Create a project to get started!
|
| Create a project to get started!
|
||||||
|
| {% elif current_user.has_cap('can-renew-subscription') %}
|
||||||
|
li.projects__list-item(data-url="https://store.blender.org/renew-my-subscription.php")
|
||||||
|
a.projects__list-thumbnail
|
||||||
|
i.pi-plus
|
||||||
|
.projects__list-details
|
||||||
|
a.title(href="https://store.blender.org/renew-my-subscription.php")
|
||||||
|
| Renew your Blender Cloud subscription to create your own projects!
|
||||||
| {% else %}
|
| {% else %}
|
||||||
li.projects__list-item(data-url="/join")
|
li.projects__list-item(data-url="/join")
|
||||||
a.projects__list-thumbnail
|
a.projects__list-thumbnail
|
||||||
@ -222,7 +234,7 @@ script.
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create project
|
// Create project
|
||||||
$nav_tabs_list.find('li.create').on('click', function(e){
|
$('#project-create').on('click', function(e){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
$(this).addClass('disabled');
|
$(this).addClass('disabled');
|
||||||
|
@ -72,7 +72,8 @@
|
|||||||
| none
|
| none
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
|
|
||||||
a#button-cancel.btn.btn-default(href="#", data-user-id='{{user.user_id}}') Cancel
|
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(
|
input#submit_edit_user.btn.btn-default(
|
||||||
data-user-id="{{user.user_id}}",
|
data-user-id="{{user.user_id}}",
|
||||||
@ -101,10 +102,18 @@ script(type="text/javascript").
|
|||||||
//- $("#user-edit-form").submit();
|
//- $("#user-edit-form").submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#button-cancel').click(function(e){
|
|
||||||
$('#user-container').html('')
|
|
||||||
});
|
|
||||||
|
|
||||||
new Clipboard('.copy-to-clipboard');
|
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 %}
|
| {% endblock %}
|
||||||
|
@ -11,13 +11,6 @@
|
|||||||
.settings-form
|
.settings-form
|
||||||
form#settings-form(method='POST', action="{{url_for('settings.profile')}}")
|
form#settings-form(method='POST', action="{{url_for('settings.profile')}}")
|
||||||
.left
|
.left
|
||||||
.form-group
|
|
||||||
| {{ form.full_name.label }}
|
|
||||||
| {{ form.full_name(size=20, class='form-control') }}
|
|
||||||
| {% if form.full_name.errors %}
|
|
||||||
| {% for error in form.full_name.errors %}{{ error|e }}{% endfor %}
|
|
||||||
| {% endif %}
|
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
| {{ form.username.label }}
|
| {{ form.username.label }}
|
||||||
| {{ form.username(size=20, class='form-control') }}
|
| {{ form.username(size=20, class='form-control') }}
|
||||||
@ -25,8 +18,15 @@
|
|||||||
| {% for error in form.username.errors %}{{ error|e }}{% endfor %}
|
| {% for error in form.username.errors %}{{ error|e }}{% endfor %}
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
|
|
||||||
.form-group.settings-password
|
.form-group
|
||||||
| {{ _("Change your password at") }} #[a(href="https://blender.org/id/change") Blender ID]
|
label {{ _("Full name") }}
|
||||||
|
p {{ current_user.full_name }}
|
||||||
|
.form-group
|
||||||
|
label {{ _("E-mail") }}
|
||||||
|
p {{ current_user.email }}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
| {{ _("Change your full name, email, and password at") }} #[a(href="https://www.blender.org/id/settings/profile",target='_blank') Blender ID].
|
||||||
|
|
||||||
.right
|
.right
|
||||||
.settings-avatar
|
.settings-avatar
|
||||||
|
@ -677,7 +677,7 @@ class RequireRolesTest(AbstractPillarTest):
|
|||||||
self.assertFalse(called[0])
|
self.assertFalse(called[0])
|
||||||
|
|
||||||
with self.app.test_request_context():
|
with self.app.test_request_context():
|
||||||
self.login_api_as(ObjectId(24 * 'a'), ['admin'])
|
self.login_api_as(ObjectId(24 * 'a'), ['demo'])
|
||||||
call_me()
|
call_me()
|
||||||
self.assertTrue(called[0])
|
self.assertTrue(called[0])
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ class ProjectCreationTest(AbstractProjectTest):
|
|||||||
def test_project_creation_access_admin(self):
|
def test_project_creation_access_admin(self):
|
||||||
"""Admin-created projects should be public"""
|
"""Admin-created projects should be public"""
|
||||||
|
|
||||||
proj = self._create_user_and_project(roles={'admin'})
|
proj = self._create_user_and_project(roles={'admin', 'demo'})
|
||||||
self.assertEqual(['GET'], proj['permissions']['world'])
|
self.assertEqual(['GET'], proj['permissions']['world'])
|
||||||
|
|
||||||
def test_project_creation_access_subscriber(self):
|
def test_project_creation_access_subscriber(self):
|
||||||
@ -311,13 +311,14 @@ class ProjectEditTest(AbstractProjectTest):
|
|||||||
|
|
||||||
def test_delete_by_admin(self):
|
def test_delete_by_admin(self):
|
||||||
# Create public test project.
|
# Create public test project.
|
||||||
project_info = self._create_user_and_project(['admin'])
|
project_info = self._create_user_and_project(['admin', 'demo'])
|
||||||
project_id = project_info['_id']
|
project_id = project_info['_id']
|
||||||
project_url = '/api/projects/%s' % project_id
|
project_url = '/api/projects/%s' % project_id
|
||||||
|
|
||||||
# Create admin user that doesn't own the project, to check that
|
# Create admin user that doesn't own the project, to check that
|
||||||
# non-owner admins can delete projects too.
|
# non-owner admins can delete projects too.
|
||||||
self._create_user_with_token(['admin'], 'admin-token', user_id='cafef00dbeefcafef00dbeef')
|
self._create_user_with_token(['admin'], 'admin-token',
|
||||||
|
user_id='cafef00dbeefcafef00dbeef')
|
||||||
|
|
||||||
# Admin user should be able to DELETE.
|
# Admin user should be able to DELETE.
|
||||||
resp = self.client.delete(project_url,
|
resp = self.client.delete(project_url,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import typing
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import responses
|
import responses
|
||||||
@ -17,22 +18,16 @@ class RoleUpdatingTest(AbstractPillarTest):
|
|||||||
self.create_standard_groups()
|
self.create_standard_groups()
|
||||||
|
|
||||||
def _setup_testcase(self, mocked_fetch_blenderid_user, *,
|
def _setup_testcase(self, mocked_fetch_blenderid_user, *,
|
||||||
store_says_cloud_access: bool,
|
bid_roles: typing.Set[str]):
|
||||||
bid_says_cloud_demo: bool):
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
|
# The Store API endpoint should not be called upon any more.
|
||||||
url = '%s?blenderid=%s' % (self.app.config['EXTERNAL_SUBSCRIPTIONS_MANAGEMENT_SERVER'],
|
url = '%s?blenderid=%s' % (self.app.config['EXTERNAL_SUBSCRIPTIONS_MANAGEMENT_SERVER'],
|
||||||
urllib.parse.quote(TEST_EMAIL_ADDRESS))
|
urllib.parse.quote(TEST_EMAIL_ADDRESS))
|
||||||
responses.add('GET', url,
|
responses.add('GET', url,
|
||||||
json={'shop_id': 58432,
|
status=500,
|
||||||
'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)
|
match_querystring=True)
|
||||||
|
|
||||||
self.mock_blenderid_validate_happy()
|
self.mock_blenderid_validate_happy()
|
||||||
mocked_fetch_blenderid_user.return_value = {
|
mocked_fetch_blenderid_user.return_value = {
|
||||||
'email': TEST_EMAIL_ADDRESS,
|
'email': TEST_EMAIL_ADDRESS,
|
||||||
@ -45,27 +40,25 @@ class RoleUpdatingTest(AbstractPillarTest):
|
|||||||
'network_member': True
|
'network_member': True
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if bid_says_cloud_demo:
|
for role in bid_roles:
|
||||||
mocked_fetch_blenderid_user.return_value['roles']['cloud_demo'] = True
|
mocked_fetch_blenderid_user.return_value['roles'][role] = True
|
||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
@mock.patch('pillar.api.blender_id.fetch_blenderid_user')
|
@mock.patch('pillar.api.blender_id.fetch_blenderid_user')
|
||||||
def test_store_api_role_grant_subscriber(self, mocked_fetch_blenderid_user):
|
def test_store_api_role_grant_subscriber(self, mocked_fetch_blenderid_user):
|
||||||
self._setup_testcase(mocked_fetch_blenderid_user,
|
self._setup_testcase(mocked_fetch_blenderid_user,
|
||||||
store_says_cloud_access=True,
|
bid_roles={'cloud_subscriber', 'cloud_has_subscription'})
|
||||||
bid_says_cloud_demo=False)
|
|
||||||
|
|
||||||
self.get('/api/bcloud/update-subscription', auth_token='my-happy-token',
|
self.get('/api/bcloud/update-subscription', auth_token='my-happy-token',
|
||||||
expected_status=204)
|
expected_status=204)
|
||||||
user_info = self.get('/api/users/me', auth_token='my-happy-token').json()
|
user_info = self.get('/api/users/me', auth_token='my-happy-token').json()
|
||||||
self.assertEqual(['subscriber'], user_info['roles'])
|
self.assertEqual({'subscriber', 'has_subscription'}, set(user_info['roles']))
|
||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
@mock.patch('pillar.api.blender_id.fetch_blenderid_user')
|
@mock.patch('pillar.api.blender_id.fetch_blenderid_user')
|
||||||
def test_store_api_role_revoke_subscriber(self, mocked_fetch_blenderid_user):
|
def test_store_api_role_revoke_subscriber(self, mocked_fetch_blenderid_user):
|
||||||
self._setup_testcase(mocked_fetch_blenderid_user,
|
self._setup_testcase(mocked_fetch_blenderid_user,
|
||||||
store_says_cloud_access=False,
|
bid_roles={'conference_speaker'})
|
||||||
bid_says_cloud_demo=False)
|
|
||||||
|
|
||||||
# Make sure this user is currently known as a subcriber.
|
# Make sure this user is currently known as a subcriber.
|
||||||
self.create_user(roles={'subscriber'}, token='my-happy-token')
|
self.create_user(roles={'subscriber'}, token='my-happy-token')
|
||||||
@ -82,8 +75,7 @@ class RoleUpdatingTest(AbstractPillarTest):
|
|||||||
@mock.patch('pillar.api.blender_id.fetch_blenderid_user')
|
@mock.patch('pillar.api.blender_id.fetch_blenderid_user')
|
||||||
def test_bid_api_grant_demo(self, mocked_fetch_blenderid_user):
|
def test_bid_api_grant_demo(self, mocked_fetch_blenderid_user):
|
||||||
self._setup_testcase(mocked_fetch_blenderid_user,
|
self._setup_testcase(mocked_fetch_blenderid_user,
|
||||||
store_says_cloud_access=False,
|
bid_roles={'cloud_demo'})
|
||||||
bid_says_cloud_demo=True)
|
|
||||||
|
|
||||||
self.get('/api/bcloud/update-subscription', auth_token='my-happy-token',
|
self.get('/api/bcloud/update-subscription', auth_token='my-happy-token',
|
||||||
expected_status=204)
|
expected_status=204)
|
||||||
@ -93,10 +85,9 @@ class RoleUpdatingTest(AbstractPillarTest):
|
|||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
@mock.patch('pillar.api.blender_id.fetch_blenderid_user')
|
@mock.patch('pillar.api.blender_id.fetch_blenderid_user')
|
||||||
def test_bid_api_role_revoke_subscriber(self, mocked_fetch_blenderid_user):
|
def test_bid_api_role_revoke_demo(self, mocked_fetch_blenderid_user):
|
||||||
self._setup_testcase(mocked_fetch_blenderid_user,
|
self._setup_testcase(mocked_fetch_blenderid_user,
|
||||||
store_says_cloud_access=False,
|
bid_roles={'conference_speaker'})
|
||||||
bid_says_cloud_demo=False)
|
|
||||||
|
|
||||||
# Make sure this user is currently known as demo user.
|
# Make sure this user is currently known as demo user.
|
||||||
self.create_user(roles={'demo'}, token='my-happy-token')
|
self.create_user(roles={'demo'}, token='my-happy-token')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user