Exemple #1
0
def handle_job_status_update(job_doc, original_doc):
    """Calls upon the JobManager to handle a job status update, if there is any."""

    if original_doc is None:
        return

    job_id = job_doc.get('_id')
    if not job_id:
        log.warning('handle_job_status_update: No _id in new job document, rejecting')
        raise wz_exceptions.UnprocessableEntity('missing _id')

    try:
        old_status = original_doc['status']
    except KeyError:
        log.info('handle_job_status_update: No status in old job document %s, ignoring', job_id)
        return

    try:
        new_status = job_doc['status']
    except KeyError:
        log.warning('handle_job_status_update: No status in new job document %s, rejecting', job_id)
        raise wz_exceptions.UnprocessableEntity('missing status field')

    if old_status == new_status:
        # No change, so nothing to handle.
        return

    from flamenco import current_flamenco
    current_flamenco.job_manager.handle_job_status_change(job_id, old_status, new_status)
Exemple #2
0
def deduct_content_type_and_duration(node_doc, original=None):
    """Deduct the content type from the attached file, if any."""

    if node_doc['node_type'] != 'asset':
        log.debug('deduct_content_type: called on node type %r, ignoring',
                  node_doc['node_type'])
        return

    node_id = node_doc.get('_id')
    try:
        file_id = ObjectId(node_doc['properties']['file'])
    except KeyError:
        if node_id is None:
            # Creation of a file-less node is allowed, but updates aren't.
            return
        log.warning(
            'deduct_content_type: Asset without properties.file, rejecting.')
        raise wz_exceptions.UnprocessableEntity(
            'Missing file property for asset node')

    files = current_app.data.driver.db['files']
    file_doc = files.find_one({'_id': file_id}, {
        'content_type': 1,
        'variations': 1
    })
    if not file_doc:
        log.warning(
            'deduct_content_type: Node %s refers to non-existing file %s, rejecting.',
            node_id, file_id)
        raise wz_exceptions.UnprocessableEntity(
            'File property refers to non-existing file')

    # Guess the node content type from the file content type
    file_type = file_doc['content_type']
    if file_type.startswith('video/'):
        content_type = 'video'
    elif file_type.startswith('image/'):
        content_type = 'image'
    else:
        content_type = 'file'

    node_doc['properties']['content_type'] = content_type

    if content_type == 'video':
        duration = file_doc['variations'][0].get('duration')
        if duration:
            node_doc['properties']['duration_seconds'] = duration
        else:
            log.warning('Video file %s has no duration', file_id)
Exemple #3
0
 def system_import():
     db = app.data.driver.db
     dump_first = request.method.upper() == "POST"
     if not "file" in request.files:
         raise exceptions.UnprocessableEntity("Missing 'file' parameter")
     (f, filename) = tempfile.mkstemp()
     os.close(f)
     try:
         request.files["file"].save(filename)
         cmd = [
             "mongorestore", "--host", db.client.address[0], "--port",
             str(db.client.address[1]), "--gzip",
             "--archive={}".format(filename)
         ]
         if dump_first:
             cmd.append("--drop")
         print("RUNNING: {}".format(cmd))
         output = subprocess.check_output(cmd)
         print(output)
         return jsonify({"success": True})
     except Exception as e:
         print(e)
         raise exceptions.BadRequest("Error parsing input:" + str(e))
     finally:
         os.remove(filename)
Exemple #4
0
    def archive_job(self, job: dict):
        """Initiates job archival by creating a Celery task for it."""

        from flamenco.celery import job_archival

        job_id = job['_id']
        job_status = job['status']

        if job_status in ARCHIVE_JOB_STATES:
            msg = f'Job {job_id} cannot be archived, it has status {job_status}'
            self._log.info(msg)
            raise wz_exceptions.UnprocessableEntity(msg)

        # Store current job status in a special key so that it can be restored before
        # writing to the archive ZIP file as JSON.
        jobs_coll = current_flamenco.db('jobs')
        jobs_coll.update_one({'_id': job_id},
                             {'$set': {
                                 'pre_archive_status': job_status
                             }})

        # Immediately set job status to 'archiving', as this should be reflected ASAP in the
        # database + web interface, rather than waiting for a Celery Worker to pick it up.
        self.api_set_job_status(job_id, 'archiving')

        self._log.info(
            'Creating Celery background task for archival of job %s', job_id)
        job_archival.archive_job.delay(str(job_id))
    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')
