Beispiel #1
0
    def verify_user_status(cls, user: User) -> None:

        if not user.is_active:

            cls.log_event(
                Events.refused_login,
                payload={
                    "username": user.email,
                    "motivation": "account not active"
                },
            )

            # Beware, frontend leverages on this exact message,
            # do not modified it without fix also on frontend side
            raise Forbidden("Sorry, this account is not active")

        now: Optional[datetime] = None

        if cls.DISABLE_UNUSED_CREDENTIALS_AFTER and user.last_login:

            if TESTING and user.email == cls.default_user:
                log.info(
                    "Default user can't be blocked for inactivity during tests"
                )
            else:
                now = get_now(user.last_login.tzinfo)
                if user.last_login + cls.DISABLE_UNUSED_CREDENTIALS_AFTER < now:
                    cls.log_event(
                        Events.refused_login,
                        payload={
                            "username": user.email,
                            "motivation": "account blocked due to inactivity",
                        },
                    )
                    raise Forbidden(
                        "Sorry, this account is blocked for inactivity")

        if user.expiration:
            # Reuse the now instance, if previously inizialized
            # tzinfo should be the same for both last_login and expiration fields
            if not now:
                now = get_now(user.expiration.tzinfo)

            if user.expiration < now:
                cls.log_event(
                    Events.refused_login,
                    payload={
                        "username": user.email,
                        "motivation": "account expired"
                    },
                )
                raise Forbidden("Sorry, this account is expired")
Beispiel #2
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
Beispiel #3
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")
Beispiel #4
0
def inject_token(endpoint: EndpointResource, token_id: str,
                 user: User) -> Dict[str, Any]:

    tokens = endpoint.auth.get_tokens(user=user)

    for token in tokens:
        if token["id"] == token_id:
            return {"token": token["token"]}

    raise Forbidden("Token not emitted for your account or does not exist")
Beispiel #5
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")
Beispiel #6
0
        def post(self, reset_email: str) -> Response:

            reset_email = reset_email.lower()

            self.auth.verify_blocked_username(reset_email)

            user = self.auth.get_user(username=reset_email)

            if user is None:
                raise Forbidden(
                    f"Sorry, {reset_email} is not recognized as a valid username",
                )

            self.auth.verify_user_status(user)

            title = get_project_configuration("project.title",
                                              default="Unkown title")

            reset_token, payload = self.auth.create_temporary_token(
                user, self.auth.PWD_RESET)

            server_url = get_frontend_url()

            rt = reset_token.replace(".", "+")

            uri = Env.get("RESET_PASSWORD_URI", "/public/reset")
            complete_uri = f"{server_url}{uri}/{rt}"

            smtp_client = smtp.get_instance()
            send_password_reset_link(smtp_client, complete_uri, title,
                                     reset_email)

            ##################
            # Completing the reset task
            self.auth.save_token(user,
                                 reset_token,
                                 payload,
                                 token_type=self.auth.PWD_RESET)

            msg = "We'll send instructions to the email provided if it's associated "
            msg += "with an account. Please check your spam/junk folder."

            self.log_event(self.events.reset_password_request, user=user)
            return self.response(msg)
Beispiel #7
0
        def post(self, reset_email: str) -> Response:

            reset_email = reset_email.lower()

            self.auth.verify_blocked_username(reset_email)

            user = self.auth.get_user(username=reset_email)

            if user is None:
                raise Forbidden(
                    f"Sorry, {reset_email} is not recognized as a valid username",
                )

            self.auth.verify_user_status(user)

            reset_token, payload = self.auth.create_temporary_token(
                user, self.auth.PWD_RESET)

            server_url = get_frontend_url()

            rt = reset_token.replace(".", "+")

            uri = Env.get("RESET_PASSWORD_URI", "/public/reset")
            complete_uri = f"{server_url}{uri}/{rt}"

            sent = send_password_reset_link(user, complete_uri, reset_email)

            if not sent:  # pragma: no cover
                raise ServiceUnavailable("Error sending email, please retry")

            ##################
            # Completing the reset task
            self.auth.save_token(user,
                                 reset_token,
                                 payload,
                                 token_type=self.auth.PWD_RESET)

            msg = "We'll send instructions to the email provided if it's associated "
            msg += "with an account. Please check your spam/junk folder."

            self.log_event(self.events.reset_password_request, user=user)
            return self.response(msg)
