Beispiel #1
0
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)
Beispiel #2
0
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
Beispiel #3
0
    def test_validate_token__force_not_logged_in(self):
        from pillar.api.utils import authentication as auth
        from pillar.auth import UserClass, current_user

        with self.app.test_request_context():
            from flask import g
            g.current_user = UserClass('12345')
            self.assertTrue(g.current_user.is_authenticated)

            self.assertFalse(auth.validate_token(force=True))
            self.assertFalse(g.current_user.is_authenticated)
Beispiel #4
0
    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)
Beispiel #5
0
    def test_validate_token__force_logged_in(self):
        from pillar.api.utils import authentication as auth
        from pillar.auth import UserClass

        self.mock_blenderid_validate_happy()
        with self.app.test_request_context(
                headers={'Authorization': self.make_header('knowntoken')}):
            from flask import g
            g.current_user = UserClass('12345')
            self.assertTrue(g.current_user.is_authenticated)

            self.assertTrue(auth.validate_token(force=True))
            self.assertTrue(g.current_user.is_authenticated)
Beispiel #6
0
    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
Beispiel #7
0
    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)
Beispiel #8
0
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
        })
Beispiel #9
0
    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')
Beispiel #10
0
    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')
Beispiel #11
0
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)
Beispiel #12
0
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
Beispiel #13
0
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)
Beispiel #14
0
    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
Beispiel #15
0
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
Beispiel #16
0
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)
Beispiel #17
0
def user_subscription_changed(user: UserClass, *, grant_roles: set,
                              revoke_roles: set):
    if user.has_cap('subscriber') and 'has_subscription' in grant_roles:
        log.info('user %s just got a new subscription', user.email)
        queue_welcome_mail(user)
Beispiel #18
0
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