Ejemplo n.º 1
0
    def patch_change_ownership(self, manager_id: bson.ObjectId, patch: dict):
        """Shares or un-shares the Manager with a user."""

        man_man = current_flamenco.manager_manager
        if not man_man.user_is_owner(mngr_doc_id=manager_id):
            log.warning('User %s uses PATCH to (un)share manager %s, '
                        'but user is not owner of that Manager. Request denied.',
                        current_user_id(), manager_id)
            raise wz_exceptions.Forbidden()

        action = patch.get('action', '')
        try:
            action = man_man.ShareAction[action]
        except KeyError:
            raise wz_exceptions.BadRequest(f'Unknown action {action!r}')

        subject_uid = str2id(patch.get('user', ''))
        if action == man_man.ShareAction.share and subject_uid == current_user_id():
            log.warning('%s tries to %s Manager %s with itself',
                        current_user_id(), action, manager_id)
            raise wz_exceptions.BadRequest(f'Cannot share a Manager with yourself')

        if action == man_man.ShareAction.share and \
                not current_flamenco.auth.user_is_flamenco_user(subject_uid):
            log.warning('%s Manager %s on behalf of user %s, but subject user %s '
                        'is not Flamenco user', action, manager_id, current_user_id(),
                        subject_uid)
            raise wz_exceptions.Forbidden(f'User {subject_uid} is not allowed to use Flamenco')

        try:
            man_man.share_unshare_manager(manager_id, action, subject_uid)
        except ValueError as ex:
            raise wz_exceptions.BadRequest(str(ex))
Ejemplo n.º 2
0
    def patch_rna_overrides(self, job_id: bson.ObjectId, patch: dict):
        """Update the RNA overrides of this render job, and re-queue dependent tasks.

        Note that once a job has RNA overrides, the RNA overrides task cannot
        be deleted. If such task deletion were possible, it would still not
        delete the RNA override file and effectively keep the old overrides in
        place. Having an empty overrides file is better.
        """
        self.assert_job_access(job_id)

        rna_overrides = patch.get('rna_overrides') or []
        if not all(isinstance(override, str) for override in rna_overrides):
            log.info('User %s wants to PATCH job %s to set RNA overrides, but not all '
                     'overrides are strings', current_user_id(), job_id)
            raise wz_exceptions.BadRequest(f'"rna_overrides" should be a list of strings,'
                                           f' not {rna_overrides!r}')

        result = rna_overrides_mod.validate_rna_overrides(rna_overrides)
        if result:
            msg, line_num = result
            log.info('User %s tries PATCH to update RNA overrides of job %s but has '
                     'error %r in override %d',
                     current_user_id(), job_id, msg, line_num)

            return jsonify({
                'validation_error': {
                    'message': msg,
                    'line_num': line_num,
                }
            }, status=422)

        log.info('User %s uses PATCH to update RNA overrides of job %s to %d overrides',
                 current_user_id(), job_id, len(rna_overrides))
        current_flamenco.job_manager.api_update_rna_overrides(job_id, rna_overrides)
Ejemplo n.º 3
0
def enrich(response):
    response['_is_own'] = response['user'] == current_user_id()
    response['_current_user_rating'] = None  # tri-state boolean
    response['_rating'] = response['properties']['rating_positive'] - response[
        'properties']['rating_negative']
    if current_user_id():
        if 'ratings' not in response['properties']:
            return
        for rating in response['properties']['ratings'] or ():
            if rating['user'] != current_user_id():
                continue
            response['_current_user_rating'] = rating['is_positive']
Ejemplo n.º 4
0
    def _assign_or_remove_project(self, manager_id: bson.ObjectId, patch: dict,
                                  action: str):
        """Assigns a manager to a project or removes it.

        The calling user must be owner of the manager (always)
        and member of the project (if assigning).
        """

        from pillar.api.projects.utils import user_rights_in_project

        from flamenco import current_flamenco

        try:
            project_strid = patch['project']
        except KeyError:
            log.warning('User %s sent invalid PATCH %r for manager %s.',
                        current_user_id(), patch, manager_id)
            raise wz_exceptions.BadRequest('Missing key "project"')

        project_id = str2id(project_strid)

        if not current_flamenco.manager_manager.user_is_owner(
                mngr_doc_id=manager_id):
            log.warning(
                'User %s uses PATCH to %s manager %s to/from project %s, '
                'but user is not owner of that Manager. Request denied.',
                current_user_id(), action, manager_id, project_id)
            raise wz_exceptions.Forbidden()

        # Removing from a project doesn't require project membership.
        if action != 'remove':
            methods = user_rights_in_project(project_id)
            if 'PUT' not in methods:
                log.warning(
                    'User %s uses PATCH to %s manager %s to/from project %s, '
                    'but only has %s rights on project. Request denied.',
                    current_user_id(), action, manager_id, project_id,
                    ', '.join(methods))
                raise wz_exceptions.Forbidden()

        log.info('User %s uses PATCH to %s manager %s to/from project %s',
                 current_user_id(), action, manager_id, project_id)

        ok = current_flamenco.manager_manager.api_assign_to_project(
            manager_id, project_id, action)
        if not ok:
            # Manager Manager will have already logged the cause.
            raise wz_exceptions.InternalServerError()
