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
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
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
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
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
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()
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)
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
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
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, )