Ejemplo n.º 1
0
    def upload(self, subfolder: Path, force: bool = False) -> Response:

        if "file" not in request.files:
            raise BadRequest("No files specified")

        myfile = request.files["file"]

        if not myfile.filename:  # pragma: no cover
            raise BadRequest("Invalid filename")

        if not self.allowed_file(myfile.filename):
            raise BadRequest("File extension not allowed")

        Uploader.validate_upload_folder(subfolder)

        if not subfolder.exists():
            subfolder.mkdir(parents=True, exist_ok=True)

        fname = secure_filename(myfile.filename)
        abs_file = subfolder.joinpath(fname)

        log.info("File request for [{}]({})", myfile, abs_file)

        if abs_file.exists():
            if not force:
                raise Conflict(
                    f"File '{fname}' already exists, use force parameter to overwrite"
                )
            abs_file.unlink()

        # Save the file
        try:
            myfile.save(abs_file)
            log.debug("Absolute file path should be '{}'", abs_file)
        except Exception as e:  # pragma: no cover
            log.error(e)
            raise ServiceUnavailable(
                "Permission denied: failed to write the file")

        # Check exists - but it is basicaly a test that cannot fail...
        # The has just been uploaded!
        if not abs_file.exists():  # pragma: no cover
            raise ServiceUnavailable("Unable to retrieve the uploaded file")

        ########################
        # ## Final response

        abs_file.chmod(DEFAULT_PERMISSIONS)

        # Default redirect is to 302 state, which makes client
        # think that response was unauthorized....
        # see http://dotnet.dzone.com/articles/getting-know-cross-origin

        return EndpointResource.response(
            {
                "filename": fname,
                "meta": self.get_file_metadata(abs_file)
            },
            code=200,
        )
Ejemplo n.º 2
0
    def make_login(self, username: str, password: str,
                   totp_code: Optional[str]) -> Tuple[str, Payload, User]:

        self.verify_blocked_username(username)

        try:
            user = self.get_user(username=username)
        except ValueError as e:  # pragma: no cover
            # SqlAlchemy can raise the following error:
            # A string literal cannot contain NUL (0x00) characters.
            log.error(e)
            raise BadRequest("Invalid input received")
        except Exception as e:  # pragma: no cover
            log.error("Unable to connect to auth backend\n[{}] {}", type(e), e)

            raise ServiceUnavailable("Unable to connect to auth backend")

        if user is None:
            self.register_failed_login(username, user=None)

            self.log_event(
                Events.failed_login,
                payload={"username": username},
                user=user,
            )

            raise Unauthorized("Invalid access credentials", is_warning=True)

        # Currently only credentials are allowed
        if user.authmethod != "credentials":  # pragma: no cover
            raise BadRequest("Invalid authentication method")

        if not self.verify_password(password, user.password):
            self.log_event(
                Events.failed_login,
                payload={"username": username},
                user=user,
            )
            self.register_failed_login(username, user=user)
            raise Unauthorized("Invalid access credentials", is_warning=True)

        self.verify_user_status(user)

        if self.SECOND_FACTOR_AUTHENTICATION and not totp_code:
            raise AuthMissingTOTP()

        if totp_code:
            self.verify_totp(user, totp_code)

        # Token expiration is capped by the user expiration date, if set
        payload, full_payload = self.fill_payload(user,
                                                  expiration=user.expiration)
        token = self.create_token(payload)

        self.save_login(username, user, failed=False)
        self.log_event(Events.login, user=user)
        return token, full_payload, user
Ejemplo n.º 3
0
    def upload(self,
               subfolder: Optional[str] = None,
               force: bool = False) -> Response:

        if "file" not in request.files:
            raise BadRequest("No files specified")

        myfile = request.files["file"]

        # Check file extension?
        if not self.allowed_file(myfile.filename):
            raise BadRequest("File extension not allowed")

        # Check file name
        fname = secure_filename(myfile.filename)
        abs_file = Uploader.absolute_upload_file(fname, subfolder)
        log.info("File request for [{}]({})", myfile, abs_file)

        if os.path.exists(abs_file):
            if not force:
                raise BadRequest(
                    f"File '{fname}' already exists, use force parameter to overwrite"
                )
            os.remove(abs_file)
            log.debug("Already exists, forced removal")

        # Save the file
        try:
            myfile.save(abs_file)
            log.debug("Absolute file path should be '{}'", abs_file)
        except Exception:  # pragma: no cover
            raise ServiceUnavailable(
                "Permission denied: failed to write the file")

        # Check exists - but it is basicaly a test that cannot fail...
        # The has just been uploaded!
        if not os.path.exists(abs_file):  # pragma: no cover
            raise ServiceUnavailable("Unable to retrieve the uploaded file")

        ########################
        # ## Final response

        # Default redirect is to 302 state, which makes client
        # think that response was unauthorized....
        # see http://dotnet.dzone.com/articles/getting-know-cross-origin

        return EndpointResource.response(
            {
                "filename": fname,
                "meta": self.get_file_metadata(abs_file)
            },
            code=200,
        )