Ejemplo n.º 5
0
    def patch(self, object_id: str):
        from flask import request
        import werkzeug.exceptions as wz_exceptions
        from pillar.api.utils import str2id, authentication

        # Parse the request
        real_object_id = str2id(object_id)
        patch = request.get_json()
        if not patch:
            self.log.info('Bad PATCH request, did not contain JSON')
            raise wz_exceptions.BadRequest('Patch must contain JSON')

        try:
            patch_op = patch['op']
        except KeyError:
            self.log.info("Bad PATCH request, did not contain 'op' key")
            raise wz_exceptions.BadRequest("PATCH should contain 'op' key to denote operation.")

        log.debug('User %s wants to PATCH "%s" %s %s',
                  authentication.current_user_id(), patch_op, self.item_name, real_object_id)

        # Find the PATCH handler for the operation.
        try:
            handler = self.patch_handlers[patch_op]
        except KeyError:
            log.warning('No %s PATCH handler for operation %r', self.item_name, patch_op)
            raise wz_exceptions.BadRequest('Operation %r not supported' % patch_op)

        # Let the PATCH handler do its thing.
        response = handler(real_object_id, patch)
        if response is None:
            return '', 204
        return response
Ejemplo n.º 6
0
def assert_is_valid_patch(node_id, patch):
    """Raises an exception when the patch isn't valid."""

    try:
        op = patch['op']
    except KeyError:
        raise wz_exceptions.BadRequest("PATCH should have a key 'op' indicating the operation.")

    if op not in VALID_COMMENT_OPERATIONS:
        raise wz_exceptions.BadRequest('Operation should be one of %s',
                                       ', '.join(VALID_COMMENT_OPERATIONS))

    if op not in COMMENT_VOTING_OPS:
        # We can't check here, we need the node owner for that.
        return

    # See whether the user is allowed to patch
    if authorization.user_matches_roles(current_app.config['ROLES_FOR_COMMENT_VOTING']):
        log.debug('User is allowed to upvote/downvote comment')
        return

    # Access denied.
    log.info('User %s wants to PATCH comment node %s, but is not allowed.',
             authentication.current_user_id(), node_id)
    raise wz_exceptions.Forbidden()
Ejemplo n.º 7
0
    def patch_set_task_status(self, task_id: bson.ObjectId, patch: dict):
        """Updates a task's status in the database."""

        from flamenco import current_flamenco
        from pillar.api.utils.authentication import current_user_id

        tasks_coll = current_flamenco.db('tasks')
        task = tasks_coll.find_one({'_id': task_id},
                                   projection={
                                       'job': 1,
                                       'manager': 1,
                                       'status': 1
                                   })

        if not current_flamenco.manager_manager.user_may_use(
                mngr_doc_id=task['manager']):
            log.warning(
                'patch_set_task_status(%s, %r): User %s is not allowed to use manager %s!',
                task_id, patch, current_user_id(), task['manager'])
            raise wz_exceptions.Forbidden()

        new_status = patch['status']
        try:
            current_flamenco.task_manager.api_set_task_status(task, new_status)
        except ValueError:
            raise wz_exceptions.UnprocessableEntity('Invalid status')
Ejemplo n.º 8
0
    def patch_requeue(self, task_id: bson.ObjectId, patch: dict):
        """Re-queue a task and its successors."""

        from flamenco import current_flamenco
        from pillar.api.utils.authentication import current_user_id

        tasks_coll = current_flamenco.db('tasks')
        task = tasks_coll.find_one({'_id': task_id},
                                   projection={
                                       'job': 1,
                                       'manager': 1
                                   })

        if not current_flamenco.manager_manager.user_may_use(
                mngr_doc_id=task['manager']):
            log.warning(
                'patch_set_task_status(%s, %r): User %s is not allowed to use manager %s!',
                task_id, patch, current_user_id(), task['manager'])
            raise wz_exceptions.Forbidden()

        current_flamenco.task_manager.api_requeue_task_and_successors(task_id)

        # Also inspect other tasks of the same job, and possibly update the job status as well.
        current_flamenco.job_manager.update_job_after_task_status_change(
            task['job'], task_id, 'queued')