Exemple #6
0
    def remove_user(self,
                    org_id: bson.ObjectId,
                    *,
                    user_id: bson.ObjectId = None,
                    email: str = None) -> dict:
        """Removes a user from the organization.

        The user can be identified by either user ID or email.

        Returns the new organization document.
        """

        users_coll = current_app.db('users')

        assert user_id or email

        # Collect the email address if not given. This ensures the removal
        # if the email was accidentally in the unknown_members list.
        if email is None:
            user_doc = users_coll.find_one(user_id, projection={'email': 1})
            if user_doc is not None:
                email = user_doc['email']

        # See if we know this user.
        if user_id is None:
            user_doc = users_coll.find_one({'email': email}, projection={'_id': 1})
            if user_doc is not None:
                user_id = user_doc['_id']

        if user_id and not users_coll.count({'_id': user_id}):
            raise wz_exceptions.UnprocessableEntity('User does not exist')

        self._log.info('Removing user %s / %s from organization %s', user_id, email, org_id)

        org_doc = self._get_org(org_id)

        # Compute the new members.
        if user_id:
            members = set(org_doc.get('members') or []) - {user_id}
            org_doc['members'] = list(members)

        if email:
            unknown_members = set(org_doc.get('unknown_members')) - {email}
            org_doc['unknown_members'] = list(unknown_members)

        r, _, _, status = current_app.put_internal('organizations',
                                                   remove_private_keys(org_doc),
                                                   _id=org_id)
        if status != 200:
            self._log.error('Error updating organization; status should be 200, not %i: %s',
                            status, r)
            raise ValueError(f'Unable to update organization, status code {status}')
        org_doc.update(r)

        # Update the roles for the affected member.
        if user_id:
            self.refresh_roles(user_id)

        return org_doc
Exemple #7
0
def comments_create():
    content = request.form['content']
    parent_id = request.form.get('parent_id')

    if not parent_id:
        log.warning('User %s tried to create comment without parent_id',
                    current_user.objectid)
        raise wz_exceptions.UnprocessableEntity()

    api = system_util.pillar_api()
    parent_node = Node.find(parent_id, api=api)
    if not parent_node:
        log.warning(
            'Unable to create comment for user %s, parent node %r not found',
            current_user.objectid, parent_id)
        raise wz_exceptions.UnprocessableEntity()

    log.info('Creating comment for user %s on parent node %r',
             current_user.objectid, parent_id)

    comment_props = dict(project=parent_node.project,
                         name='Comment',
                         user=current_user.objectid,
                         node_type='comment',
                         properties=dict(content=content,
                                         status='published',
                                         confidence=0,
                                         rating_positive=0,
                                         rating_negative=0))

    if parent_id:
        comment_props['parent'] = parent_id

    # Get the parent node and check if it's a comment. In which case we flag
    # the current comment as a reply.
    parent_node = Node.find(parent_id, api=api)
    if parent_node.node_type == 'comment':
        comment_props['properties']['is_reply'] = True

    comment = Node(comment_props)
    comment.create(api=api)

    return jsonify({'node_id': comment._id}), 201
    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)
Exemple #9
0
def find_node_or_raise(node_id, *args):
    nodes_coll = current_app.db('nodes')
    node_to_comment = nodes_coll.find_one({
        '_id': node_id,
        '_deleted': {
            '$ne': True
        },
    })
    if not node_to_comment:
        log.warning(args)
        raise wz_exceptions.UnprocessableEntity()
    return node_to_comment
def set_task_status(project, task_id):
    new_status = request.form['status']
    if new_status not in ALLOWED_TASK_STATUSES_FROM_WEB:
        log.warning(
            'User %s tried to set status of task %s to disallowed status "%s"; denied.',
            current_user.objectid, task_id, new_status)
        raise wz_exceptions.UnprocessableEntity('Status "%s" not allowed' %
                                                new_status)

    log.info('User %s set status of task %s to "%s"', current_user.objectid,
             task_id, new_status)
    current_flamenco.task_manager.web_set_task_status(task_id, new_status)

    return '', 204