Ejemplo n.º 4
0
    def put(self, token: str) -> Response:

        token = token.replace("%2B", ".")
        token = token.replace("+", ".")
        try:
            unpacked_token = self.auth.verify_token(
                token, raiseErrors=True, token_type=self.auth.ACTIVATE_ACCOUNT)

        # If token is expired
        except ExpiredSignatureError:
            raise BadRequest(
                "Invalid activation token: this request is expired", )

        # if token is not yet active
        except ImmatureSignatureError:
            raise BadRequest("Invalid activation token")

        # if token does not exist (or other generic errors)
        except BaseException:
            raise BadRequest("Invalid activation token")

        user = unpacked_token[3]
        self.auth.verify_blocked_username(user.email)

        # Recovering token object from jti
        jti = unpacked_token[2]
        token_obj = self.auth.get_tokens(token_jti=jti)
        # Cannot be tested, this is an extra test to prevent any unauthorized access...
        # but invalid tokens are already refused above, with auth.verify_token
        if len(token_obj) == 0:  # pragma: no cover
            raise BadRequest(
                "Invalid activation token: this request is no longer valid")

        # If user logged is already active, invalidate the token
        if user.is_active:
            self.auth.invalidate_token(token)
            raise BadRequest(
                "Invalid activation token: this request is no longer valid")

        # The activation token is valid, do something
        user.is_active = True
        self.auth.save_user(user)

        # Bye bye token (activation tokens are valid only once)
        self.auth.invalidate_token(token)

        self.log_event(self.events.activation, user=user, target=user)

        return self.response("Account activated")
Ejemplo n.º 5
0
 def get(self, test: str) -> Response:
     self.neo4j = neo4j.get_instance()
     try:
         if test == "1":
             log.info("First Test")
             self.neo4j.cypher("MATCH (n) RETURN n LIMIT 1")
         elif test == "2":
             log.info("Second Test")
             self.neo4j.cypher("MATCH (n) RETURN n with a syntax error")
         # This test will verify that a timestamped node when saved
         # Automatically update the modified attribute
         elif test == "3":
             data = {}
             n = self.neo4j.JustATest(p_str="")
             n.save()
             data["created"] = n.created
             data["modified1"] = n.modified
             n.save()
             data["modified2"] = n.modified
             return self.response(data)
         else:
             log.info("No Test")
     except Exception as e:
         raise BadRequest(str(e))
     return self.response({"val": 1})
Ejemplo n.º 6
0
    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
Ejemplo n.º 7
0
    def get(self, query: str, user: User) -> Response:

        # Chars whitelist: letters, numbers, space, colon and hyphen
        if not re.match("^[a-zA-Z0-9 :-]+$", query):
            raise BadRequest("Invalid HPO query")

        cypher = "MATCH (hpo:HPO)"

        regexp = f"(?i).*{query}.*"
        if query.startswith("HP:") and len(query) >= 4:
            cypher += " WHERE hpo.hpo_id =~ $regexp"
        else:
            cypher += " WHERE hpo.label =~ $regexp"
        cypher += " RETURN hpo ORDER BY hpo.hpo_id DESC"
        cypher += " LIMIT 50"

        graph = neo4j.get_instance()
        result = graph.cypher(cypher, regexp=regexp)

        data: List[Dict[str, str]] = []
        for row in result:
            hpo = graph.HPO.inflate(row[0])
            data.append({"hpo_id": hpo.hpo_id, "label": hpo.label})

        return self.response(data)
