def process_posts(community_name): """Manually trigger pre-update hooks.""" from flask import g from pillar.auth import UserClass from dillo.api.posts.hooks import process_picture_oembed, before_replacing_post from dillo.setup import _get_project project = _get_project(community_name) nodes_collection = current_app.db()['nodes'] user_collection = current_app.db()['users'] nc = nodes_collection.find({ 'node_type': 'dillo_post', 'properties.status': 'published', 'project': project['_id'], }) for n in nc: # Log in as doc user user_doc = user_collection.find_one({'_id': n['user']}) u = UserClass.construct(user_doc['_id'], user_doc) g.current_user = u n_id = n['_id'] print(f'Processing node {n_id}') process_picture_oembed(n, n) before_replacing_post(n, n) nodes_collection.find_one_and_replace({'_id': n_id}, n)
def force_cli_user(): """Sets g.current_user to the CLI_USER object. This is used as a marker to avoid authorization checks and just allow everything. """ global CLI_USER from pillar.auth import UserClass if CLI_USER is ...: CLI_USER = UserClass.construct( 'CLI', { '_id': 'CLI', 'groups': [], 'roles': {'admin'}, 'email': 'local@nowhere', 'username': '******', }) log.info('CONSTRUCTED CLI USER %s of type %s', id(CLI_USER), id(type(CLI_USER))) log.info( 'Logging in as CLI_USER (%s) of type %s, circumventing authentication.', id(CLI_USER), id(type(CLI_USER))) g.current_user = CLI_USER
def test_current_user_logged_in(self): self.enter_app_context() from flask import g from pillar.auth import UserClass from pillar.api.utils.authentication import current_user with self.app.test_request_context(): g.current_user = UserClass.construct('the token', ctd.EXAMPLE_USER) user = current_user() self.assertIs(g.current_user, user)
def do_user(idx, user): nonlocal count_skipped, count_processed log.info('Processing %i/%i %s', idx + 1, count_users, user['email']) # Get the Requests session for this thread. try: sess = sessions.session except AttributeError: sess = sessions.session = requests.Session() # Get the info from Blender ID bid_user_id = blender_id.get_user_blenderid(user) if not bid_user_id: with lock: count_skipped += 1 return url = urljoin(api_url, bid_user_id) resp = sess.get(url, headers={'Authorization': f'Bearer {api_token}'}) if resp.status_code == 404: log.info('User %s with Blender ID %s not found, skipping', user['email'], bid_user_id) with lock: count_skipped += 1 return if resp.status_code != 200: log.error('Unable to reach Blender ID (code %d), aborting', resp.status_code) with lock: count_skipped += 1 return bid_user = resp.json() if not bid_user: log.error('Unable to parse response for user %s, aborting', user['email']) with lock: count_skipped += 1 return # Actually update the user, and do it thread-safe just to be sure. with real_current_app.app_context(): local_user = UserClass.construct('', user) with lock: do_update_subscription(local_user, bid_user) count_processed += 1
def create_user_object(self, user_id=ObjectId(), roles=frozenset(), group_ids=None): """Creates a pillar.auth.UserClass object. :rtype: pillar.auth.UserClass """ from pillar.auth import UserClass db_user = copy.deepcopy(ctd.EXAMPLE_USER) db_user['_id'] = user_id db_user['roles'] = list(roles) if roles is not None else None if group_ids is not None: db_user['groups'] = list(group_ids) return UserClass.construct('', db_user)
def setup_db(admin_email): """Setup the database - Create admin, subscriber and demo Group collection - Create admin user (must use valid blender-id credentials) - Create one project """ # Create default groups groups_list = [] for group in ['admin', 'subscriber', 'demo']: g = {'name': group} g = current_app.post_internal('groups', g) groups_list.append(g[0]['_id']) print("Creating group {0}".format(group)) # Create admin user user = { 'username': admin_email, 'groups': groups_list, 'roles': ['admin', 'subscriber', 'demo'], 'settings': { 'email_communications': 1 }, 'auth': [], 'full_name': admin_email, 'email': admin_email } result, _, _, status = current_app.post_internal('users', user) if status != 201: raise SystemExit('Error creating user {}: {}'.format( admin_email, result)) user.update(result) print("Created user {0}".format(user['_id'])) # Create a default project by faking a POST request. with current_app.test_request_context( data={'project_name': 'Default Project'}): from flask import g from pillar.auth import UserClass from pillar.api.projects import routes as proj_routes g.current_user = UserClass.construct('', user) proj_routes.create_project(overrides={ 'url': 'default-project', 'is_private': False })
def user_is_flamenco_user(self, user_id: bson.ObjectId) -> bool: """Returns True iff the user has Flamenco User role.""" from pillar import current_app from pillar.auth import UserClass assert isinstance(user_id, bson.ObjectId) # TODO: move role/cap checking code to Pillar. users_coll = current_app.db('users') db_user = users_coll.find_one({'_id': user_id}, {'roles': 1}) if not db_user: self._log.debug('user_is_flamenco_user: User %s not found', user_id) return False user = UserClass.construct('', db_user) return user.has_cap('flamenco-use')
def welcome_new_user(user_doc: dict): """Sends a welcome email to a new user.""" user_email = user_doc.get('email') if not user_email: log.warning('user %s has no email address', user_doc.get('_id', '-no-id-')) return # Only send mail to new users when they actually are subscribers. user = UserClass.construct('', user_doc) if not (user.has_cap('subscriber') or user.has_cap('can-renew-subscription')): log.debug( 'user %s is new, but not a subscriber, so no email for them.', user_email) return email.queue_welcome_mail(user)
def users_edit(user_id): from pillar.auth import UserClass if not current_user.has_cap('admin'): return abort(403) api = system_util.pillar_api() try: user = User.find(user_id, api=api) except sdk_exceptions.ResourceNotFound: log.warning('Non-existing user %r requested.', user_id) raise wz_exceptions.NotFound('Non-existing user %r requested.' % user_id) form = forms.UserEditForm() if form.validate_on_submit(): _users_edit(form, user, api) else: form.roles.data = user.roles form.email.data = user.email user_ob = UserClass.construct('', db_user=user.to_dict()) return render_template('users/edit_embed.html', user=user_ob, form=form)
def _get_db_user(self, proj, repo_id, user_id) -> dict: """Returns the user from the database. Raises a ValueError if the user is not allowed to use svn. """ from pillar.auth import UserClass user_oid = str2id(user_id) db_user = current_app.db('users').find_one({'_id': user_oid}) if not db_user: self._log.warning('user %s not found, not modifying access to repo %s of project %s', user_id, repo_id, proj['_id']) raise ValueError('User not found') thatuser = UserClass.construct('', db_user) if not thatuser.has_cap('svn-use'): self._log.warning('user %s has no svn-use cap, not modifying access to repo %s of' ' project %s', user_id, repo_id, proj['_id']) raise UnavailableForLegalReasons('User is not allowed to use Subversion') return db_user
def validate_this_token(token, oauth_subclient=None): """Validates a given token, and sets g.current_user. :returns: the user in MongoDB, or None if not a valid token. :rtype: dict """ from pillar.auth import UserClass, AnonymousUser, user_authenticated g.current_user = None _delete_expired_tokens() # Check the users to see if there is one with this Blender ID token. db_token = find_token(token, oauth_subclient) if not db_token: log.debug('Token %r not found in our local database.', token) # If no valid token is found in our local database, we issue a new # request to the Blender ID server to verify the validity of the token # passed via the HTTP header. We will get basic user info if the user # is authorized, and we will store the token in our local database. from pillar.api import blender_id db_user, status = blender_id.validate_create_user( '', token, oauth_subclient) else: # log.debug("User is already in our database and token hasn't expired yet.") users = current_app.data.driver.db['users'] db_user = users.find_one(db_token['user']) if db_user is None: log.debug('Validation failed, user not logged in') g.current_user = AnonymousUser() return None g.current_user = UserClass.construct(token, db_user) user_authenticated.send(g.current_user) return db_user
def after_inserting_project(project, db_user): from pillar.auth import UserClass project_id = project['_id'] user_id = db_user['_id'] # Create a project-specific admin group (with name matching the project id) result, _, _, status = current_app.post_internal('groups', {'name': str(project_id)}) if status != 201: log.error('Unable to create admin group for new project %s: %s', project_id, result) return abort_with_error(status) admin_group_id = result['_id'] log.debug('Created admin group %s for project %s', admin_group_id, project_id) # Assign the current user to the group db_user.setdefault('groups', []).append(admin_group_id) result, _, _, status = current_app.patch_internal( 'users', {'groups': db_user['groups']}, _id=user_id) if status != 200: log.error( 'Unable to add user %s as member of admin group %s for new project %s: %s', user_id, admin_group_id, project_id, result) return abort_with_error(status) log.debug('Made user %s member of group %s', user_id, admin_group_id) # Assign the group to the project with admin rights owner_user = UserClass.construct('', db_user) is_admin = authorization.is_admin(owner_user) world_permissions = ['GET'] if is_admin else [] permissions = { 'world': world_permissions, 'users': [], 'groups': [ { 'group': admin_group_id, 'methods': DEFAULT_ADMIN_GROUP_PERMISSIONS[:] }, ] } def with_permissions(node_type): copied = copy.deepcopy(node_type) copied['permissions'] = permissions return copied # Assign permissions to the project itself, as well as to the node_types project['permissions'] = permissions project['node_types'] = [ with_permissions(node_type_group), with_permissions(node_type_asset), with_permissions(node_type_comment), with_permissions(node_type_texture), with_permissions(node_type_group_texture), ] # Allow admin users to use whatever url they want. if not is_admin or not project.get('url'): if project.get('category', '') == 'home': project['url'] = 'home' else: project['url'] = "p-{!s}".format(project_id) # Initialize storage using the default specified in STORAGE_BACKEND default_storage_backend(str(project_id)) # Commit the changes directly to the MongoDB; a PUT is not allowed yet, # as the project doesn't have a valid permission structure. projects_collection = current_app.data.driver.db['projects'] result = projects_collection.update_one( {'_id': project_id}, {'$set': remove_private_keys(project)}) if result.matched_count != 1: log.error('Unable to update project %s: %s', project_id, result.raw_result) abort_with_error(500)
def insert_or_fetch_user(wh_payload: dict) -> typing.Optional[dict]: """Fetch the user from the DB or create it. Only creates it if the webhook payload indicates they could actually use Blender Cloud (i.e. demo or subscriber). This prevents us from creating Cloud accounts for Blender Network users. :returns the user document, or None when not created. """ users_coll = current_app.db('users') my_log = log.getChild('insert_or_fetch_user') bid_str = str(wh_payload['id']) email = wh_payload['email'] # Find the user by their Blender ID, or any of their email addresses. # We use one query to find all matching users. This is done as a # consistency check; if more than one user is returned, we know the # database is inconsistent with Blender ID and can emit a warning # about this. query = {'$or': [ {'auth.provider': 'blender-id', 'auth.user_id': bid_str}, {'email': {'$in': [wh_payload['old_email'], email]}}, ]} db_users = list(users_coll.find(query)) user_count = len(db_users) if user_count > 1: # Now we have to pay the price for finding users in one query; we # have to prioritise them and return the one we think is most reliable. calc_score = functools.partial(score, wh_payload) best_score = max(db_users, key=calc_score) my_log.error('%d users found for query %s, picking user %s (%s)', user_count, query, best_score['_id'], best_score['email']) return best_score if user_count: db_user = db_users[0] my_log.debug('found user %s', db_user['email']) return db_user # Pretend to create the user, so that we can inspect the resulting # capabilities. This is more future-proof than looking at the list # of roles in the webhook payload. username = make_unique_username(email) user_doc = create_new_user_document(email, bid_str, username, provider='blender-id', full_name=wh_payload['full_name']) # Figure out the user's eventual roles. These aren't stored in the document yet, # because that's handled by the badger service. eventual_roles = [subscription.ROLES_BID_TO_PILLAR[r] for r in wh_payload.get('roles', []) if r in subscription.ROLES_BID_TO_PILLAR] user_ob = UserClass.construct('', user_doc) user_ob.roles = eventual_roles user_ob.collect_capabilities() create = (user_ob.has_cap('subscriber') or user_ob.has_cap('can-renew-subscription') or current_app.org_manager.user_is_unknown_member(email)) if not create: my_log.info('Received update for unknown user %r without Cloud access (caps=%s)', wh_payload['old_email'], user_ob.capabilities) return None # Actually create the user in the database. r, _, _, status = current_app.post_internal('users', user_doc) if status != 201: my_log.error('unable to create user %s: : %r %r', email, status, r) raise wz_exceptions.InternalServerError('unable to create user') user_doc.update(r) my_log.info('created user %r = %s to allow immediate Cloud access', email, user_doc['_id']) return user_doc
def user_modified(): """Update the local user based on the info from Blender ID. If the payload indicates the user has access to Blender Cloud (or at least a renewable subscription), create the user if not already in our DB. The payload we expect is a dictionary like: {'id': 12345, # the user's ID in Blender ID 'old_email': '*****@*****.**', 'full_name': 'Harry', 'email': 'new@example'com, 'avatar_changed': True, 'roles': ['role1', 'role2', …]} """ my_log = log.getChild('user_modified') my_log.debug('Received request from %s', request.remote_addr) hmac_secret = current_app.config['BLENDER_ID_WEBHOOK_USER_CHANGED_SECRET'] payload = webhook_payload(hmac_secret) my_log.info('payload: %s', payload) # Update the user db_user = insert_or_fetch_user(payload) if not db_user: my_log.info('Received update for unknown user %r', payload['old_email']) return '', 204 # Use direct database updates to change the email and full name. # Also updates the db_user dict so that local_user below will have # the updated information. updates = {} if db_user['email'] != payload['email']: my_log.info('User changed email from %s to %s', payload['old_email'], payload['email']) updates['email'] = payload['email'] db_user['email'] = payload['email'] if db_user['full_name'] != payload['full_name']: my_log.info('User changed full name from %r to %r', db_user['full_name'], payload['full_name']) if payload['full_name']: updates['full_name'] = payload['full_name'] else: # Fall back to the username when the full name was erased. updates['full_name'] = db_user['username'] db_user['full_name'] = updates['full_name'] if payload.get('avatar_changed'): import pillar.celery.avatar my_log.info('User %s changed avatar, scheduling download', db_user['_id']) pillar.celery.avatar.sync_avatar_for_user.delay(str(db_user['_id'])) if updates: users_coll = current_app.db('users') update_res = users_coll.update_one({'_id': db_user['_id']}, {'$set': updates}) if update_res.matched_count != 1: my_log.error('Unable to find user %s to update, even though ' 'we found them by email address %s', db_user['_id'], payload['old_email']) # Defer to Pillar to do the role updates. local_user = UserClass.construct('', db_user) subscription.do_update_subscription(local_user, payload) return '', 204