def delete(self, uuid1: str, uuid2: str, user: User) -> Response: graph = neo4j.get_instance() phenotype1 = graph.Phenotype.nodes.get_or_none(uuid=uuid1) if phenotype1 is None: raise NotFound(PHENOTYPE_NOT_FOUND) study = phenotype1.defined_in.single() self.verifyStudyAccess(study, user=user, error_type="Phenotype", read=False) phenotype2 = graph.Phenotype.nodes.get_or_none(uuid=uuid2) if phenotype2 is None: raise NotFound(PHENOTYPE_NOT_FOUND) study = phenotype2.defined_in.single() self.verifyStudyAccess(study, user=user, error_type="Phenotype", read=False) # [1] - FATHER -> [2] if phenotype1.father.is_connected(phenotype2): phenotype1.father.disconnect(phenotype2) # delete son relationship phenotype2.son.disconnect(phenotype1) # [] - MOTHER -> [2] elif phenotype1.mother.is_connected(phenotype2): phenotype1.mother.disconnect(phenotype2) # delete son relationship phenotype2.son.disconnect(phenotype1) # [1] <- FATHER - [2] _or_ [1] <- MOTHER - [2] elif phenotype1.son.is_connected(phenotype2): phenotype1.son.disconnect(phenotype2) # delete mother or father relationship if phenotype2.mother.is_connected(phenotype1): phenotype2.mother.disconnect(phenotype1) if phenotype2.father.is_connected(phenotype1): phenotype2.father.disconnect(phenotype1) self.log_event( self.events.modify, phenotype1, { "relationship": "removed", "target": phenotype2.uuid }, ) return self.empty_response()
def inject_user(endpoint: EndpointResource, user_id: str, user: User) -> Dict[str, Any]: target_user = endpoint.auth.get_user(user_id=user_id) if target_user is None: raise NotFound("This user cannot be found or you are not authorized") # Non admins (i.e. Staff users) are not allowed to target Admins if endpoint.auth.is_admin( target_user) and not endpoint.auth.is_admin(user): raise NotFound("This user cannot be found or you are not authorized") return {"target_user": target_user}
def post(self, uuid1: str, uuid2: str, user: User) -> Response: graph = neo4j.get_instance() if uuid1 == uuid2: raise BadRequest( f"Cannot set relationship between {uuid1} and itself") phenotype1 = graph.Phenotype.nodes.get_or_none(uuid=uuid1) if phenotype1 is None: raise NotFound(PHENOTYPE_NOT_FOUND) study = phenotype1.defined_in.single() self.verifyStudyAccess(study, user=user, error_type="Phenotype", read=False) phenotype2 = graph.Phenotype.nodes.get_or_none(uuid=uuid2) if phenotype2 is None: raise NotFound(PHENOTYPE_NOT_FOUND) study = phenotype2.defined_in.single() self.verifyStudyAccess(study, user=user, error_type="Phenotype", read=False) # check parent sex if phenotype2.sex == "male": relationship = "father" phenotype2.son.connect(phenotype1) phenotype1.father.connect(phenotype2) elif phenotype2.sex == "female": relationship = "mother" phenotype2.son.connect(phenotype1) phenotype1.mother.connect(phenotype2) self.log_event( self.events.modify, phenotype1, { "relationship": relationship, "target": phenotype2.uuid }, ) res = {"uuid": phenotype2.uuid, "name": phenotype2.name} return self.response(res)
def test_exceptions(self) -> None: with pytest.raises(RestApiException) as e: raise BadRequest("test") assert e.value.status_code == 400 with pytest.raises(RestApiException) as e: raise Unauthorized("test") assert e.value.status_code == 401 with pytest.raises(RestApiException) as e: raise Forbidden("test") assert e.value.status_code == 403 with pytest.raises(RestApiException) as e: raise NotFound("test") assert e.value.status_code == 404 with pytest.raises(RestApiException) as e: raise Conflict("test") assert e.value.status_code == 409 with pytest.raises(RestApiException) as e: raise ServerError("test") assert e.value.status_code == 500 with pytest.raises(RestApiException) as e: raise ServiceUnavailable("test") assert e.value.status_code == 503
def inject_group(endpoint: EndpointResource, group_id: str) -> Dict[str, Any]: group = endpoint.auth.get_group(group_id=group_id) if not group: raise NotFound("This group cannot be found") return {"group": group}
class Phenotypes(NIGEndpoint): # schema_expose = True labels = ["phenotype"] def link_hpo( self, graph: neo4j.NeoModel, phenotype: Any, hpo: List[str] ) -> List[str]: # disconnect all previous hpo for p in phenotype.hpo.all(): phenotype.hpo.disconnect(p) connected_hpo = [] for id in hpo: hpo = graph.HPO.nodes.get_or_none(hpo_id=id) if hpo: phenotype.hpo.connect(hpo) connected_hpo.append(id) return connected_hpo def link_geodata( self, graph: neo4j.NeoModel, phenotype: Any, geodata_uuid: str ) -> None: if previous := phenotype.birth_place.single(): phenotype.birth_place.disconnect(previous) geodata = graph.GeoData.nodes.get_or_none(uuid=geodata_uuid) if geodata is None: raise NotFound("This birth place cannot be found") phenotype.birth_place.connect(geodata)
def put( self, uuid: str, # should be an instance of neo4j.Study, # but typing is still not working with neomodel study: Any, # should be an instance of neo4j.Dataset, # but typing is still not working with neomodel dataset: Any, user: User, name: Optional[str] = None, description: Optional[str] = None, phenotype: Optional[str] = None, technical: Optional[str] = None, ) -> Response: kwargs = {} if phenotype: kwargs["phenotype"] = phenotype if previous := dataset.phenotype.single(): dataset.phenotype.disconnect(previous) if phenotype != "-1": phenotype = study.phenotypes.get_or_none(uuid=phenotype) if phenotype is None: # pragma: no cover raise NotFound(PHENOTYPE_NOT_FOUND) dataset.phenotype.connect(phenotype)
def send_file_streamed( filename: str, subfolder: Path, mime: Optional[str] = None, out_filename: Optional[str] = None, ) -> Response: Uploader.validate_upload_folder(subfolder) filename = secure_filename(filename) filepath = subfolder.joinpath(filename) if not filepath.is_file(): raise NotFound("The requested file does not exist") if mime is None: mime = Downloader.guess_mime_type(filepath) log.info("Providing streamed content from {} (mime={})", filepath, mime) response = Response( stream_with_context(Downloader.read_in_chunks(filepath)), mimetype=mime, ) if not out_filename: out_filename = filepath.name response.headers[ "Content-Disposition"] = f"attachment; filename={out_filename}" response.headers["Content-Length"] = filepath.stat().st_size return response
def inject_token(endpoint: EndpointResource, token_id: str) -> Dict[str, Any]: tokens = endpoint.auth.get_tokens(token_jti=token_id) if not tokens: raise NotFound("This token does not exist") return {"token": tokens[0]["token"]}
def post( self, uuid: str, name: str, description: str, # should be an instance of neo4j.Study, # but typing is still not working with neomodel study: Any, user: User, phenotype: Optional[str] = None, technical: Optional[str] = None, ) -> Response: graph = neo4j.get_instance() kwargs = {"name": name, "description": description} dataset = graph.Dataset(**kwargs).save() dataset.ownership.connect(user) dataset.parent_study.connect(study) if phenotype: kwargs["phenotype"] = phenotype phenotype = study.phenotypes.get_or_none(uuid=phenotype) if phenotype is None: # pragma: no cover raise NotFound(PHENOTYPE_NOT_FOUND) dataset.phenotype.connect(phenotype) if technical: kwargs["technical"] = technical technical = study.technicals.get_or_none(uuid=technical) if technical is None: # pragma: no cover raise NotFound(TECHMETA_NOT_FOUND) dataset.technical.connect(technical) path = self.getPath(user=user, dataset=dataset) try: path.mkdir(parents=True, exist_ok=False) # Almost impossible to have the same uuid was already used for an other study except FileExistsError as exc: # pragma: no cover dataset.delete() raise Conflict(str(exc)) self.log_event(self.events.create, dataset, kwargs) return self.response(dataset.uuid)
def verify_phenotype_access( endpoint: NIGEndpoint, uuid: str, user: User ) -> Dict[str, Any]: graph = neo4j.get_instance() phenotype = graph.Phenotype.nodes.get_or_none(uuid=uuid) if phenotype is None: raise NotFound(PHENOTYPE_NOT_FOUND) study = phenotype.defined_in.single() endpoint.verifyStudyAccess(study, user=user, error_type="Phenotype") return {"phenotype": phenotype, "study": study}
def verifyDatasetAccess( self, dataset: Dataset, user: User, error_type: str = "Dataset", read: bool = False, raiseError: bool = True, update_status: bool = False, ) -> bool: not_found = self.getError(error_type) if dataset is None: if raiseError: raise NotFound(not_found) else: return False # The owner has always access owner = dataset.ownership.single() if owner is None: log.warning("Dataset with null owner: %s" % dataset.uuid) return False # The owner has always access if owner == user: return True # A member of the some group of the owner, has always access for group in owner.belongs_to.all(): if group.members.is_connected(user): return True # An admin has always access for readonly or to modify dataset status if self.auth.is_admin(user): if read or update_status: return True if raiseError: raise NotFound(not_found) else: return False
def put(self, uuid: str, filename: str, user: User) -> Response: graph = neo4j.get_instance() # check permission dataset = graph.Dataset.nodes.get_or_none(uuid=uuid) self.verifyDatasetAccess(dataset, user=user) study = dataset.parent_study.single() self.verifyStudyAccess(study, user=user, error_type="Dataset") path = self.getPath(user=user, dataset=dataset) completed, response = self.chunk_upload(Path(path), filename) log.debug("check {}", response) if completed: # get the file file = None for f in dataset.files.all(): if f.name == filename: file = f if not file: raise NotFound(FILE_NOT_FOUND) # check the final size filepath = self.getPath(user=user, file=file) filesize = filepath.stat().st_size # check the final size if filesize != file.size: log.debug( "size expected: {},actual size: {}", file.size, filesize, ) file.delete() graph.db.commit() filepath.unlink() raise ServerError( "File has not been uploaded correctly: final size does not " "correspond to total size. Please try a new upload", ) # check the content of the file file_validation = validate_gzipped_fastq(filepath) if not file_validation[0]: # delete the file file.delete() graph.db.commit() filepath.unlink() raise BadRequest(file_validation[1]) file.status = "uploaded" file.save() self.log_event( self.events.create, file, {filename: f"Upload completed in dataset {uuid}"}, ) return response
def verifyStudyAccess( self, study: Study, user: User, error_type: str = "Study", read: bool = False, raiseError: bool = True, update_dataset_status: bool = False, ) -> bool: not_found = self.getError(error_type) if study is None: if raiseError: raise NotFound(not_found) else: return False owner = study.ownership.single() if owner is None: # pragma: no cover log.warning("Study with null owner: %s" % study.uuid) return False # The owner has always access if owner == user: return True # A member of the some group of the owner, has always access for group in owner.belongs_to.all(): if group.members.is_connected(user): return True # An admin has always access for readonly or to update dataset status if self.auth.is_admin(user): if read or update_dataset_status: return True if raiseError: raise NotFound(not_found) else: return False
def delete(self, token_id: str) -> Response: tokens = self.auth.get_tokens(token_jti=token_id) if not tokens: raise NotFound("This token does not exist") token = tokens[0] if not self.auth.invalidate_token(token=token["token"]): raise BadRequest(f"Failed token invalidation: '{token}'") return self.empty_response()
def delete(self, group_id: str) -> Response: group = self.auth.get_group(group_id=group_id) if not group: raise NotFound("This group cannot be found") self.auth.delete_group(group) self.log_event(self.events.delete, group) return self.empty_response()
def put(self, group_id: str, **kwargs: Any) -> Response: group = self.auth.get_group(group_id=group_id) if not group: raise NotFound("This group cannot be found") self.auth.db.update_properties(group, kwargs) self.auth.save_group(group) self.log_event(self.events.modify, group, kwargs) return self.empty_response()
def delete(self, user_id: str) -> Response: user = self.auth.get_user(user_id=user_id) if user is None: raise NotFound( "This user cannot be found or you are not authorized") self.auth.delete_user(user) self.log_event(self.events.delete, user) return self.empty_response()
def delete(self, uuid: str, user: User) -> Response: graph = neo4j.get_instance() phenotype = graph.Phenotype.nodes.get_or_none(uuid=uuid) if phenotype is None: raise NotFound(PHENOTYPE_NOT_FOUND) study = phenotype.defined_in.single() self.verifyStudyAccess(study, user=user, error_type="Phenotype") phenotype.delete() self.log_event(self.events.delete, phenotype) return self.empty_response()
def delete(self, uuid: str, user: User) -> Response: graph = neo4j.get_instance() techmeta = graph.TechnicalMetadata.nodes.get_or_none(uuid=uuid) if techmeta is None: raise NotFound(TECHMETA_NOT_FOUND) study = techmeta.defined_in.single() self.verifyStudyAccess(study, user=user, error_type="Technical Metadata") techmeta.delete() self.log_event(self.events.delete, techmeta) return self.empty_response()
def get(self, uuid: str, user: User) -> Response: graph = neo4j.get_instance() techmeta = graph.TechnicalMetadata.nodes.get_or_none(uuid=uuid) if not techmeta: raise NotFound(TECHMETA_NOT_FOUND) study = techmeta.defined_in.single() self.verifyStudyAccess( study, user=user, error_type="Technical Metadata", read=True ) self.log_event(self.events.access, techmeta) return self.response(techmeta)
def getPath( self, user: User, study: Optional[Study] = None, dataset: Optional[Dataset] = None, file: Optional[File] = None, read: bool = False, get_output_dir: bool = False, ) -> Path: if get_output_dir: dir_root = OUTPUT_ROOT else: dir_root = INPUT_ROOT group = user.belongs_to.single() if not group: raise NotFound("User group not found") if study: return dir_root.joinpath(group.uuid, study.uuid) if dataset: study = dataset.parent_study.single() if read: # it can be an admin so i have to get the group uuid of the dataset owner = dataset.ownership.single() group = owner.belongs_to.single() return dir_root.joinpath(group.uuid, study.uuid, dataset.uuid) if file: dataset = file.dataset.single() study = dataset.parent_study.single() if read: # it can be an admin so i have to get the group uuid of the dataset owner = dataset.ownership.single() group = owner.belongs_to.single() return dir_root.joinpath(group.uuid, study.uuid, dataset.uuid, file.name) raise BadRequest( # pragma: no cover "Can't get a path without a study specification")
def put(self, uuid: str, user: User, **kwargs: Any) -> Response: graph = neo4j.get_instance() techmeta = graph.TechnicalMetadata.nodes.get_or_none(uuid=uuid) if techmeta is None: raise NotFound(TECHMETA_NOT_FOUND) study = techmeta.defined_in.single() self.verifyStudyAccess(study, user=user, error_type="Technical Metadata") # kit = v.get("enrichment_kit", None) # if kit is not None and "value" in kit: # v["enrichment_kit"] = kit["value"] graph.update_properties(techmeta, kwargs) techmeta.save() self.log_event(self.events.modify, techmeta, kwargs) return self.empty_response()
def delete(self, uuid: str, user: User) -> Response: graph = neo4j.get_instance() file = graph.File.nodes.get_or_none(uuid=uuid) if file is None: raise NotFound(FILE_NOT_FOUND) dataset = file.dataset.single() self.verifyDatasetAccess(dataset, user=user, error_type="File") study = dataset.parent_study.single() self.verifyStudyAccess(study, user=user, error_type="File") path = self.getPath(user=user, file=file) file.delete() if path.exists(): path.unlink() self.log_event(self.events.delete, file) return self.empty_response()
def send_file_content( filename: str, subfolder: Path, mime: Optional[str] = None, ) -> Response: Uploader.validate_upload_folder(subfolder) filename = secure_filename(filename) filepath = subfolder.joinpath(filename) if not filepath.is_file(): raise NotFound("The requested file does not exist") if mime is None: mime = Downloader.guess_mime_type(filepath) log.info("Sending file content from {}", filepath) # This function is mainly used for displayable files like images and video # so that DO NOT SET as_attachment=True that would force the download return send_from_directory(subfolder, filename, mimetype=mime)
class AdminUsers(EndpointResource): depends_on = ["MAIN_LOGIN_ENABLE"] labels = ["admin"] private = True @decorators.auth.require_all(Role.ADMIN) @decorators.marshal_with(get_output_schema(), code=200) @decorators.endpoint( path="/admin/users", summary="List of users", responses={200: "List of users successfully retrieved"}, ) @decorators.endpoint( path="/admin/users/<user_id>", summary="Obtain information on a single user", responses={200: "User information successfully retrieved"}, ) def get(self, user_id: Optional[str] = None) -> Response: user = None users = None if not user_id: users = self.auth.get_users() elif user := self.auth.get_user(user_id=user_id): users = [user] if users is None: raise NotFound( "This user cannot be found or you are not authorized") if Connector.authentication_service == "neo4j": for u in users: u.belongs_to = u.belongs_to.single() if user: self.log_event(self.events.access, user) return self.response(users)
def post(self, **kwargs: Any) -> Response: roles: List[str] = kwargs.pop("roles", []) payload = kwargs.copy() group_id = kwargs.pop("group") email_notification = kwargs.pop("email_notification", False) unhashed_password = kwargs["password"] # If created by admins users must accept privacy at first login kwargs["privacy_accepted"] = False try: user = self.auth.create_user(kwargs, roles) self.auth.save_user(user) except DatabaseDuplicatedEntry as e: if Connector.authentication_service == "sqlalchemy": self.auth.db.session.rollback() raise Conflict(str(e)) group = self.auth.get_group(group_id=group_id) if not group: # Can't be reached because grup_id is prefiltered by marshmallow raise NotFound("This group cannot be found") # pragma: no cover self.auth.add_user_to_group(user, group) if email_notification and unhashed_password is not None: smtp_client = smtp.get_instance() send_notification(smtp_client, user, unhashed_password, is_update=False) self.log_event(self.events.create, user, payload) return self.response(user.uuid)
def post(self, user: User, **kwargs: Any) -> Response: roles: List[str] = kwargs.pop("roles", []) # The role is already refused by webards... This is an additional check # to improve the security, but can't be reached if not self.auth.is_admin( user) and Role.ADMIN in roles: # pragma: no cover raise Forbidden("This role is not allowed") payload = kwargs.copy() group_id = kwargs.pop("group") email_notification = kwargs.pop("email_notification", False) unhashed_password = kwargs["password"] # If created by admins users must accept privacy at first login kwargs["privacy_accepted"] = False user = self.auth.create_user(kwargs, roles) self.auth.save_user(user) group = self.auth.get_group(group_id=group_id) if not group: # Can't be reached because group_id is prefiltered by marshmallow raise NotFound("This group cannot be found") # pragma: no cover self.auth.add_user_to_group(user, group) if email_notification and unhashed_password is not None: notify_new_credentials_to_user(user, unhashed_password) self.log_event(self.events.create, user, payload) return self.response(user.uuid)
def get(self, uuid: str, user: User) -> Response: graph = neo4j.get_instance() file = graph.File.nodes.get_or_none(uuid=uuid) if file is None: raise NotFound(FILE_NOT_FOUND) dataset = file.dataset.single() self.verifyDatasetAccess(dataset, user=user, error_type="File", read=True) study = dataset.parent_study.single() self.verifyStudyAccess(study, user=user, error_type="File", read=True) # check if file exists in the folder path = self.getPath(user=user, dataset=dataset, read=True) if not path.joinpath(file.name).exists(): file.status = "unknown" file.save() else: # check if the status is correct if file.status == "unknown": filepath = self.getPath(user=user, file=file, read=True) if filepath.stat().st_size != file.size: file.status = "importing" else: file.status = "uploaded" file.save() self.log_event(self.events.access, file) return self.response(file)
def test_exceptions(self) -> None: try: raise BadRequest("test") except RestApiException as e: assert e.status_code == 400 try: raise Unauthorized("test") except RestApiException as e: assert e.status_code == 401 try: raise Forbidden("test") except RestApiException as e: assert e.status_code == 403 try: raise NotFound("test") except RestApiException as e: assert e.status_code == 404 try: raise Conflict("test") except RestApiException as e: assert e.status_code == 409 try: raise ServerError("test") except RestApiException as e: assert e.status_code == 500 try: raise ServiceUnavailable("test") except RestApiException as e: assert e.status_code == 503