Ejemplo n.º 9
0
def badger():
    if request.mimetype != 'application/json':
        log.debug('Received %s instead of application/json', request.mimetype)
        raise wz_exceptions.BadRequest()

    # Parse the request
    args = request.json
    action = args.get('action', '')
    user_email = args.get('user_email', '')
    role = args.get('role', '')

    current_user_id = authentication.current_user_id()
    log.info('Service account %s %ss role %r to/from user %s', current_user_id,
             action, role, user_email)

    users_coll = current_app.data.driver.db['users']

    # Check that the user is allowed to grant this role.
    srv_user = users_coll.find_one(current_user_id,
                                   projection={'service.badger': 1})
    if srv_user is None:
        log.error(
            'badger(%s, %s, %s): current user %s not found -- how did they log in?',
            action, user_email, role, current_user_id)
        return 'User not found', 403

    allowed_roles = set(srv_user.get('service', {}).get('badger', []))
    if role not in allowed_roles:
        log.warning(
            'badger(%s, %s, %s): service account not authorized to %s role %s',
            action, user_email, role, action, role)
        return 'Role not allowed', 403

    return do_badger(action, role=role, user_email=user_email)
Ejemplo n.º 10
0
    def patch_archive_job(self, job_id: bson.ObjectId, patch: dict):
        """Archives the given job in a background task."""
        job = self.assert_job_access(job_id)

        log.info('User %s uses PATCH to start archival of job %s',
                 current_user_id(), job_id)
        current_flamenco.job_manager.archive_job(job)
Ejemplo n.º 11
0
def patch_node(node_id):
    # Parse the request
    node_id = str2id(node_id)
    patch = request.get_json()

    # Find the node type.
    node = mongo.find_one_or_404('nodes', node_id, projection={'node_type': 1})
    try:
        node_type = node['node_type']
    except KeyError:
        msg = 'Node %s has no node_type property' % node_id
        log.warning(msg)
        raise wz_exceptions.InternalServerError(msg)
    log.debug('User %s wants to PATCH %s node %s',
              authentication.current_user_id(), node_type, node_id)

    # Find the PATCH handler for the node type.
    try:
        patch_handler = custom.patch_handlers[node_type]
    except KeyError:
        log.info('No patch handler for node type %r', node_type)
        raise wz_exceptions.MethodNotAllowed(
            'PATCH on node type %r not allowed' % node_type)

    # Let the PATCH handler do its thing.
    return patch_handler(node_id, patch)
Ejemplo n.º 12
0
def texture_libraries():
    from . import blender_cloud_addon_version

    # Use Eve method so that we get filtering on permissions for free.
    # This gives all the projects that contain the required node types.
    request.args = MultiDict(
        request.args)  # allow changes; it's an ImmutableMultiDict by default.
    request.args.setlist(eve_config.QUERY_PROJECTION, [TL_PROJECTION])
    request.args.setlist(eve_config.QUERY_SORT, [TL_SORT])

    # Determine whether to return HDRi projects too, based on the version
    # of the Blender Cloud Addon. If the addon version is None, we're dealing
    # with a version of the BCA that's so old it doesn't send its version along.
    addon_version = blender_cloud_addon_version()
    return_hdri = addon_version is not None and addon_version >= FIRST_ADDON_VERSION_WITH_HDRI
    log.debug('User %s has Blender Cloud Addon version %s; return_hdri=%s',
              current_user_id(), addon_version, return_hdri)

    accept_as_library = functools.partial(has_texture_node,
                                          return_hdri=return_hdri)

    # Construct eve-like response.
    projects = list(keep_fetching_texture_libraries(accept_as_library))
    result = {
        '_items': projects,
        '_meta': {
            'max_results': len(projects),
            'page': 1,
            'total': len(projects),
        }
    }

    return utils.jsonify(result)
Ejemplo n.º 13
0
    def patch_requeue_failed_tasks(self, job_id: bson.ObjectId, patch: dict):
        """Re-queue all failed tasks in this job."""
        self.assert_job_access(job_id)

        log.info('User %s uses PATCH to requeue failed tasks of job %s', current_user_id(), job_id)
        current_flamenco.task_manager.api_set_task_status_for_job(
            job_id, from_status='failed', to_status='queued')
Ejemplo n.º 14
0
    def patch_request_task_log_file(self, task_id: bson.ObjectId, patch: dict):
        """Queue a request to the Manager to upload this task's log file."""

        from flamenco import current_flamenco
        from pillar.api.utils.authentication import current_user_id

        tasks_coll = current_flamenco.db('tasks')
        task = tasks_coll.find_one(
            {'_id': task_id},
            projection={'job': 1, 'manager': 1, 'log_file': 1, 'project': 1, 'status': 1})

        if not current_flamenco.manager_manager.user_may_use(mngr_doc_id=task['manager']):
            log.warning('request_task_log_file(%s, %r): User %s is not allowed to use manager %s!',
                        task_id, patch, current_user_id(), task['manager'])
            raise wz_exceptions.Forbidden()

        status = task['status']
        if status not in LOG_UPLOAD_REQUESTABLE_TASK_STATES:
            ok = ', '.join(LOG_UPLOAD_REQUESTABLE_TASK_STATES)
            raise wz_exceptions.BadRequest(
                f'Log file not requestable while task is in status {status}, must be in {ok}')

        # Check that the log file hasn't arrived yet (this may not be the
        # first request for this task).
        force_rerequest = patch.get('force_rerequest', False)
        if task.get('log_file') and not force_rerequest:
            url = url_for('flamenco.tasks.perproject.download_task_log_file',
                          project_url=get_project_url(task['project']),
                          task_id=task_id)
            # Using 409 Conflict because a 303 See Other (which would be more
            # appropriate) cannot be intercepted by some AJAX calls.
            return redirect(url, code=409)

        current_flamenco.manager_manager.queue_task_log_request(
            task['manager'], task['job'], task_id)