Ejemplo n.º 8
0
    def patch(
        self,
        uuid: str,
        study: Any,
        dataset: Any,
        status: str,
        user: User,
    ) -> Response:

        # patch can only be done on dataset with status UPLOAD COMPLETED
        if (dataset.status and dataset.status != "UPLOAD COMPLETED"
                and not self.auth.is_admin(user)):
            raise BadRequest(
                f"The status of dataset {dataset.name} cannot be modified")

        if status == "-1":
            dataset.status = None
        else:
            dataset.status = status

        dataset.save()

        self.log_event(self.events.modify, dataset, {"status": status})

        return self.empty_response()
Ejemplo n.º 9
0
    def make_login(self, username: str, password: str) -> Tuple[str, Payload, User]:
        """ The method which will check if credentials are good to go """

        try:
            user = self.get_user(username=username)
        except ValueError as e:  # pragma: no cover
            # SqlAlchemy can raise the following error:
            # A string literal cannot contain NUL (0x00) characters.
            log.error(e)
            raise BadRequest("Invalid input received")
        except BaseException as e:  # pragma: no cover
            log.error("Unable to connect to auth backend\n[{}] {}", type(e), e)

            raise ServiceUnavailable("Unable to connect to auth backend")

        if user is None:
            self.register_failed_login(username)

            self.log_event(
                Events.failed_login,
                payload={"username": username},
                user=user,
            )

            raise Unauthorized("Invalid access credentials", is_warning=True)

        # Check if Oauth2 is enabled
        if user.authmethod != "credentials":  # pragma: no cover
            raise BadRequest("Invalid authentication method")

        # New hashing algorithm, based on bcrypt
        if self.verify_password(password, user.password):
            # Token expiration is capped by the user expiration date, if set
            payload, full_payload = self.fill_payload(user, expiration=user.expiration)
            token = self.create_token(payload)

            self.log_event(Events.login, user=user)
            return token, full_payload, user

        self.log_event(
            Events.failed_login,
            payload={"username": username},
            user=user,
        )
        self.register_failed_login(username)
        raise Unauthorized("Invalid access credentials", is_warning=True)
Ejemplo n.º 10
0
    def post(self, channel: str) -> Response:

        try:
            # in_events = decode_websocket_events(request.get_data())
            in_events = decode_websocket_events(request.get_data())
        except ValueError as e:
            log.error(e)
            raise BadRequest("Cannot decode websocket request: invalid format")

        if in_events is None or len(in_events) <= 0:
            log.error("Websocket request: {}", request.data)
            raise BadRequest(
                "Cannot decode websocket request: invalid in_event")
        in_events = in_events[0]

        event_type = None

        try:
            event_type = in_events.type
        except BaseException as e:  # pragma: no cover
            log.error(e)
            raise BadRequest("Cannot decode websocket request: invalid type")

        if event_type is None:  # pragma: no cover
            raise BadRequest("Cannot decode websocket request, no event type")

        out_events = []
        if event_type == "OPEN":
            ctrl_msg = websocket_control_message("subscribe",
                                                 {"channel": channel})
            out_events.append(WebSocketEvent("OPEN"))
            out_events.append(WebSocketEvent(
                "TEXT",
                f"c:{ctrl_msg}",
            ))
            headers = {"Sec-WebSocket-Extensions": "grip"}
            resp = FlaskResponse(
                encode_websocket_events(out_events),
                mimetype="application/websocket-events",
                headers=headers,
            )
            return resp

        log.error("Unknkown event type: {}", event_type)
        raise BadRequest("Cannot understand websocket request")
Ejemplo n.º 11
0
    def wrapper(*args, **kwargs):

        try:
            return func(*args, **kwargs)
        except DatabaseDuplicatedEntry as e:
            # already catched and parser, raise up
            raise (e)
        except DoesNotExist as e:
            raise (e)
        except CypherSyntaxError as e:
            raise (e)
        except UniqueProperty as e:

            t = "already exists with label"
            m = re.search(
                fr"Node\([0-9]+\) {t} `(.+)` and property `(.+)` = '(.+)'",
                str(e))

            if m:
                node = m.group(1)
                prop = m.group(2)
                val = m.group(3)
                error = f"A {node.title()} already exists with {prop}: {val}"
                raise DatabaseDuplicatedEntry(error)

            # Can't be tested, should never happen except in case of new neo4j version
            log.error("Unrecognized error message: {}", e)  # pragma: no cover
            raise DatabaseDuplicatedEntry(
                "Duplicated entry")  # pragma: no cover
        except RequiredProperty as e:

            # message = property 'xyz' on objects of class XYZ

            message = str(e)
            m = re.search(r"property '(.*)' on objects of class (.*)", str(e))
            if m:
                missing_property = m.group(1)
                model = m.group(2)
                message = f"Missing property {missing_property} required by {model}"

            raise BadRequest(message)
        except DeflateError as e:
            log.warning(e)
            return None

        except ServiceUnavailable:  # pragma: no cover
            # refresh_connection()
            raise

        # Catched in case of re-raise for example RequiredProperty -> BadRequest
        except RestApiException:  # pragma: no cover
            raise

        except Exception as e:  # pragma: no cover
            log.critical("Raised unknown exception: {}", type(e))
            raise e