Exemple #11
0
def set_job_status(job_id):
    from flask_login import current_user

    # FIXME Sybren: add permission check.

    new_status = request.form['status']
    if new_status not in ALLOWED_JOB_STATUSES_FROM_WEB:
        log.warning('User %s tried to set status of job %s to disallowed status "%s"; denied.',
                    current_user.objectid, job_id, new_status)
        raise wz_exceptions.UnprocessableEntity('Status "%s" not allowed' % new_status)

    log.info('User %s set status of job %s to "%s"', current_user.objectid, job_id, new_status)
    current_flamenco.job_manager.web_set_job_status(job_id, new_status)

    return '', 204
Exemple #12
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')
Exemple #13
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')
Exemple #14
0
def before_replacing_user(request, lookup):
    """Prevents changes to any field of the user doc, except USER_EDITABLE_FIELDS."""

    # Find the user that is being replaced
    req = parse_request('users')
    req.projection = json.dumps({key: 0 for key in USER_EDITABLE_FIELDS})
    original = current_app.data.find_one('users', req, **lookup)

    # Make sure that the replacement has a valid auth field.
    put_data = request.get_json()
    if put_data is None:
        raise wz_exceptions.BadRequest('No JSON data received')

    # We should get a ref to the cached JSON, and not a copy. This will allow us to
    # modify the cached JSON so that Eve sees our modifications.
    assert put_data is request.get_json()

    # Reset fields that shouldn't be edited to their original values. This is only
    # needed when users are editing themselves; admins are allowed to edit much more.
    if not pillar.auth.current_user.has_cap('admin'):
        for db_key, db_value in original.items():
            if db_key[0] == '_' or db_key in USER_EDITABLE_FIELDS:
                continue

            if db_key in original:
                put_data[db_key] = copy.deepcopy(original[db_key])

        # Remove fields added by this PUT request, except when they are user-editable.
        for put_key in list(put_data.keys()):
            if put_key[0] == '_' or put_key in USER_EDITABLE_FIELDS:
                continue

            if put_key not in original:
                del put_data[put_key]

    # Always restore those fields
    for db_key in USER_ALWAYS_RESTORE_FIELDS:
        if db_key in original:
            put_data[db_key] = copy.deepcopy(original[db_key])
        else:
            del put_data[db_key]

    # Regular users should always have an email address
    if 'service' not in put_data.get('roles', ()):
        if not put_data.get('email'):
            raise wz_exceptions.UnprocessableEntity(
                'email field must be given')
Exemple #15
0
def view_task_log(project, task_id):
    """Shows a limited number of task log entries.

    Pass page=N (N≥1) to request further entries.
    """

    from pillarsdk import ResourceNotFound
    from pillar.web.utils import is_valid_id, last_page_index
    from flamenco.tasks.sdk import TaskLog

    if not is_valid_id(task_id):
        raise wz_exceptions.UnprocessableEntity()

    page_idx = int(request.args.get('page', 1))
    api = pillar_api()
    try:
        logs = TaskLog.all(
            {
                'where': {
                    'task': task_id
                },
                'page': page_idx,
                'max_results': TASK_LOG_PAGE_SIZE
            },
            api=api)
    except ResourceNotFound:
        logs = {
            '_items': [],
            '_meta': {
                'total': 0,
                'page': page_idx,
                'max_results': TASK_LOG_PAGE_SIZE
            }
        }

    last_page_idx = last_page_index(logs['_meta'])
    has_next_page = page_idx < last_page_idx
    has_prev_page = page_idx > 1

    return render_template('flamenco/tasks/view_task_log_embed.html',
                           page_idx=page_idx,
                           logs=logs,
                           has_prev_page=has_prev_page,
                           has_next_page=has_next_page,
                           last_page_idx=last_page_idx,
                           project=project,
                           task_id=task_id)