Ejemplo n.º 15
0
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 '', 204

    # 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 'Internal error', 500
    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'
        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:
        action = 'grant' if grant_demo else 'revoke'
        my_log.info('%sing demo role to user %s (Blender ID email %s)', action,
                    user_id, email)
        service.do_badger(action, role='demo', user_id=user_id)
    else:
        my_log.debug('Not changing demo role, grant=%r and is=%s', grant_demo,
                     is_demo)

    return '', 204
Ejemplo n.º 16
0
    def patch_archive_job(self, job_id: bson.ObjectId, patch: dict):
        """Archives the given job in a background task."""

        from flamenco import current_flamenco
        from pillar.api.utils.authentication import current_user_id

        job = self.assert_job_access(job_id)

        log.info('User %s uses PATCH to start archival of job %s', current_user_id(), job_id)
        current_flamenco.job_manager.archive_job(job)
Ejemplo n.º 17
0
    def _assign_or_remove_project(self, manager_id: bson.ObjectId, patch: dict, action: str):
        """Assigns a manager to a project or removes it.

        The calling user must be owner of the manager (always)
        and member of the project (if assigning).
        """

        from pillar.api.projects.utils import user_rights_in_project

        from flamenco import current_flamenco

        try:
            project_strid = patch['project']
        except KeyError:
            log.warning('User %s sent invalid PATCH %r for manager %s.',
                        current_user_id(), patch, manager_id)
            raise wz_exceptions.BadRequest('Missing key "project"')

        project_id = str2id(project_strid)

        if not current_flamenco.manager_manager.user_is_owner(mngr_doc_id=manager_id):
            log.warning('User %s uses PATCH to %s manager %s to/from project %s, '
                        'but user is not owner of that Manager. Request denied.',
                        current_user_id(), action, manager_id, project_id)
            raise wz_exceptions.Forbidden()

        # Removing from a project doesn't require project membership.
        if action != 'remove':
            methods = user_rights_in_project(project_id)
            if 'PUT' not in methods:
                log.warning('User %s uses PATCH to %s manager %s to/from project %s, '
                            'but only has %s rights on project. Request denied.',
                            current_user_id(), action, manager_id, project_id, ', '.join(methods))
                raise wz_exceptions.Forbidden()

        log.info('User %s uses PATCH to %s manager %s to/from project %s',
                 current_user_id(), action, manager_id, project_id)

        ok = current_flamenco.manager_manager.api_assign_to_project(
            manager_id, project_id, action)
        if not ok:
            # Manager Manager will have already logged the cause.
            raise wz_exceptions.InternalServerError()
Ejemplo n.º 18
0
def recreate_job(project: pillarsdk.Project, job_id):
    from pillar.api.utils.authentication import current_user_id
    from pillar.api.utils import str2id

    log.info('Recreating job %s on behalf of user %s', job_id, current_user_id())

    job_id = str2id(job_id)
    current_flamenco.api_recreate_job(job_id)

    return '', 204
Ejemplo n.º 19
0
def recreate_job(project: pillarsdk.Project, job_id):
    from pillar.api.utils.authentication import current_user_id
    from pillar.api.utils import str2id

    log.info('Recreating job %s on behalf of user %s', job_id, current_user_id())

    job_id = str2id(job_id)
    current_flamenco.api_recreate_job(job_id)

    return '', 204
