Exemple #1
0
def test_login_required_with_roles_unauthorized(mock_is_authenticated,
                                                inspire_app):
    func = Mock()
    decorated_func = login_required_with_roles(["role_a"])(func)
    with pytest.raises(Forbidden):
        decorated_func()
        assert func.called
Exemple #2
0
def test_login_required_with_roles_without_roles(
    mock_is_authenticated, base_app, db, es
):
    func = Mock()
    decorated_func = login_required_with_roles()(func)
    decorated_func()
    assert func.called
Exemple #3
0
def test_login_required_with_roles_unauthorized(
    mock_is_authenticated, base_app, db, es
):
    func = Mock()
    decorated_func = login_required_with_roles(["role_a"])(func)
    with pytest.raises(Unauthorized):
        decorated_func()
        assert func.called
Exemple #4
0
class AuthorSubmissionsResource(BaseSubmissionsResource):
    decorators = [login_required_with_roles()]

    def get(self, pid_value):
        try:
            record = AuthorsRecord.get_record_by_pid_value(pid_value)
        except PIDDoesNotExistError:
            abort(404)

        serialized_record = author_v1.dump(record)
        return jsonify({"data": serialized_record})

    def post(self):
        submission_data = request.get_json()
        return self.start_workflow_for_submission(submission_data["data"])

    def put(self, pid_value):
        submission_data = request.get_json()
        return self.start_workflow_for_submission(submission_data["data"], pid_value)

    def start_workflow_for_submission(self, submission_data, control_number=None):

        serialized_data = self.populate_and_serialize_data_for_submission(
            submission_data, control_number
        )
        data = {"data": serialized_data}
        response = self.send_post_request_to_inspire_next("/workflows/authors", data)

        if response.status_code == 200:
            return response.content
        else:
            abort(503)

    def populate_and_serialize_data_for_submission(
        self, submission_data, control_number=None
    ):
        submission_data["acquisition_source"] = self.get_acquisition_source()

        # TODO: create and use loader instead of directly using schema
        serialized_data = Author().load(submission_data).data

        if control_number:
            serialized_data["control_number"] = int(control_number)

        return serialized_data
Exemple #5
0
class MigratorErrorListResource(MethodView):
    """Return a list of errors belonging to invalid mirror records."""

    decorators = [
        login_required_with_roles(
            [Roles.superuser.value, Roles.cataloger.value])
    ]

    def get(self):
        errors = (LegacyRecordsMirror.query.filter(
            LegacyRecordsMirror.valid.is_(False),
            LegacyRecordsMirror.collection.in_(NON_DELETED_COLLECTIONS),
        ).order_by(LegacyRecordsMirror.last_updated.desc()).all())

        data = {"data": errors}
        data_serialized = ErrorList().dump(data).data
        response = jsonify(data_serialized)

        return response, 200
Exemple #6
0
class BaseSubmissionsResource(MethodView):
    decorators = [login_required_with_roles()]

    def load_data_from_request(self):
        return request.get_json()

    def send_post_request_to_inspire_next(self, endpoint, data):

        headers = {
            "content-type":
            "application/json",
            "Authorization":
            f"Bearer {current_app.config['AUTHENTICATION_TOKEN']}",
        }
        response = requests.post(
            f"{current_app.config['INSPIRE_NEXT_URL']}{endpoint}",
            data=json.dumps(data),
            headers=headers,
        )
        if response.status_code == 200:
            return response.content
        raise WorkflowStartError

    def get_acquisition_source(self):
        acquisition_source = dict(
            email=current_user.email,
            datetime=datetime.datetime.utcnow().isoformat(),
            method="submitter",
            source="submitter",
            internal_uid=int(current_user.get_id()),
        )

        orcid = self.get_user_orcid()
        if orcid:
            acquisition_source["orcid"] = orcid
        return acquisition_source

    # TODO: remove this and directly use `get_current_user_orcid`
    def get_user_orcid(self):
        return get_current_user_orcid()