Exemple #16
0
    def _predict(self):
        # retrieve the data and validate the inputs. If
        # self.validate_request_data is True and a feature schema was
        # provided, the schema is vetted in get_post_data()
        X_input = self.get_post_data()

        # Only perform user checks after the schema has been (optionally)
        # validated. This way users don't need to do any error handling in
        # additional_checks.
        # If the user checks fail (ValueError raised) raise a 422.
        if self.additional_checks is not None:
            try:
                self.additional_checks(X_input)
            except ValueError as err:
                raise werkzeug_exc.UnprocessableEntity(*err.args) from err

        # Once the input data has been fully validated, extract the feature
        # columns (all features provided in ``feature_schema``) if provided.
        # This allows the user to fully anticipate what features are passed
        # to the preprocessor.
        if self.feature_columns:
            X_preprocessed = X_input[self.feature_columns]
        else:
            X_preprocessed = X_input

        # preprocess if user specified a preprocessor
        if self._preprocess_model_input:
            X_preprocessed = self.preprocessor.process(X_preprocessed)

        # get the predictions
        preds = self.model.predict(X_preprocessed)

        # postprocess
        if self._postprocess_model_output:
            preds = self.postprocessor.process(X_input, X_preprocessed, preds)

        # finally format the predictions and return
        if self.batch_prediction:
            response = porter_responses.make_batch_prediction_response(
                X_input[_ID], preds)
        else:
            response = porter_responses.make_prediction_response(
                X_input[_ID].iloc[0], preds[0])

        return response
Exemple #17
0
def update_drink(token, id):
    try:
        data = request.get_json()
        drink = Drink.query.filter(Drink.id == id).one_or_none()
        if not drink:
            abort(404)

        for key in data.keys():
            if hasattr(drink, key):
                if key == "recipe":
                    if not isinstance(data["recipe"], list):
                        raise exceptions.UnprocessableEntity()
                    setattr(drink, key, json.dumps(data[key]))
                    continue
                setattr(drink, key, data[key])
        drink.update()

        return jsonify({"success": True, "drinks": [drink.long()]})
    except:
        abort(404)  # Resource Not Found
def download_task_log(project, task_id):
    """Show the entire task log as text/plain.

    This endpoint is for obtaining the task log stored in MongoDB. This
    approach is deprecated in favour of having the Manager store & serve the
    logs and upload them to the Server on demand (see flamenco.managers.api).
    """

    if not is_valid_id(task_id):
        raise wz_exceptions.UnprocessableEntity()

    # Required because the stream_log() generator will run outside the app context.
    app = current_app.real_app
    api = pillar_api()

    def stream_log():
        page_idx = 1
        while True:
            with app.app_context():
                logs = TaskLog.all(
                    {
                        'where': {
                            'task': task_id
                        },
                        'page': page_idx,
                        'max_results': TASK_LOG_PAGE_SIZE
                    },
                    api=api)

            for tasklog in logs['_items']:
                yield tasklog.log + '\n'

            if page_idx >= last_page_index(logs['_meta']):
                break

            page_idx += 1

    return Response(stream_log(), mimetype='text/plain')
Exemple #19
0
def download_task_log(project, task_id):
    """Shows the entire task log as text/plain"""

    from flask import Response, current_app

    from pillar.web.utils import is_valid_id, last_page_index
    from flamenco.tasks.sdk import TaskLog

    if not is_valid_id(task_id):
        raise wz_exceptions.UnprocessableEntity()

    # Required because the stream_log() generator will run outside the app context.
    app = current_app._get_current_object()
    api = pillar_api()

    def stream_log():
        page_idx = 1
        while True:
            with app.app_context():
                logs = TaskLog.all(
                    {
                        'where': {
                            'task': task_id
                        },
                        'page': page_idx,
                        'max_results': TASK_LOG_PAGE_SIZE
                    },
                    api=api)

            for tasklog in logs['_items']:
                yield tasklog.log + '\n'

            if page_idx >= last_page_index(logs['_meta']):
                break

            page_idx += 1

    return Response(stream_log(), mimetype='text/plain')
def download_task_log_file(project, task_id):
    """Redirect to the storage backend for the task log.

    This endpoint is for obtaining the task log sent to us by Flamenco Manager,
    and stored as a gzipped file in the storage backend. See the Manager API
    for the endpoint that actually accepts the log file from the Manager.
    """
    from pillar.web.utils import is_valid_id
    from .sdk import Task

    if not is_valid_id(task_id):
        raise wz_exceptions.UnprocessableEntity()

    api = pillar_api()
    task = Task.find(task_id, api=api)

    if not task.log_file:
        return wz_exceptions.NotFound(
            f'Task {task_id} has no attached log file')

    blob = current_flamenco.task_manager.logfile_blob(task.to_dict())
    url = blob.get_url(is_public=False)
    return redirect(url, code=307)
    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})')