Ejemplo n.º 20
0
    def patch_edit_from_web(self, manager_id: bson.ObjectId, patch: dict):
        """Updates Manager fields from the web."""

        from pymongo.results import UpdateResult

        if not current_flamenco.manager_manager.user_is_owner(
                mngr_doc_id=manager_id):
            log.warning(
                'User %s uses PATCH to edit manager %s, '
                'but user is not owner of that Manager. Request denied.',
                current_user_id(), manager_id)
            raise wz_exceptions.Forbidden()

        # Only take known fields from the patch, don't just copy everything.
        update = {'name': patch['name'], 'description': patch['description']}
        self.log.info('User %s edits Manager %s: %s', current_user_id(),
                      manager_id, update)

        validator = current_app.validator_for_resource('flamenco_managers')
        if not validator.validate_update(
                update, manager_id, persisted_document={}):
            resp = jsonify({
                '_errors':
                validator.errors,
                '_message':
                ', '.join(f'{field}: {error}'
                          for field, error in validator.errors.items()),
            })
            resp.status_code = 422
            return resp

        managers_coll = current_flamenco.db('managers')
        result: UpdateResult = managers_coll.update_one({'_id': manager_id},
                                                        {'$set': update})

        if result.matched_count != 1:
            self.log.warning(
                'User %s edits Manager %s but update matched %i items',
                current_user_id(), manager_id, result.matched_count)
            raise wz_exceptions.BadRequest()

        return '', 204
Ejemplo n.º 21
0
    def user_is_admin(self, org_id: bson.ObjectId) -> bool:
        """Returns whether the currently logged in user is the admin of the organization."""

        from pillar.api.utils.authentication import current_user_id

        uid = current_user_id()
        if uid is None:
            return False

        org = self._get_org(org_id, projection={'admin_uid': 1})
        return org.get('admin_uid') == uid
Ejemplo n.º 22
0
def before_returning_project_resource_permissions(response):
    # Return only those projects the user has access to.
    allow = []
    for project in response['_items']:
        if authorization.has_permissions('projects', project,
                                         'GET', append_allowed_methods=True):
            allow.append(project)
        else:
            log.debug('User %s requested project %s, but has no access to it; filtered out.',
                      authentication.current_user_id(), project['_id'])

    response['_items'] = allow
Ejemplo n.º 23
0
    def patch_set_job_priority(self, job_id: bson.ObjectId, patch: dict):
        """Updates a job's priority in the database."""
        self.assert_job_access(job_id)

        new_prio = patch['priority']
        if not isinstance(new_prio, int):
            log.debug('patch_set_job_priority(%s): invalid prio %r received', job_id, new_prio)
            raise wz_exceptions.UnprocessableEntity(f'Priority {new_prio} should be an integer')

        log.info('User %s uses PATCH to set job %s priority to %d',
                 current_user_id(), job_id, new_prio)
        current_flamenco.job_manager.api_set_job_priority(job_id, new_prio)
Ejemplo n.º 24
0
def check_task_edit_permissions(task_doc: typing.Union[list, dict], *, action: str):
    """For now, only admins are allowed to create and delete tasks."""

    from pillar.api.utils.authentication import current_user_id

    if isinstance(task_doc, list):
        assert action == 'create'
        for task in task_doc:
            check_task_edit_permissions(task, action=action)
        return

    project_id = task_doc.get('project')
    if not project_id:
        log.info('User %s tried to %s a task without project ID; denied',
                 current_user_id(), action)
        raise wz_exceptions.BadRequest()

    auth = current_flamenco.auth
    if not auth.current_user_may(auth.Actions.USE, project_id):
        log.info('User %s tried to %s a task on project %s, but has no access to Flamenco there;'
                 ' denied', current_user_id(), action, project_id)
        raise wz_exceptions.Forbidden()
Ejemplo n.º 25
0
def check_job_permissions_modify(job_doc, original_doc=None):
    """Checks whether the current user is allowed to use Flamenco on this project."""

    flauth = current_flamenco.auth
    if not flauth.current_user_may(flauth.Actions.USE, job_doc.get('project')):
        from pillar.api.utils.authentication import current_user_id
        from flask import request

        log.warning('Denying user %s %s of job %s',
                    current_user_id(), request.method, job_doc.get('_id'))
        raise wz_exceptions.Forbidden()

    handle_job_status_update(job_doc, original_doc)
Ejemplo n.º 26
0
def check_job_permissions_modify(job_doc, original_doc=None):
    """Checks whether the current user is allowed to use Flamenco on this project."""

    flauth = current_flamenco.auth
    if not flauth.current_user_may(flauth.Actions.USE, job_doc.get('project')):
        from pillar.api.utils.authentication import current_user_id
        from flask import request

        log.warning('Denying user %s %s of job %s',
                    current_user_id(), request.method, job_doc.get('_id'))
        raise wz_exceptions.Forbidden()

    handle_job_status_update(job_doc, original_doc)