Exemple #7
0
class LiteratureSubmissionResource(BaseSubmissionsResource):
    decorators = [login_required_with_roles()]

    def post(self):
        submission_data = request.get_json()
        return self.start_workflow_for_submission(submission_data["data"])

    def start_workflow_for_submission(self, submission_data, control_number=None):
        serialized_data = Literature().load(submission_data).data
        serialized_data["acquisition_source"] = self.get_acquisition_source()
        form_data = {
            "url": submission_data.get("pdf_link"),
            "references": submission_data.get("references"),
        }
        payload = {"data": serialized_data, "form_data": form_data}

        response = self.send_post_request_to_inspire_next(
            "/workflows/literature", payload
        )

        if response.status_code == 200:
            return response.content
        abort(503)
Exemple #8
0
class WorkflowsRecordSourcesResource(MethodView):

    view_name = "workflows_record_sources"
    decorators = [
        login_required_with_roles(
            [Roles.superuser.value, Roles.cataloger.value])
    ]

    @parser.error_handler
    def handle_error(error, req, schema, error_status_code, error_headers):
        message = f"Incorrect input for fields: {''.join(error.field_names)}"
        abort(400, message)

    @parser.use_args({
        "record_uuid": fields.String(required=True),
        "source": fields.String()
    })
    def get(self, args):
        record_uuid = args["record_uuid"]
        required_fields_mapping = {
            "created": WorkflowsRecordSources.created,
            "json": WorkflowsRecordSources.json,
            "record_uuid": WorkflowsRecordSources.record_uuid,
            "source": WorkflowsRecordSources.source,
            "updated": WorkflowsRecordSources.updated,
        }
        query = WorkflowsRecordSources.query.with_entities(
            *required_fields_mapping.values()).filter_by(
                record_uuid=str(record_uuid))
        source = args.get("source")
        if source:
            query = query.filter_by(source=source.lower())
        results = query.all()
        if not results:
            return jsonify({"message": "Workflow source not found"}), 404
        results_data = [{
            key: val
            for key, val in zip(required_fields_mapping.keys(), result)
        } for result in results]
        return jsonify({"workflow_sources": results_data}), 200

    @parser.use_args({
        "record_uuid": fields.String(required=True),
        "source": fields.String(required=True),
        "json": fields.Dict(required=True),
    })
    def post(self, args):
        record_uuid = args["record_uuid"]
        source = args["source"]
        root_json = args["json"]
        root = WorkflowsRecordSources(source=source,
                                      record_uuid=record_uuid,
                                      json=root_json)
        db.session.merge(root)
        db.session.commit()
        if root:
            return (
                jsonify({
                    "message":
                    f"workflow source for record {record_uuid} and source {source} added"
                }),
                200,
            )

    @parser.use_args({
        "record_uuid": fields.String(required=True),
        "source": fields.String(required=True),
    })
    def delete(self, args):
        record_uuid = args.get("record_uuid")
        source = args.get("source")
        result = WorkflowsRecordSources.query.filter_by(
            record_uuid=str(record_uuid), source=source.lower()).one_or_none()
        if not result:
            return (
                jsonify({
                    "message":
                    "No record found for given record_uuid and source!"
                }),
                404,
            )
        db.session.delete(result)
        db.session.commit()
        return jsonify({"message": "Record succesfully deleted"}), 200
Exemple #9
0
def test_login_required_role_a_superuser_always_allowed(
        mock_is_authenticated, inspire_app):
    func = Mock()
    decorated_func = login_required_with_roles(["role_a"])(func)
    decorated_func()
    assert func.called