Exemple #22
0
    def get_post_data(self):
        """Return POST data.

        Returns:
            The result of ``porter.config.json_encoder``

        Raises:
            :class:`werkzeug.exceptions.UnprocessableEntity`

        If ``self.validate_request_data is True`` and a request schema has
        been defined the data will be validated against the schema.
        """
        data = api.request_json()
        if self.validate_request_data:
            schema = self._request_schemas.get('POST')
            if schema is not None:
                try:
                    schema.validate(data)
                except ValueError as err:
                    if err.args[0].startswith('Schema validation failed'):
                        raise werkzeug_exc.UnprocessableEntity(*err.args)
                    else:
                        raise err
        return data
Exemple #23
0
def index():
    user_id = current_user.user_id

    # Fetch available Managers.
    man_man = current_flamenco.manager_manager
    managers = list(man_man.owned_managers(
        current_user.group_ids, {'_id': 1, 'name': 1}))
    manager_limit_reached = len(managers) >= flamenco.auth.MAX_MANAGERS_PER_USER

    # Get the query arguments
    identifier: str = request.args.get('identifier', '')
    return_url: str = request.args.get('return', '')
    request_hmac: str = request.args.get('hmac', '')

    ident_oid = str2id(identifier)
    keys_coll = current_flamenco.db('manager_linking_keys')

    # Verify the received identifier and return URL.
    key_info = keys_coll.find_one({'_id': ident_oid})
    if key_info is None:
        log.warning('User %s tries to link a manager with an identifier that cannot be found',
                    user_id)
        return render_template('flamenco/managers/linking/key_not_found.html')

    check_hmac(key_info['secret_key'],
               f'{identifier}-{return_url}'.encode('ascii'),
               request_hmac)

    # Only deal with POST data after HMAC verification is performed.
    if request.method == 'POST':
        manager_id = request.form['manager-id']

        if manager_id == 'new':
            manager_name = request.form.get('manager-name', '')
            if not manager_name:
                raise wz_exceptions.UnprocessableEntity('no Manager name given')

            if manager_limit_reached:
                log.warning('User %s tries to create a manager %r, but their limit is reached.',
                            user_id, manager_name)
                raise wz_exceptions.UnprocessableEntity('Manager count limit reached')

            log.info('Creating new Manager named %r for user %s', manager_name, user_id)
            account, mngr_doc, token_data = man_man.create_new_manager(manager_name, '', user_id)
            manager_oid = mngr_doc['_id']
        else:
            log.info('Linking existing Manager %r for user %s', manager_id, user_id)
            manager_oid = str2id(manager_id)

        # Store that this identifier belongs to this ManagerID
        update_res = keys_coll.update_one({'_id': ident_oid},
                                          {'$set': {'manager_id': manager_oid}})
        if update_res.matched_count != 1:
            log.error('Match count was %s when trying to update secret key '
                      'for Manager %s on behalf of user %s',
                      update_res.matched_count, manager_oid, user_id)
            raise wz_exceptions.InternalServerError('Unable to store manager ID')

        log.info('Manager ID %s is stored as belonging to key with identifier %s',
                 manager_oid, identifier)

        # Now redirect the user back to Flamenco Manager in a verifyable way.
        msg = f'{identifier}-{manager_oid}'.encode('ascii')
        mac = _compute_hash(key_info['secret_key'], msg)
        qs = urllib.parse.urlencode({
            'hmac': mac,
            'oid': str(manager_oid),
        })

        direct_to = urllib.parse.urljoin(return_url, f'?{qs}')
        log.info('Directing user to URL %s', direct_to)

        return redirect(direct_to, 307)

    return render_template('flamenco/managers/linking/choose_manager.html',
                           managers=managers,
                           can_create_manager=not manager_limit_reached)