Ejemplo n.º 27
0
    def patch_edit_from_web(self, manager_id: bson.ObjectId, patch: dict):
        """Updates Manager fields from the web."""

        from pymongo.results import UpdateResult

        if not current_flamenco.manager_manager.user_is_owner(mngr_doc_id=manager_id):
            log.warning('User %s uses PATCH to edit manager %s, '
                        'but user is not owner of that Manager. Request denied.',
                        current_user_id(), manager_id)
            raise wz_exceptions.Forbidden()

        # Only take known fields from the patch, don't just copy everything.
        update = {'name': patch['name'],
                  'description': patch['description']}
        self.log.info('User %s edits Manager %s: %s', current_user_id(), manager_id, update)

        validator = current_app.validator_for_resource('flamenco_managers')
        if not validator.validate_update(update, manager_id):
            resp = jsonify({
                '_errors': validator.errors,
                '_message': ', '.join(f'{field}: {error}'
                                      for field, error in validator.errors.items()),
            })
            resp.status_code = 422
            return resp

        managers_coll = current_flamenco.db('managers')
        result: UpdateResult = managers_coll.update_one(
            {'_id': manager_id},
            {'$set': update}
        )

        if result.matched_count != 1:
            self.log.warning('User %s edits Manager %s but update matched %i items',
                             current_user_id(), manager_id, result.matched_count)
            raise wz_exceptions.BadRequest()

        return '', 204
Ejemplo n.º 28
0
    def wrapper(manager_id, *args, **kwargs):
        from flamenco import current_flamenco
        from pillar.api.utils import str2id, mongo

        manager_id = str2id(manager_id)
        manager = mongo.find_one_or_404('flamenco_managers', manager_id)
        if not current_flamenco.manager_manager.user_manages(mngr_doc=manager):
            user_id = authentication.current_user_id()
            log.warning(
                'Service account %s sent startup notification for manager %s of another '
                'service account', user_id, manager_id)
            raise wz_exceptions.Unauthorized()

        return wrapped(manager_id, request.json, *args, **kwargs)
Ejemplo n.º 29
0
def patch_comment(node_id, patch):
    assert_is_valid_patch(node_id, patch)
    user_id = authentication.current_user_id()

    if patch['op'] in COMMENT_VOTING_OPS:
        result, node = vote_comment(user_id, node_id, patch)
    else:
        assert patch['op'] == 'edit', 'Invalid patch operation %s' % patch['op']
        result, node = edit_comment(user_id, node_id, patch)

    return jsonify({'_status': 'OK',
                    'result': result,
                    'properties': node['properties']
                    })
Ejemplo n.º 30
0
def patch_post(node_id, patch):
    assert_is_valid_patch(node_id, patch)
    user_id = authentication.current_user_id()

    if patch['op'] in COMMENT_VOTING_OPS:
        nodes_coll = current_app.db()['nodes']
        node = nodes_coll.find_one({'_id': node_id})

        old_rating = rating_difference(node)
        result, node = vote_comment(user_id, node_id, patch)
        new_rating = rating_difference(node)

        # Update the user karma based on the rating differences.
        karma_increase = (new_rating - old_rating) * POST_VOTE_WEIGHT
        if karma_increase != 0:
            node_user_id = nodes_coll.find_one({'_id': node_id},
                                               projection={
                                                   'user': 1,
                                               })['user']

            users_collection = current_app.db()['users']
            db_fieldname = f'extension_props_public.{EXTENSION_NAME}.karma'

            users_collection.find_one_and_update(
                {'_id': node_user_id},
                {'$inc': {
                    db_fieldname: karma_increase
                }},
                {db_fieldname: 1},
            )

        # Fetch the full node for updating hotness and reindexing
        # TODO (can be improved by making a partial update)
        node = nodes_coll.find_one({'_id': node['_id']})
        update_hot(node)
        nodes_coll.update_one(
            {'_id': node['_id']},
            {'$set': {
                'properties.hot': node['properties']['hot']
            }})

        algolia_index_post_save(node)
    else:
        return abort(403)

    return jsonify({
        '_status': 'OK',
        'result': result,
        'properties': node['properties']
    })
Ejemplo n.º 31
0
    def assert_job_access(self, job_id: bson.ObjectId) -> dict:
        # TODO: possibly store job and project into flask.g to reduce the nr of Mongo queries.
        job = current_flamenco.db('jobs').find_one({'_id': job_id},
                                                   {'project': 1,
                                                    'status': 1})
        auth = current_flamenco.auth

        if not auth.current_user_may(auth.Actions.USE, job['project']):
            log.info(
                'User %s wants to PATCH job %s, but has no right to use Flamenco on project %s',
                current_user_id(), job_id, job['project'])
            raise wz_exceptions.Forbidden('Denied Flamenco use on this project')

        return job
Ejemplo n.º 32
0
def check_task_edit_permissions(task_doc: typing.Union[list, dict], *,
                                action: str):
    """For now, only admins are allowed to create and delete tasks."""

    from pillar.api.utils.authentication import current_user_id

    if isinstance(task_doc, list):
        assert action == 'create'
        for task in task_doc:
            check_task_edit_permissions(task, action=action)
        return

    project_id = task_doc.get('project')
    if not project_id:
        log.info('User %s tried to %s a task without project ID; denied',
                 current_user_id(), action)
        raise wz_exceptions.BadRequest()

    auth = current_flamenco.auth
    if not auth.current_user_may(auth.Actions.USE, project_id):
        log.info(
            'User %s tried to %s a task on project %s, but has no access to Flamenco there;'
            ' denied', current_user_id(), action, project_id)
        raise wz_exceptions.Forbidden()