Exemple #10
0
class JobSubmissionsResource(BaseSubmissionsResource):
    decorators = [login_required_with_roles()]
    user_allowed_status_changes = {
        "pending": ["pending"],
        "open": ["open", "closed"],
        "closed": ["closed"],
    }

    def get(self, pid_value):
        try:
            pid, _ = pid_value.data
            record = JobsRecord.get_record_by_pid_value(pid.pid_value)
        except PIDDoesNotExistError:
            abort(404)

        serialized_record = job_v1.dump(record)
        return jsonify({"data": serialized_record})

    def post(self):
        """Adds new job record"""
        data = job_loader_v1()
        data = self.prepare_data(data)
        record = JobsRecord.create(data)
        db.session.commit()
        self.create_ticket(record, "rt/new_job.html")
        return jsonify({"pid_value": record["control_number"]}), 201

    def put(self, pid_value):
        """Updates existing record in db"""
        data = job_loader_v1()
        try:
            pid, _ = pid_value.data
            record = JobsRecord.get_record_by_pid_value(pid.pid_value)
            if not self.user_can_edit(record):
                return (
                    jsonify(
                        {"message": "You are not allowed to edit this Job opening"}
                    ),
                    403,
                )
        except PIDDoesNotExistError:
            abort(404)
        data = self.prepare_data(data, record)
        record.update(data)
        db.session.commit()

        if not is_superuser_or_cataloger_logged_in():
            self.create_ticket(record, "rt/update_job.html")

        return jsonify({"pid_value": record["control_number"]})

    def prepare_new_record(self, data):
        if "$schema" not in data:
            data["$schema"] = url_for(
                "invenio_jsonschemas.get_schema",
                schema_path="records/jobs.json",
                _external=True,
            )
        if not is_superuser_or_cataloger_logged_in():
            data["status"] = "pending"

        builder = JobBuilder(record=data)
        if "acquisition_source" not in builder.record:
            acquisition_source = self.get_acquisition_source()
            builder.add_acquisition_source(**acquisition_source)
        return builder

    def prepare_update_record(self, data, record):
        # This contains all fields which can be removed from record (they are optional)
        # if new value sent from the form is None, or empty in any other way
        # (after de-serialization if it's missing from input data)
        # this fields will be removed from record
        additional_fields = [
            "external_job_identifier",
            "accelerator_experiments",
            "urls",
            "contact_details",
            "reference_letters",
        ]

        if not is_superuser_or_cataloger_logged_in():
            old_status = record.get("status", "pending")
            new_status = data.get("status", old_status)
            if (
                new_status != old_status
                and new_status not in self.user_allowed_status_changes[old_status]
            ):
                raise RESTDataError(
                    f"Only curator can change status from '{old_status}' to '{new_status}'."
                )
        record_data = dict(record)
        for key in additional_fields:
            if key not in data and key in record_data:
                del record_data[key]
        record_data.update(data)
        builder = JobBuilder(record=record_data)
        return builder

    def prepare_data(self, data, record=None):
        """Prepares data received from form.
        As jobs do not have any 'workflows' it's required to set all the logic
        for updating record from data provided by the user somewhere..."""

        if record:
            builder = self.prepare_update_record(data, record)
        else:
            builder = self.prepare_new_record(data)

        try:
            builder.validate_record()
        except ValidationError as e:
            LOGGER.exception("Cannot process job submission")
            raise RESTDataError(e.args[0])
        except SchemaError as e:
            LOGGER.exception("Schema is broken")
            abort(500, str(e))
        data = builder.record
        return data

    def user_can_edit(self, record):
        if is_superuser_or_cataloger_logged_in():
            return True
        acquisition_source = record.get("acquisition_source")
        if (
            acquisition_source.get("orcid") == self.get_user_orcid()
            and acquisition_source.get("email") == current_user.email
            and record.get("status") != "closed"
        ):
            return True
        return False

    def create_ticket(self, record, rt_template):
        control_number = record["control_number"]

        PROTOCOL = current_app.config["PREFERRED_URL_SCHEME"]
        SERVER = current_app.config["SERVER_NAME"]
        INSPIREHEP_URL = f"{PROTOCOL}://{SERVER}"
        JOB_DETAILS = f"{INSPIREHEP_URL}/jobs/{control_number}"
        JOB_EDIT = f"{INSPIREHEP_URL}/submissions/jobs/{control_number}"

        rt_queue = "JOBS"
        requestor = record["acquisition_source"]["email"] or record[
            "acquisition_source"
        ].get("name", "UNKNOWN")
        rt_template_context = {
            "job_url": JOB_DETAILS,
            "job_url_edit": JOB_EDIT,
            "hep_url": INSPIREHEP_URL,
        }
        async_create_ticket_with_template.delay(
            rt_queue,
            requestor,
            rt_template,
            rt_template_context,
            f"Job {control_number} has been submitted to the Jobs database",
            control_number,
        )