Beispiel #8
0
    def verify_blocked_username(self, username: str) -> None:

        # We do not count failed logins
        if self.MAX_LOGIN_ATTEMPTS <= 0:
            return

        # We register failed logins but the user does not reached it yet
        if self.count_failed_login(username) < self.MAX_LOGIN_ATTEMPTS:
            return

        self.log_event(
            Events.refused_login,
            payload={
                "username": username,
                "motivation": "account blocked due to too many failed logins",
            },
        )

        # Dear user, you have exceeded the limit!
        raise Forbidden("Sorry, this account is temporarily blocked "
                        "due to the number of failed login attempts.")
Beispiel #9
0
    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)
Beispiel #10
0
    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
Beispiel #11
0
    def put(self, user_id: str, target_user: User, user: User,
            **kwargs: Any) -> Response:

        if "password" in kwargs:
            unhashed_password = kwargs["password"]
            kwargs["password"] = BaseAuthentication.get_password_hash(
                kwargs["password"])
        else:
            unhashed_password = None

        payload = kwargs.copy()
        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")

        group_id = kwargs.pop("group", None)

        email_notification = kwargs.pop("email_notification", False)

        self.auth.link_roles(target_user, roles)

        userdata, extra_userdata = self.auth.custom_user_properties_pre(kwargs)

        prev_expiration = target_user.expiration

        # mypy correctly raises errors because update_properties is not defined
        # in generic Connector instances, but in this case this is an instance
        # of an auth db and their implementation always contains this method
        self.auth.db.update_properties(target_user, userdata)  # type: ignore

        self.auth.custom_user_properties_post(target_user, userdata,
                                              extra_userdata, self.auth.db)

        self.auth.save_user(target_user)

        if group_id is not None:
            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(target_user, group)

        if email_notification and unhashed_password is not None:
            notify_update_credentials_to_user(target_user, unhashed_password)

        if target_user.expiration:
            # Set expiration on a previously non-expiring account
            # or update the expiration by reducing the validity period
            # In both cases tokens should be invalited to prevent to have tokens
            # with TTL > account validity

            # dt_lower (alias for date_lower_than) is a comparison fn that ignores tz
            if not prev_expiration or dt_lower(target_user.expiration,
                                               prev_expiration):
                for token in self.auth.get_tokens(user=target_user):
                    # Invalidate all tokens with expiration after the account expiration
                    if dt_lower(target_user.expiration, token["expiration"]):
                        self.auth.invalidate_token(token=token["token"])

        self.log_event(self.events.modify, target_user, payload)

        return self.empty_response()
Beispiel #12
0
    def post(
        self,
        username: str,
        password: str,
        new_password: Optional[str] = None,
        password_confirm: Optional[str] = None,
        totp_code: Optional[str] = None,
    ) -> Response:

        username = username.lower()

        # ##################################################
        # Authentication control
        self.auth.verify_blocked_username(username)

        token, payload, user = self.auth.make_login(username, password)

        self.auth.verify_user_status(user)

        if self.auth.SECOND_FACTOR_AUTHENTICATION:

            if totp_code is None:
                message = self.check_password_validity(
                    user,
                    totp_authentication=self.auth.SECOND_FACTOR_AUTHENTICATION,
                )
                message["actions"].append("TOTP")
                message["errors"].append(
                    "You do not provided a valid verification code")
                if message["errors"]:
                    raise Forbidden(message)

            self.auth.verify_totp(user, totp_code)

        # ##################################################
        # If requested, change the password
        if new_password is not None and password_confirm is not None:

            pwd_changed = self.auth.change_password(user, password,
                                                    new_password,
                                                    password_confirm)

            if pwd_changed:
                password = new_password
                token, payload, user = self.auth.make_login(username, password)

        message = self.check_password_validity(
            user, totp_authentication=self.auth.SECOND_FACTOR_AUTHENTICATION)
        if message["errors"]:
            raise Forbidden(message)

        # Everything is ok, let's save authentication information

        now = datetime.now(pytz.utc)
        if user.first_login is None:
            user.first_login = now
        user.last_login = now
        self.auth.save_token(user, token, payload)

        self.auth.flush_failed_logins(username)

        return self.response(token)