Ejemplo n.º 33
0
    def patch_set_job_status(self, job_id: bson.ObjectId, patch: dict):
        """Updates a job's status in the database."""
        self.assert_job_access(job_id)

        new_status = patch['status']

        log.info('User %s uses PATCH to set job %s status to "%s"',
                 current_user_id(), job_id, new_status)
        try:
            current_flamenco.job_manager.api_set_job_status(job_id, new_status)
        except ValueError as ex:
            log.debug('api_set_job_status(%s, %r) raised %s', job_id,
                      new_status, ex)
            raise wz_exceptions.UnprocessableEntity(
                f'Status {new_status} is invalid')
Ejemplo n.º 34
0
        def wrapper(manager_id, *args, **kwargs):
            from pillar.api.utils import mongo

            manager_id = str2id(manager_id)
            manager = mongo.find_one_or_404('flamenco_managers', manager_id)
            if not current_flamenco.manager_manager.user_manages(mngr_doc=manager):
                user_id = authentication.current_user_id()
                log.warning(
                    'Service account %s called manager API endpoint for manager %s of another '
                    'service account', user_id, manager_id)
                raise wz_exceptions.Unauthorized()

            return wrapped(manager if pass_manager_doc else manager_id,
                           request.json,
                           *args, **kwargs)
Ejemplo n.º 35
0
def setup_for_flamenco(project: pillarsdk.Project):
    from pillar.api.utils import str2id
    import flamenco.setup

    project_id = project._id

    if not project.has_method('PUT'):
        log.warning(
            'User %s tries to set up project %s for Flamenco, but has no PUT rights.',
            current_user, project_id)
        raise wz_exceptions.Forbidden()

    if not current_flamenco.auth.current_user_is_flamenco_user():
        log.warning(
            'User %s tries to set up project %s for Flamenco, but is not flamenco-user.',
            current_user, project_id)
        raise wz_exceptions.Forbidden()

    log.info('User %s sets up project %s for Flamenco', current_user,
             project_id)
    flamenco.setup.setup_for_flamenco(project.url)

    # Find the Managers available to this user, so we can auto-assign if there is exactly one.
    man_man = current_flamenco.manager_manager
    managers = man_man.owned_managers(
        [bson.ObjectId(gid) for gid in current_user.groups])
    manager_count = managers.count()

    project_oid = str2id(project_id)
    user_id = current_user_id()

    if manager_count == 0:
        _, mngr_doc, _ = man_man.create_new_manager('My Manager', '', user_id)
        assign_man_oid = mngr_doc['_id']
        log.info(
            'Created and auto-assigning Manager %s to project %s upon setup for Flamenco.',
            assign_man_oid, project_oid)
        man_man.api_assign_to_project(assign_man_oid, project_oid, 'assign')

    elif manager_count == 1:
        assign_manager = managers.next()
        assign_man_oid = str2id(assign_manager['_id'])
        log.info(
            'Auto-assigning Manager %s to project %s upon setup for Flamenco.',
            assign_man_oid, project_oid)
        man_man.api_assign_to_project(assign_man_oid, project_oid, 'assign')

    return '', 204
Ejemplo n.º 36
0
    def patch_set_job_status(self, job_id: bson.ObjectId, patch: dict):
        """Updates a job's status in the database."""

        from flamenco import current_flamenco
        from pillar.api.utils.authentication import current_user_id

        self.assert_job_access(job_id)

        new_status = patch['status']

        log.info('User %s uses PATCH to set job %s status to "%s"',
                 current_user_id(), job_id, new_status)
        try:
            current_flamenco.job_manager.api_set_job_status(job_id, new_status)
        except ValueError:
            raise wz_exceptions.UnprocessableEntity(f'Status {new_status} is invalid')
Ejemplo n.º 37
0
def check_permission_fetch(doc: dict, *, doc_name: str):
    """Checks permissions on the given task/job/anything with 'project' and 'manager' fields."""

    if current_flamenco.manager_manager.user_manages(mngr_doc_id=doc.get('manager')):
        # Managers can re-fetch their own tasks/jobs to validate their local cache.
        return

    project_id = doc.get('project')
    if not project_id:
        log.warning('Denying user %s GET access to %s %s because it has no "project" field',
                    current_user_id(), doc_name, doc.get('_id'))
        raise wz_exceptions.Forbidden()

    auth = current_flamenco.auth
    if auth.current_user_may(auth.Actions.VIEW, project_id):
        return

    raise wz_exceptions.Forbidden()