Ejemplo n.º 12
0
    def delete(self, token_id: str, token: str, user: User) -> Response:

        if self.auth.invalidate_token(token=token):
            return self.empty_response()

        # Added just to make very sure, but it can never happen because
        # invalidate_token can only fail if the token is invalid
        # since this is an authenticated endpoint the token is already verified
        raise BadRequest(
            f"Failed token invalidation: {token}")  # pragma: no cover
Ejemplo n.º 13
0
    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
Ejemplo n.º 14
0
    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()
Ejemplo n.º 15
0
    def custom_user_properties_post(user, userdata, extra_userdata, db):
        try:
            mem.customizer.custom_user_properties_post(
                user, userdata, extra_userdata, db
            )
        except (RestApiException, DatabaseDuplicatedEntry):  # pragma: no cover
            raise
        except BaseException as e:  # pragma: no cover
            raise BadRequest(f"Unable to post-customize user properties: {e}")

        return userdata
Ejemplo n.º 16
0
    def change_password(
        self,
        user: User,
        password: str,
        new_password: Optional[str],
        password_confirm: Optional[str],
    ) -> bool:

        if new_password is None:
            raise BadRequest("Missing new password")

        if password_confirm is None:
            raise BadRequest("Missing password confirmation")

        if new_password != password_confirm:
            raise Conflict("Your password doesn't match the confirmation")

        check, msg = self.verify_password_strength(
            pwd=new_password,
            old_pwd=password,
            email=user.email,
            name=user.name,
            surname=user.surname,
        )

        if not check:
            raise Conflict(msg)

        user.password = BaseAuthentication.get_password_hash(new_password)
        user.last_password_change = datetime.now(pytz.utc)
        self.save_user(user)

        self.log_event(Events.change_password, user=user)

        for token in self.get_tokens(user=user):
            try:
                self.invalidate_token(token=token["token"])
            except Exception as e:  # pragma: no cover
                log.critical("Failed to invalidate token {}", e)

        return True
Ejemplo n.º 17
0
    def custom_user_properties_post(user: User, userdata: Props,
                                    extra_userdata: Props,
                                    db: "Connector") -> Props:
        try:
            mem.customizer.custom_user_properties_post(user, userdata,
                                                       extra_userdata, db)
        except RestApiException:  # pragma: no cover
            raise
        except Exception as e:  # pragma: no cover
            raise BadRequest(f"Unable to post-customize user properties: {e}")

        return userdata
Ejemplo n.º 18
0
    def chunk_upload(
            self,
            upload_dir: str,
            filename: str,
            chunk_size: Optional[int] = None) -> Tuple[bool, Response]:
        filename = secure_filename(filename)

        range_header = request.headers.get("Content-Range", "")
        total_length, start, stop = self.parse_content_range(range_header)

        if total_length is None or start is None or stop is None:
            raise BadRequest("Invalid request")

        completed = stop >= total_length

        # Default chunk size, put this somewhere
        if chunk_size is None:
            chunk_size = 1048576

        file_path = os.path.join(upload_dir, filename)
        with open(file_path, "ab") as f:
            while True:
                chunk = request.stream.read(chunk_size)
                if not chunk:
                    break
                f.seek(start)
                f.write(chunk)

        if completed:
            return (
                completed,
                EndpointResource.response(
                    {
                        "filename": filename,
                        "meta": self.get_file_metadata(file_path)
                    },
                    code=200,
                ),
            )

        return (
            completed,
            EndpointResource.response(
                "partial",
                headers={
                    "Access-Control-Expose-Headers": "Range",
                    "Range": "0-{}".format(stop - 1),
                },
                code=206,
            ),
        )
Ejemplo n.º 19
0
    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)