Exemple #24
0
    def patch_edit_from_web(self, org_id: bson.ObjectId, patch: dict):
        """Updates Organization fields from the web.

        The PATCH command supports the following payload. The 'name' field must
        be set, all other fields are optional. When an optional field is
        ommitted it will be handled as an instruction to clear that field.
            {'name': str,
             'description': str,
             'website': str,
             'location': str,
             'ip_ranges': list of human-readable IP ranges}
        """

        from pymongo.results import UpdateResult
        from . import ip_ranges

        self._assert_is_admin(org_id)
        user = current_user()
        current_user_id = user.user_id

        # Only take known fields from the patch, don't just copy everything.
        update = {
            'name': patch['name'].strip(),
            'description': patch.get('description', '').strip(),
            'website': patch.get('website', '').strip(),
            'location': patch.get('location', '').strip(),
        }
        unset = {}

        # Special transformation for IP ranges
        iprs = patch.get('ip_ranges')
        if iprs:
            ipr_docs = []
            for r in iprs:
                try:
                    doc = ip_ranges.doc(r, min_prefixlen6=48, min_prefixlen4=8)
                except ValueError as ex:
                    raise wz_exceptions.UnprocessableEntity(
                        f'Invalid IP range {r!r}: {ex}')
                ipr_docs.append(doc)
            update['ip_ranges'] = ipr_docs
        else:
            unset['ip_ranges'] = True

        refresh_user_roles = False
        if user.has_cap('admin'):
            if 'seat_count' in patch:
                update['seat_count'] = int(patch['seat_count'])
            if 'org_roles' in patch:
                org_roles = [
                    stripped for stripped in (role.strip()
                                              for role in patch['org_roles'])
                    if stripped
                ]
                if not all(role.startswith('org-') for role in org_roles):
                    raise wz_exceptions.UnprocessableEntity(
                        'Invalid role given, all roles must start with "org-"')

                update['org_roles'] = org_roles
                refresh_user_roles = True

        self.log.info('User %s edits Organization %s: %s', current_user_id,
                      org_id, update)

        validator = current_app.validator_for_resource('organizations')
        if not validator.validate_update(update, org_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

        # Figure out what to set and what to unset
        for_mongo = {'$set': update}
        if unset:
            for_mongo['$unset'] = unset

        organizations_coll = current_app.db('organizations')
        result: UpdateResult = organizations_coll.update_one({'_id': org_id},
                                                             for_mongo)

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

        if refresh_user_roles:
            self.log.info(
                'Organization roles set for org %s, refreshing users', org_id)
            current_app.org_manager.refresh_all_user_roles(org_id)

        return '', 204
Exemple #25
0
def printjob_create(org_uuid):
    data = request.json
    if not data:
        return abort(make_response(jsonify(message="Missing payload"), 400))
    gcode_uuid = data.get("gcode", None)
    printer_uuid = data.get("printer",
                            None)  # FIXME: this should be part of the path
    if not gcode_uuid or not printer_uuid:
        return abort(
            make_response(
                jsonify(message="Missing gcode_uuid or printer_uuid"), 400))

    printer = printers.get_printer(printer_uuid)
    if not printer or printer['organization_uuid'] != org_uuid:
        raise http_exceptions.UnprocessableEntity(
            f"Invalid printer {printer_uuid} - does not exist.")

    gcode = gcodes.get_gcode(gcode_uuid)
    if not gcode:
        raise http_exceptions.UnprocessableEntity(
            "Invalid gcode {gcode_uuid} - does not exist.")

    network_client = network_clients.get_network_client(
        printer["network_client_uuid"])
    printer_data = dict(network_client)
    printer_data.update(dict(printer))
    printer_inst = clients.get_printer_instance(printer_data)
    try:
        printer_inst.upload_and_start_job(gcode["absolute_path"],
                                          gcode["path"])
    except DeviceInvalidState as e:
        raise http_exceptions.Conflict(*e.args)
    except DeviceCommunicationError as e:
        raise http_exceptions.GatewayTimeout(*e.args)
    # TODO: robin - add_printjob should be method of printer and printer a
    #               method of organization
    printjob_uuid = printjobs.add_printjob(
        gcode_uuid=gcode["uuid"],
        organization_uuid=org_uuid,
        printer_uuid=printer["uuid"],
        user_uuid=get_current_user()["uuid"],
        gcode_data={
            "uuid": gcode["uuid"],
            "filename": gcode["filename"],
            "size": gcode["size"],
            "available": True,
        },
        # FIXME: printer data should be kept in printer object only
        printer_data={
            "ip": printer_inst.ip,
            "port": printer_inst.port,
            "hostname": printer_inst.hostname,
            "name": printer_inst.name,
            "client": printer_inst.client,
        },
    )
    return (
        jsonify({
            "uuid": printjob_uuid,
            "user_uuid": get_current_user()["uuid"]
        }),
        201,
    )
Exemple #26
0
    def patch_edit_from_web(self, org_id: bson.ObjectId, patch: dict):
        """Updates Organization fields from the web."""

        from pymongo.results import UpdateResult

        self._assert_is_admin(org_id)
        user = current_user()
        current_user_id = user.user_id

        # Only take known fields from the patch, don't just copy everything.
        update = {
            'name': patch['name'].strip(),
            'description': patch.get('description', '').strip(),
            'website': patch.get('website', '').strip(),
            'location': patch.get('location', '').strip(),
        }

        refresh_user_roles = False
        if user.has_cap('admin'):
            if 'seat_count' in patch:
                update['seat_count'] = int(patch['seat_count'])
            if 'org_roles' in patch:
                org_roles = [
                    stripped for stripped in (role.strip()
                                              for role in patch['org_roles'])
                    if stripped
                ]
                if not all(role.startswith('org-') for role in org_roles):
                    raise wz_exceptions.UnprocessableEntity(
                        'Invalid role given, all roles must start with "org-"')

                update['org_roles'] = org_roles
                refresh_user_roles = True

        self.log.info('User %s edits Organization %s: %s', current_user_id,
                      org_id, update)

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

        organizations_coll = current_app.db('organizations')
        result: UpdateResult = organizations_coll.update_one({'_id': org_id},
                                                             {'$set': update})

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

        if refresh_user_roles:
            self.log.info(
                'Organization roles set for org %s, refreshing users', org_id)
            current_app.org_manager.refresh_all_user_roles(org_id)

        return '', 204
Exemple #27
0
def project_manage_users():
    """Manage users of a project. In this initial implementation, we handle
    addition and removal of a user to the admin group of a project.
    No changes are done on the project itself.
    """

    projects_collection = current_app.data.driver.db['projects']
    users_collection = current_app.data.driver.db['users']

    # TODO: check if user is admin of the project before anything
    if request.method == 'GET':
        project_id = request.args['project_id']
        project = projects_collection.find_one({'_id': ObjectId(project_id)})
        admin_group_id = project['permissions']['groups'][0]['group']

        users = users_collection.find({'groups': {
            '$in': [admin_group_id]
        }}, {
            'username': 1,
            'email': 1,
            'full_name': 1
        })
        return jsonify({'_status': 'OK', '_items': list(users)})

    # The request is not a form, since it comes from the API sdk
    data = json.loads(request.data)
    project_id = ObjectId(data['project_id'])
    target_user_id = ObjectId(data['user_id'])
    action = data['action']
    current_user_id = g.current_user['user_id']

    project = projects_collection.find_one({'_id': project_id})

    # Check if the current_user is owner of the project, or removing themselves.
    remove_self = target_user_id == current_user_id and action == 'remove'
    if project['user'] != current_user_id and not remove_self:
        return abort_with_error(403)

    admin_group = get_admin_group(project)

    # Get the user and add the admin group to it
    if action == 'add':
        operation = '$addToSet'
        log.info(
            'project_manage_users: Adding user %s to admin group of project %s',
            target_user_id, project_id)
    elif action == 'remove':
        log.info(
            'project_manage_users: Removing user %s from admin group of project %s',
            target_user_id, project_id)
        operation = '$pull'
    else:
        log.warning(
            'project_manage_users: Unsupported action %r called by user %s',
            action, current_user_id)
        raise wz_exceptions.UnprocessableEntity()

    users_collection.update({'_id': target_user_id},
                            {operation: {
                                'groups': admin_group['_id']
                            }})

    user = users_collection.find_one({'_id': target_user_id}, {
        'username': 1,
        'email': 1,
        'full_name': 1
    })
    user['_status'] = 'OK'
    return jsonify(user)