Ejemplo n.º 38
0
    def patch_requeue(self, task_id: bson.ObjectId, patch: dict):
        """Re-queue a task and its successors."""

        from flamenco import current_flamenco
        from pillar.api.utils.authentication import current_user_id

        tasks_coll = current_flamenco.db('tasks')
        task = tasks_coll.find_one({'_id': task_id}, projection={'job': 1, 'manager': 1})

        if not current_flamenco.manager_manager.user_may_use(mngr_doc_id=task['manager']):
            log.warning('patch_set_task_status(%s, %r): User %s is not allowed to use manager %s!',
                        task_id, patch, current_user_id(), task['manager'])
            raise wz_exceptions.Forbidden()

        current_flamenco.task_manager.api_requeue_task_and_successors(task_id)

        # Also inspect other tasks of the same job, and possibly update the job status as well.
        current_flamenco.job_manager.update_job_after_task_status_change(
            task['job'], task_id, 'queued')
Ejemplo n.º 39
0
def setup_for_flamenco(project: pillarsdk.Project):
    from pillar.api.utils import str2id
    import flamenco.setup

    project_id = project._id

    if not project.has_method('PUT'):
        log.warning('User %s tries to set up project %s for Flamenco, but has no PUT rights.',
                    current_user, project_id)
        raise wz_exceptions.Forbidden()

    if not current_flamenco.auth.current_user_is_flamenco_user():
        log.warning('User %s tries to set up project %s for Flamenco, but is not flamenco-user.',
                    current_user, project_id)
        raise wz_exceptions.Forbidden()

    log.info('User %s sets up project %s for Flamenco', current_user, project_id)
    flamenco.setup.setup_for_flamenco(project.url)

    # Find the Managers available to this user, so we can auto-assign if there is exactly one.
    man_man = current_flamenco.manager_manager
    managers = man_man.owned_managers([bson.ObjectId(gid) for gid in current_user.groups])
    manager_count = managers.count()

    project_oid = str2id(project_id)
    user_id = current_user_id()

    if manager_count == 0:
        _, mngr_doc, _ = man_man.create_new_manager('My Manager', '', user_id)
        assign_man_oid = mngr_doc['_id']
        log.info('Created and auto-assigning Manager %s to project %s upon setup for Flamenco.',
                 assign_man_oid, project_oid)
        man_man.api_assign_to_project(assign_man_oid, project_oid, 'assign')

    elif manager_count == 1:
        assign_manager = managers.next()
        assign_man_oid = str2id(assign_manager['_id'])
        log.info('Auto-assigning Manager %s to project %s upon setup for Flamenco.',
                 assign_man_oid, project_oid)
        man_man.api_assign_to_project(assign_man_oid, project_oid, 'assign')

    return '', 204
Ejemplo n.º 40
0
    def patch_set_task_status(self, task_id: bson.ObjectId, patch: dict):
        """Updates a task's status in the database."""

        from flamenco import current_flamenco
        from pillar.api.utils.authentication import current_user_id

        tasks_coll = current_flamenco.db('tasks')
        task = tasks_coll.find_one({'_id': task_id},
                                   projection={'job': 1, 'manager': 1, 'status': 1})

        if not current_flamenco.manager_manager.user_may_use(mngr_doc_id=task['manager']):
            log.warning('patch_set_task_status(%s, %r): User %s is not allowed to use manager %s!',
                        task_id, patch, current_user_id(), task['manager'])
            raise wz_exceptions.Forbidden()

        new_status = patch['status']
        try:
            current_flamenco.task_manager.api_set_task_status(task, new_status)
        except ValueError:
            raise wz_exceptions.UnprocessableEntity('Invalid status')
Ejemplo n.º 41
0
    def patch_construct(self, job_id: bson.ObjectId, patch: dict):
        """Send job to 'under-construction' status and compile its tasks.

        The patch can contain extra settings for the job, such as 'filepath'
        for Blender render jobs.
        """
        job = self.assert_job_access(job_id)

        job_status = job.get('status', '-unset-')
        if job_status != 'waiting-for-files':
            log.info('User %s wants to PATCH to construct job %s; rejecting because it is in '
                     'status %r', current_user_id(), job_id, job_status)
            raise wz_exceptions.UnprocessableEntity(
                f'Current job status {job_status} does not allow job construction')

        # TODO(Sybren): add job settings handling.
        user = current_user()
        new_job_settings = patch.get('settings')
        log.info('User %s uses PATCH to construct job %s with settings %s',
                 user.user_id, job_id, new_job_settings)
        current_flamenco.job_manager.api_construct_job(
            job_id, new_job_settings,
            reason=f'Construction initiated by {user.full_name} (@{user.username})')
Ejemplo n.º 42
0
    def patch_archive_job(self, job_id: bson.ObjectId, patch: dict):
        """Archives the given job in a background task."""
        job = self.assert_job_access(job_id)

        log.info('User %s uses PATCH to start archival of job %s', current_user_id(), job_id)
        current_flamenco.job_manager.archive_job(job)