Ejemplo n.º 20
0
        def get(self, size: str) -> Response:

            size_int = 0
            try:
                size_int = int(size)
            except Exception as e:
                log.error("Invalid int value {} -> {}", size, e)
                raise BadRequest("Invalid numeric value {size}")

            if size_int <= 0:
                raise RestApiException("Invalid size", status_code=416)

            # Just to prevent super giant responses
            return self.response("a" * min(size_int, 1_000_000))
Ejemplo n.º 21
0
    def custom_user_properties_pre(
        userdata: Dict[str, Any]
    ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
        try:
            userdata, extradata = mem.customizer.custom_user_properties_pre(userdata)
        except (RestApiException, DatabaseDuplicatedEntry):  # pragma: no cover
            raise
        except BaseException as e:  # pragma: no cover
            raise BadRequest(f"Unable to pre-customize user properties: {e}")

        if "email" in userdata:
            userdata["email"] = userdata["email"].lower()

        return userdata, extradata
Ejemplo n.º 22
0
        def post(self, data: str) -> Response:

            # Special value! This will try to create a group without shortname
            # A BadRequest will be raised due to the missing property
            if data == "400":
                group = self.auth.create_group({"fullname": data})
                # The following two lines will be never executed because the
                # create group above is intended to fail due to the missing property
                self.auth.save_group(group)  # pragma: no cover
                return self.response("0")  # pragma: no cover

            # This is just to limit schemathesis to create troubles :-)
            if not re.match(r"^[A-Za-z]+$", data):
                raise BadRequest(f"Invalid input name {data}")

            # Only DatabaseDuplicatedEntry will be raised by this endpoint
            # Any other exceptions will be suppressed. This will ensure that
            # DatabaseDuplicatedEntry is raised and no others.
            # As a side effect this endpoint will modifiy the fullname of the default
            # Group if the exception is not raised. Otherwise this modification will
            # be undo by the database_transaction decorator
            try:

                default_group = self.auth.get_group(name=DEFAULT_GROUP_NAME)

                if default_group is None:  # pragma: no cover
                    raise ServerError("Default group is missing")

                default_group.fullname = f"{default_group.fullname}_exteded"
                # Don't commit with alchemy or the transaction can't be rollbacked
                if Connector.authentication_service != "sqlalchemy":
                    self.auth.save_group(default_group)

                # This can fail if data is a duplicate of a already created group
                # In this case a DatabaseDuplicatedEntry excepton will be raised and the
                # database_transaction decorator will undo the change on the default grp
                group = self.auth.create_group({
                    "shortname": data,
                    "fullname": data
                })
                # Don't commit with alchemy or the transaction can't be rollbacked
                if Connector.authentication_service != "sqlalchemy":
                    self.auth.save_group(group)

                return self.response("1")
            except DatabaseDuplicatedEntry:
                raise
            except Exception:  # pragma: no cover
                return self.response("0")
Ejemplo n.º 23
0
    def validate_upload_folder(path: Path) -> None:

        if "\x00" in str(path):
            raise BadRequest("Invalid null byte in subfolder parameter")

        if path != path.resolve():
            log.error("Invalid path: path is relative or contains double-dots")
            raise Forbidden("Invalid file path")

        if path != DATA_PATH and DATA_PATH not in path.parents:
            log.error(
                "Invalid root path: {} is expected to be a child of {}",
                path,
                DATA_PATH,
            )
            raise Forbidden("Invalid file path")
Ejemplo n.º 24
0
    def download(
        filename: Optional[str] = None,
        subfolder: Optional[str] = None,
        mime: Optional[str] = None,
    ) -> Response:

        if filename is None:
            raise BadRequest("No filename specified to download")

        filename = secure_filename(filename)
        path = Uploader.absolute_upload_file(filename,
                                             subfolder=subfolder,
                                             onlydir=True)
        log.info("Starting transfer of '{}' from '{}'", filename, path)

        return send_from_directory(path, filename, mimetype=mime)
Ejemplo n.º 25
0
    def post(self, token: str) -> Response:

        token = token.replace("%2B", ".")
        token = token.replace("+", ".")

        try:
            # valid, token, jti, user
            _, _, jti, user = self.auth.verify_token(
                token, raiseErrors=True, token_type=self.auth.UNLOCK_CREDENTIALS
            )

        # If token is expired
        except ExpiredSignatureError:
            raise BadRequest(
                "Invalid unlock token: this request is expired",
            )

        # if token is not active yet
        except ImmatureSignatureError:
            raise BadRequest("Invalid unlock token")

        # if token does not exist (or other generic errors)
        except Exception:
            raise BadRequest("Invalid unlock token")

        if user is None:  # pragma: no cover
            raise BadRequest("Invalid unlock token")

        # Recovering token object from jti
        token_obj = self.auth.get_tokens(token_jti=jti)
        # Cannot be tested, this is an extra test to prevent any unauthorized access...
        # but invalid tokens are already refused above, with auth.verify_token
        if len(token_obj) == 0:  # pragma: no cover
            raise BadRequest("Invalid unlock token: this request is no longer valid")

        # If credentials are no longer locked, invalidate the token
        if self.auth.count_failed_login(user.email) < self.auth.MAX_LOGIN_ATTEMPTS:
            self.auth.invalidate_token(token)
            raise BadRequest("Invalid unlock token: this request is no longer valid")

        # The unlock token is valid, do something
        self.auth.flush_failed_logins(user.email)
        log.info(
            "{} provided a valid unlock token and credentials block is now revoked",
            user.email,
        )

        # Bye bye token (unlock tokens are valid only once)
        self.auth.invalidate_token(token)

        self.log_event(self.events.login_unlock, user=user, target=user)

        return self.response("Credentials unlocked")
Ejemplo n.º 26
0
    def post(self, uuid: str, name: str, user: User,
             **kwargs: Any) -> Response:

        # check permissions
        graph = neo4j.get_instance()
        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)

        # check if the filename is correct
        name_pattern = r"([a-zA-Z0-9_-]+)_(R[12]).fastq.gz"
        if not re.match(name_pattern, name):
            raise BadRequest(
                "Filename does not follow the correct naming convention: "
                "SampleName_R1/R2.fastq.gz")

        # set the allowed file format
        self.set_allowed_exts(["gz"])

        properties = {
            "name": name,
            "size": kwargs["size"],
            # Currently fixed
            "type": "fastq.gz",
            "status": "importing",
        }

        file = graph.File(**properties).save()

        file.dataset.connect(dataset)

        self.log_event(
            self.events.create,
            file,
            {
                "operation":
                f"Accepted upload for {name} file in {uuid} dataset"
            },
        )

        return self.init_chunk_upload(Path(path), name, force=False)
Ejemplo n.º 27
0
    def wrapper(self, *args, **kwargs):

        from neomodel.exceptions import RequiredProperty

        try:
            return func(self, *args, **kwargs)

        except DatabaseDuplicatedEntry as e:

            log.critical("boh")

            raise Conflict(str(e))

        except RequiredProperty as e:

            log.critical("Missing required")

            raise BadRequest(e)
Ejemplo n.º 28
0
    def delete(self, token_id: str) -> Response:

        user = self.get_user()
        tokens = self.auth.get_tokens(user=user)

        for token in tokens:
            if token["id"] != token_id:
                continue

            if self.auth.invalidate_token(token=token["token"]):
                return self.empty_response()

            # Added just to make very sure, but it can never happen because
            # invalidate_token can only fail if the token is invalid
            # since this is an authenticated endpoint the token is already verified
            raise BadRequest(f"Failed token invalidation: {token}")  # pragma: no cover

        raise Forbidden("Token not emitted for your account or does not exist")
Ejemplo n.º 29
0
    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")
Ejemplo n.º 30
0
    def init_chunk_upload(self,
                          upload_dir: Path,
                          filename: str,
                          force: bool = True) -> Response:

        if not self.allowed_file(filename):
            raise BadRequest("File extension not allowed")

        Uploader.validate_upload_folder(upload_dir)

        if not upload_dir.exists():
            upload_dir.mkdir(parents=True, exist_ok=True)

        filename = secure_filename(filename)

        file_path = upload_dir.joinpath(filename)

        if file_path.exists():
            log.warning("File already exists")
            if force:
                file_path.unlink()
                log.debug("Forced removal")
            else:
                raise Conflict(f"File '{filename}' already exists")

        file_path.touch()

        host = get_backend_url()
        url = f"{host}{request.path}/{filename}"

        log.info("Upload initialized on url: {}", url)

        return EndpointResource.response(
            "",
            headers={
                "Access-Control-Expose-Headers": "Location",
                "Location": url
            },
            code=201,
        )