fastlifeweb 0.22.0__py3-none-any.whl → 0.23.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- CHANGELOG.md +14 -0
- fastlife/__init__.py +29 -3
- fastlife/adapters/fastapi/request.py +4 -4
- fastlife/adapters/fastapi/routing/route.py +1 -1
- fastlife/config/configurator.py +3 -3
- fastlife/domain/model/request.py +5 -2
- fastlife/domain/model/security_policy.py +78 -5
- fastlife/service/check_permission.py +2 -0
- fastlife/service/locale_negociator.py +4 -2
- fastlife/service/security_policy.py +67 -20
- {fastlifeweb-0.22.0.dist-info → fastlifeweb-0.23.0.dist-info}/METADATA +1 -1
- {fastlifeweb-0.22.0.dist-info → fastlifeweb-0.23.0.dist-info}/RECORD +15 -15
- {fastlifeweb-0.22.0.dist-info → fastlifeweb-0.23.0.dist-info}/WHEEL +0 -0
- {fastlifeweb-0.22.0.dist-info → fastlifeweb-0.23.0.dist-info}/entry_points.txt +0 -0
- {fastlifeweb-0.22.0.dist-info → fastlifeweb-0.23.0.dist-info}/licenses/LICENSE +0 -0
    
        CHANGELOG.md
    CHANGED
    
    | @@ -1,3 +1,17 @@ | |
| 1 | 
            +
            ## 0.23.0  - Released on 2024-12-04
         | 
| 2 | 
            +
            * Update Request type.
         | 
| 3 | 
            +
              * Breaking changes: Request[TUser, TRegistry] -> Request[TRegistry, TIdentity, TClaimedIdentity].
         | 
| 4 | 
            +
            * Update SecurityPolicy, designed for MFA by default.
         | 
| 5 | 
            +
              * Breaking changes: new abstract method added. build_authentication_state.
         | 
| 6 | 
            +
              * Breaking changes: there is no more get_authenticated_userid method.
         | 
| 7 | 
            +
              * The identity method is not abstract anymore, result comes from the build_authentication_state.
         | 
| 8 | 
            +
              * New method get_authentication_state, claimed_identity and pre_remember.
         | 
| 9 | 
            +
            * Add a AbstractNoMFASecurityPolicy that build a AbstractSecurityPolicy without TClaimedIdentity as None.
         | 
| 10 | 
            +
            * New ACL type added to raise 401 errors due to missing MFA which may not be same url as tu login/password.
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            ## 0.22.1  - Released on 2024-11-27
         | 
| 13 | 
            +
            * Improve Request typing
         | 
| 14 | 
            +
             | 
| 1 15 | 
             
            ## 0.22.0  - Released on 2024-11-23
         | 
| 2 16 | 
             
            * Add a way to add fatal errors on form in order to display an error block.
         | 
| 3 17 | 
             
            * The localizer can be called gettext in the depency in order to simple translation.
         | 
    
        fastlife/__init__.py
    CHANGED
    
    | @@ -7,7 +7,13 @@ from fastapi.responses import RedirectResponse | |
| 7 7 |  | 
| 8 8 | 
             
            from .adapters.fastapi.form import form_model
         | 
| 9 9 | 
             
            from .adapters.fastapi.localizer import Localizer
         | 
| 10 | 
            -
            from .adapters.fastapi.request import  | 
| 10 | 
            +
            from .adapters.fastapi.request import (
         | 
| 11 | 
            +
                AnyRequest,
         | 
| 12 | 
            +
                Registry,
         | 
| 13 | 
            +
                Request,
         | 
| 14 | 
            +
                get_registry,
         | 
| 15 | 
            +
                get_request,
         | 
| 16 | 
            +
            )
         | 
| 11 17 | 
             
            from .config import (
         | 
| 12 18 | 
             
                Configurator,
         | 
| 13 19 | 
             
                GenericConfigurator,
         | 
| @@ -21,17 +27,27 @@ from .domain.model.form import FormModel | |
| 21 27 | 
             
            from .domain.model.request import GenericRequest
         | 
| 22 28 | 
             
            from .domain.model.security_policy import (
         | 
| 23 29 | 
             
                Allowed,
         | 
| 30 | 
            +
                Anonymous,
         | 
| 31 | 
            +
                Authenticated,
         | 
| 32 | 
            +
                AuthenticationState,
         | 
| 24 33 | 
             
                Denied,
         | 
| 25 34 | 
             
                Forbidden,
         | 
| 26 35 | 
             
                HasPermission,
         | 
| 36 | 
            +
                NoMFAAuthenticationState,
         | 
| 37 | 
            +
                PendingMFA,
         | 
| 38 | 
            +
                PreAuthenticated,
         | 
| 27 39 | 
             
                Unauthenticated,
         | 
| 28 40 | 
             
                Unauthorized,
         | 
| 29 41 | 
             
            )
         | 
| 30 42 | 
             
            from .domain.model.template import JinjaXTemplate
         | 
| 31 43 |  | 
| 32 44 | 
             
            # from .request.form_data import model
         | 
| 33 | 
            -
            from .service.registry import DefaultRegistry, GenericRegistry
         | 
| 34 | 
            -
            from .service.security_policy import  | 
| 45 | 
            +
            from .service.registry import DefaultRegistry, GenericRegistry, TRegistry, TSettings
         | 
| 46 | 
            +
            from .service.security_policy import (
         | 
| 47 | 
            +
                AbstractNoMFASecurityPolicy,
         | 
| 48 | 
            +
                AbstractSecurityPolicy,
         | 
| 49 | 
            +
                InsecurePolicy,
         | 
| 50 | 
            +
            )
         | 
| 35 51 | 
             
            from .settings import Settings
         | 
| 36 52 |  | 
| 37 53 | 
             
            __all__ = [
         | 
| @@ -47,6 +63,9 @@ __all__ = [ | |
| 47 63 | 
             
                "resource_view",
         | 
| 48 64 | 
             
                "Configurator",
         | 
| 49 65 | 
             
                "DefaultRegistry",
         | 
| 66 | 
            +
                "TSettings",
         | 
| 67 | 
            +
                "TRegistry",
         | 
| 68 | 
            +
                "get_registry",
         | 
| 50 69 | 
             
                # Form
         | 
| 51 70 | 
             
                "FormModel",
         | 
| 52 71 | 
             
                "form_model",
         | 
| @@ -60,13 +79,20 @@ __all__ = [ | |
| 60 79 | 
             
                "RedirectResponse",
         | 
| 61 80 | 
             
                # Security
         | 
| 62 81 | 
             
                "AbstractSecurityPolicy",
         | 
| 82 | 
            +
                "AbstractNoMFASecurityPolicy",
         | 
| 63 83 | 
             
                "HasPermission",
         | 
| 64 84 | 
             
                "Unauthenticated",
         | 
| 85 | 
            +
                "PreAuthenticated",
         | 
| 65 86 | 
             
                "Allowed",
         | 
| 66 87 | 
             
                "Denied",
         | 
| 67 88 | 
             
                "Unauthorized",
         | 
| 68 89 | 
             
                "Forbidden",
         | 
| 69 90 | 
             
                "InsecurePolicy",
         | 
| 91 | 
            +
                "Anonymous",
         | 
| 92 | 
            +
                "PendingMFA",
         | 
| 93 | 
            +
                "Authenticated",
         | 
| 94 | 
            +
                "AuthenticationState",
         | 
| 95 | 
            +
                "NoMFAAuthenticationState",
         | 
| 70 96 | 
             
                # Template
         | 
| 71 97 | 
             
                "JinjaXTemplate",
         | 
| 72 98 | 
             
                # i18n
         | 
| @@ -9,18 +9,18 @@ from fastlife.domain.model.request import GenericRequest | |
| 9 9 | 
             
            from fastlife.service.registry import DefaultRegistry
         | 
| 10 10 |  | 
| 11 11 |  | 
| 12 | 
            -
            def get_request(request: FastAPIRequest) -> GenericRequest[Any]:
         | 
| 12 | 
            +
            def get_request(request: FastAPIRequest) -> GenericRequest[Any, Any, Any]:
         | 
| 13 13 | 
             
                """Return the Fastlife Request object."""
         | 
| 14 14 | 
             
                return request  # type: ignore
         | 
| 15 15 |  | 
| 16 16 |  | 
| 17 | 
            -
            Request = Annotated[GenericRequest[DefaultRegistry], Depends(get_request)]
         | 
| 17 | 
            +
            Request = Annotated[GenericRequest[DefaultRegistry, Any, Any], Depends(get_request)]
         | 
| 18 18 | 
             
            """A request that is associated to the default registry."""
         | 
| 19 19 | 
             
            # FastAPI handle its Request objects using a lenient_issubclass,
         | 
| 20 | 
            -
            # basically a issubclass(Request),  | 
| 20 | 
            +
            # basically a issubclass(Request), does not work with Generic[T].
         | 
| 21 21 |  | 
| 22 22 |  | 
| 23 | 
            -
            AnyRequest = Annotated[GenericRequest[Any], Depends(get_request)]
         | 
| 23 | 
            +
            AnyRequest = Annotated[GenericRequest[Any, Any, Any], Depends(get_request)]
         | 
| 24 24 | 
             
            """A request version that is associated to the any registry."""
         | 
| 25 25 |  | 
| 26 26 |  | 
| @@ -41,7 +41,7 @@ class Route(APIRoute): | |
| 41 41 | 
             
                    orig_route_handler = super().get_route_handler()
         | 
| 42 42 |  | 
| 43 43 | 
             
                    async def route_handler(request: StarletteRequest) -> Response:
         | 
| 44 | 
            -
                        req = GenericRequest(self._registry, request)
         | 
| 44 | 
            +
                        req = GenericRequest[Any, Any, Any](self._registry, request)
         | 
| 45 45 | 
             
                        return await orig_route_handler(req)
         | 
| 46 46 |  | 
| 47 47 | 
             
                    return route_handler
         | 
    
        fastlife/config/configurator.py
    CHANGED
    
    | @@ -144,7 +144,7 @@ class GenericConfigurator(Generic[TRegistry]): | |
| 144 144 | 
             
                    self._route_prefix: str = ""
         | 
| 145 145 | 
             
                    self._routers: dict[str, Router] = defaultdict(Router)
         | 
| 146 146 | 
             
                    self._security_policies: dict[
         | 
| 147 | 
            -
                        str, type[AbstractSecurityPolicy[Any, TRegistry]]
         | 
| 147 | 
            +
                        str, type[AbstractSecurityPolicy[Any, Any, TRegistry]]
         | 
| 148 148 | 
             
                    ] = {}
         | 
| 149 149 |  | 
| 150 150 | 
             
                    self._registered_permissions: set[str] = set()
         | 
| @@ -322,7 +322,7 @@ class GenericConfigurator(Generic[TRegistry]): | |
| 322 322 | 
             
                    return self
         | 
| 323 323 |  | 
| 324 324 | 
             
                def set_security_policy(
         | 
| 325 | 
            -
                    self, security_policy: "type[AbstractSecurityPolicy[Any,  | 
| 325 | 
            +
                    self, security_policy: "type[AbstractSecurityPolicy[TRegistry, Any, Any]]"
         | 
| 326 326 | 
             
                ) -> Self:
         | 
| 327 327 | 
             
                    """
         | 
| 328 328 | 
             
                    Set a security policy for the application.
         | 
| @@ -594,7 +594,7 @@ class GenericConfigurator(Generic[TRegistry]): | |
| 594 594 | 
             
                        # class is wrong.
         | 
| 595 595 | 
             
                        # Until we store a security policy per rooter, we rebuild an
         | 
| 596 596 | 
             
                        # incomplete request here.
         | 
| 597 | 
            -
                        req = GenericRequest[DefaultRegistry](self.registry, request)
         | 
| 597 | 
            +
                        req = GenericRequest[DefaultRegistry, Any, Any](self.registry, request)
         | 
| 598 598 | 
             
                        resp = handler(req, exc)
         | 
| 599 599 | 
             
                        if isinstance(resp, Response):
         | 
| 600 600 | 
             
                            return resp
         | 
    
        fastlife/domain/model/request.py
    CHANGED
    
    | @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Any, Generic | |
| 5 5 | 
             
            from starlette.requests import Request as BaseRequest
         | 
| 6 6 |  | 
| 7 7 | 
             
            from fastlife.domain.model.csrf import CSRFToken, create_csrf_token
         | 
| 8 | 
            +
            from fastlife.domain.model.security_policy import TClaimedIdentity, TIdentity
         | 
| 8 9 | 
             
            from fastlife.service.registry import TRegistry
         | 
| 9 10 |  | 
| 10 11 | 
             
            if TYPE_CHECKING:
         | 
| @@ -14,7 +15,7 @@ if TYPE_CHECKING: | |
| 14 15 | 
             
                )
         | 
| 15 16 |  | 
| 16 17 |  | 
| 17 | 
            -
            class GenericRequest(BaseRequest, Generic[TRegistry]):
         | 
| 18 | 
            +
            class GenericRequest(BaseRequest, Generic[TRegistry, TIdentity, TClaimedIdentity]):
         | 
| 18 19 | 
             
                """HTTP Request representation."""
         | 
| 19 20 |  | 
| 20 21 | 
             
                registry: TRegistry
         | 
| @@ -22,7 +23,9 @@ class GenericRequest(BaseRequest, Generic[TRegistry]): | |
| 22 23 | 
             
                locale_name: str
         | 
| 23 24 | 
             
                """Request locale used for the i18n of the response."""
         | 
| 24 25 |  | 
| 25 | 
            -
                security_policy:  | 
| 26 | 
            +
                security_policy: (
         | 
| 27 | 
            +
                    "AbstractSecurityPolicy[TRegistry, TIdentity, TClaimedIdentity] | None"
         | 
| 28 | 
            +
                )
         | 
| 26 29 | 
             
                """Request locale used for the i18n of the response."""
         | 
| 27 30 |  | 
| 28 31 | 
             
                renderer_globals: dict[str, Any]
         | 
| @@ -2,7 +2,7 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            import logging
         | 
| 4 4 | 
             
            from collections.abc import Callable, Coroutine
         | 
| 5 | 
            -
            from typing import Any, Literal, TypeVar
         | 
| 5 | 
            +
            from typing import Any, Generic, Literal, TypeVar
         | 
| 6 6 |  | 
| 7 7 | 
             
            from fastapi import HTTPException
         | 
| 8 8 | 
             
            from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
         | 
| @@ -10,13 +10,62 @@ from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN | |
| 10 10 | 
             
            CheckPermissionHook = Callable[..., Coroutine[Any, Any, None]] | Callable[..., None]
         | 
| 11 11 | 
             
            CheckPermission = Callable[[str], CheckPermissionHook]
         | 
| 12 12 |  | 
| 13 | 
            -
             | 
| 13 | 
            +
            TClaimedIdentity = TypeVar("TClaimedIdentity")
         | 
| 14 | 
            +
            TIdentity = TypeVar("TIdentity")
         | 
| 14 15 |  | 
| 15 16 | 
             
            log = logging.getLogger(__name__)
         | 
| 16 17 |  | 
| 17 18 |  | 
| 19 | 
            +
            class _Anonymous: ...
         | 
| 20 | 
            +
             | 
| 21 | 
            +
             | 
| 22 | 
            +
            Anonymous = _Anonymous()
         | 
| 23 | 
            +
            """
         | 
| 24 | 
            +
            The user is not authenticated.
         | 
| 25 | 
            +
            """
         | 
| 26 | 
            +
             | 
| 27 | 
            +
             | 
| 28 | 
            +
            class PendingMFA(Generic[TClaimedIdentity]):
         | 
| 29 | 
            +
                """
         | 
| 30 | 
            +
                The user provided its identity, usually validated with a first factor,
         | 
| 31 | 
            +
                such as a password but it has not totally proved its authentication
         | 
| 32 | 
            +
                by a second or many other factors of authentication.
         | 
| 33 | 
            +
                The type TClaimedIdentity will store the relevant informations during
         | 
| 34 | 
            +
                this authentication phase.
         | 
| 35 | 
            +
                """
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                claimed: TClaimedIdentity | None
         | 
| 38 | 
            +
                __match_args__ = ("claimed",)
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def __init__(self, claimed: TClaimedIdentity) -> None:
         | 
| 41 | 
            +
                    self.claimed = claimed
         | 
| 42 | 
            +
             | 
| 43 | 
            +
             | 
| 44 | 
            +
            class Authenticated(Generic[TIdentity]):
         | 
| 45 | 
            +
                """The identity has been validated."""
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                __match_args__ = ("identity",)
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def __init__(self, identity: TIdentity) -> None:
         | 
| 50 | 
            +
                    self.identity = identity
         | 
| 51 | 
            +
             | 
| 52 | 
            +
             | 
| 53 | 
            +
            AuthenticationState = (
         | 
| 54 | 
            +
                _Anonymous | PendingMFA[TClaimedIdentity] | Authenticated[TIdentity]
         | 
| 55 | 
            +
            )
         | 
| 56 | 
            +
            """
         | 
| 57 | 
            +
            Type representing the state of an authentication.
         | 
| 58 | 
            +
            """
         | 
| 59 | 
            +
             | 
| 60 | 
            +
            NoMFAAuthenticationState = AuthenticationState[None, TIdentity]
         | 
| 61 | 
            +
            """
         | 
| 62 | 
            +
            Type representing a state of authentication when no multiple factor of authentication
         | 
| 63 | 
            +
            is involved.
         | 
| 64 | 
            +
            """
         | 
| 65 | 
            +
             | 
| 66 | 
            +
             | 
| 18 67 | 
             
            class Unauthorized(HTTPException):
         | 
| 19 | 
            -
                """An exception raised to stop a request  | 
| 68 | 
            +
                """An exception raised to stop a request execution and return a 401 HTTP Error."""
         | 
| 20 69 |  | 
| 21 70 | 
             
                def __init__(
         | 
| 22 71 | 
             
                    self,
         | 
| @@ -27,8 +76,22 @@ class Unauthorized(HTTPException): | |
| 27 76 | 
             
                    super().__init__(status_code, detail, headers)
         | 
| 28 77 |  | 
| 29 78 |  | 
| 79 | 
            +
            class MFARequired(Unauthorized):
         | 
| 80 | 
            +
                """
         | 
| 81 | 
            +
                An exception raised to stop a request execution and return a 401 HTTP Error for MFA.
         | 
| 82 | 
            +
                """
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                def __init__(
         | 
| 85 | 
            +
                    self,
         | 
| 86 | 
            +
                    status_code: int = HTTP_401_UNAUTHORIZED,
         | 
| 87 | 
            +
                    detail: str = "MFA Required",
         | 
| 88 | 
            +
                    headers: dict[str, str] | None = None,
         | 
| 89 | 
            +
                ) -> None:
         | 
| 90 | 
            +
                    super().__init__(status_code, detail, headers)
         | 
| 91 | 
            +
             | 
| 92 | 
            +
             | 
| 30 93 | 
             
            class Forbidden(HTTPException):
         | 
| 31 | 
            -
                """An exception raised to stop a request  | 
| 94 | 
            +
                """An exception raised to stop a request execution and return a 403 HTTP Error."""
         | 
| 32 95 |  | 
| 33 96 | 
             
                def __init__(
         | 
| 34 97 | 
             
                    self,
         | 
| @@ -59,7 +122,7 @@ class HasPermission(int, metaclass=BoolMeta): | |
| 59 122 | 
             
                or the frontend may use the information to adapt its interface.
         | 
| 60 123 | 
             
                """
         | 
| 61 124 |  | 
| 62 | 
            -
                kind: Literal["allowed", "unauthenticated", "denied"]
         | 
| 125 | 
            +
                kind: Literal["allowed", "unauthenticated", "mfa_required", "denied"]
         | 
| 63 126 | 
             
                """
         | 
| 64 127 | 
             
                Identified basic information of the response.
         | 
| 65 128 | 
             
                It distinguished unauthenticated and denied to eventually raised 401 over 403 error.
         | 
| @@ -96,6 +159,16 @@ class Unauthenticated(HasPermission): | |
| 96 159 | 
             
                reason = "Authentication required"
         | 
| 97 160 |  | 
| 98 161 |  | 
| 162 | 
            +
            class PreAuthenticated(HasPermission):
         | 
| 163 | 
            +
                """
         | 
| 164 | 
            +
                Represent a permission check result that is not allowed due to
         | 
| 165 | 
            +
                missing secondary authentication mechanism.
         | 
| 166 | 
            +
                """
         | 
| 167 | 
            +
             | 
| 168 | 
            +
                kind = "mfa_required"
         | 
| 169 | 
            +
                reason = "MFA required"
         | 
| 170 | 
            +
             | 
| 171 | 
            +
             | 
| 99 172 | 
             
            class Denied(HasPermission):
         | 
| 100 173 | 
             
                """
         | 
| 101 174 | 
             
                Represent a permission check result that is not allowed due to lack of permission.
         | 
| @@ -34,6 +34,8 @@ def check_permission(permission_name: str) -> CheckPermissionHook: | |
| 34 34 | 
             
                            return
         | 
| 35 35 | 
             
                        case "denied":
         | 
| 36 36 | 
             
                            raise request.security_policy.Forbidden(detail=allowed.reason)
         | 
| 37 | 
            +
                        case "mfa_required":
         | 
| 38 | 
            +
                            raise request.security_policy.MFARequired(detail=allowed.reason)
         | 
| 37 39 | 
             
                        case "unauthenticated":
         | 
| 38 40 | 
             
                            raise request.security_policy.Unauthorized(detail=allowed.reason)
         | 
| 39 41 |  | 
| @@ -10,14 +10,16 @@ LocaleName = str | |
| 10 10 |  | 
| 11 11 | 
             
            from fastlife.adapters.fastapi.request import GenericRequest  # coverage: ignore
         | 
| 12 12 |  | 
| 13 | 
            -
            LocaleNegociator = Callable[ | 
| 13 | 
            +
            LocaleNegociator = Callable[
         | 
| 14 | 
            +
                [GenericRequest[Any, Any, Any]], LocaleName
         | 
| 15 | 
            +
            ]  # coverage: ignore
         | 
| 14 16 | 
             
            """Interface to implement to negociate a locale"""  # coverage: ignore
         | 
| 15 17 |  | 
| 16 18 |  | 
| 17 19 | 
             
            def default_negociator(settings: Settings) -> LocaleNegociator:
         | 
| 18 20 | 
             
                """The default local negociator return the locale set in the conf."""
         | 
| 19 21 |  | 
| 20 | 
            -
                def locale_negociator(request: "GenericRequest[Any]") -> str:
         | 
| 22 | 
            +
                def locale_negociator(request: "GenericRequest[Any, Any, Any]") -> str:
         | 
| 21 23 | 
             
                    return settings.default_locale
         | 
| 22 24 |  | 
| 23 25 | 
             
                return locale_negociator
         | 
| @@ -2,34 +2,44 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            import abc
         | 
| 4 4 | 
             
            from typing import Annotated, Any, Generic
         | 
| 5 | 
            -
            from uuid import UUID
         | 
| 6 5 |  | 
| 7 6 | 
             
            from fastapi import Depends
         | 
| 8 7 |  | 
| 9 8 | 
             
            from fastlife import GenericRequest, get_request
         | 
| 10 9 | 
             
            from fastlife.domain.model.security_policy import (
         | 
| 11 10 | 
             
                Allowed,
         | 
| 11 | 
            +
                Anonymous,
         | 
| 12 | 
            +
                Authenticated,
         | 
| 13 | 
            +
                AuthenticationState,
         | 
| 12 14 | 
             
                Forbidden,
         | 
| 13 15 | 
             
                HasPermission,
         | 
| 14 | 
            -
                 | 
| 16 | 
            +
                MFARequired,
         | 
| 17 | 
            +
                PendingMFA,
         | 
| 18 | 
            +
                TClaimedIdentity,
         | 
| 19 | 
            +
                TIdentity,
         | 
| 15 20 | 
             
                Unauthorized,
         | 
| 16 21 | 
             
            )
         | 
| 17 22 | 
             
            from fastlife.service.registry import TRegistry
         | 
| 18 23 |  | 
| 19 24 |  | 
| 20 | 
            -
            class AbstractSecurityPolicy(abc.ABC, Generic[ | 
| 25 | 
            +
            class AbstractSecurityPolicy(abc.ABC, Generic[TRegistry, TIdentity, TClaimedIdentity]):
         | 
| 21 26 | 
             
                """Security policy base class."""
         | 
| 22 27 |  | 
| 23 28 | 
             
                Forbidden = Forbidden
         | 
| 24 29 | 
             
                """The exception raised if the user identified is not granted."""
         | 
| 25 30 | 
             
                Unauthorized = Unauthorized
         | 
| 26 31 | 
             
                """The exception raised if no user has been identified."""
         | 
| 32 | 
            +
                MFARequired = MFARequired
         | 
| 33 | 
            +
                """The exception raised if no user has been authenticated using a MFA."""
         | 
| 27 34 |  | 
| 28 | 
            -
                request: GenericRequest[TRegistry]
         | 
| 35 | 
            +
                request: GenericRequest[TRegistry, TIdentity, TClaimedIdentity]
         | 
| 29 36 | 
             
                """Request where the security policy is applied."""
         | 
| 30 37 |  | 
| 31 38 | 
             
                def __init__(
         | 
| 32 | 
            -
                    self, | 
| 39 | 
            +
                    self,
         | 
| 40 | 
            +
                    request: Annotated[
         | 
| 41 | 
            +
                        GenericRequest[TRegistry, TIdentity, TClaimedIdentity], Depends(get_request)
         | 
| 42 | 
            +
                    ],
         | 
| 33 43 | 
             
                ):
         | 
| 34 44 | 
             
                    """
         | 
| 35 45 | 
             
                    Build the security policy.
         | 
| @@ -42,17 +52,48 @@ class AbstractSecurityPolicy(abc.ABC, Generic[TUser, TRegistry]): | |
| 42 52 | 
             
                    """
         | 
| 43 53 | 
             
                    self.request = request
         | 
| 44 54 | 
             
                    self.request.security_policy = self  # we do backref to implement has_permission
         | 
| 55 | 
            +
                    self._authentication_state: (
         | 
| 56 | 
            +
                        AuthenticationState[TClaimedIdentity, TIdentity] | None
         | 
| 57 | 
            +
                    ) = None
         | 
| 45 58 |  | 
| 46 | 
            -
                 | 
| 47 | 
            -
             | 
| 59 | 
            +
                async def get_authentication_state(
         | 
| 60 | 
            +
                    self,
         | 
| 61 | 
            +
                ) -> AuthenticationState[TClaimedIdentity, TIdentity]:
         | 
| 62 | 
            +
                    """
         | 
| 63 | 
            +
                    Return app-specific user object or None.
         | 
| 64 | 
            +
                    """
         | 
| 65 | 
            +
                    if self._authentication_state is None:
         | 
| 66 | 
            +
                        self._authentication_state = await self.build_authentication_state()
         | 
| 67 | 
            +
                    return self._authentication_state
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                async def claimed_identity(self) -> TClaimedIdentity | None:
         | 
| 70 | 
            +
                    """
         | 
| 71 | 
            +
                    Return app-specific user object that pretend to be identified.
         | 
| 72 | 
            +
                    """
         | 
| 73 | 
            +
                    auth = await self.get_authentication_state()
         | 
| 74 | 
            +
                    match auth:
         | 
| 75 | 
            +
                        case PendingMFA(claimed):
         | 
| 76 | 
            +
                            return claimed
         | 
| 77 | 
            +
                        case _:
         | 
| 78 | 
            +
                            return None
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                async def identity(self) -> TIdentity | None:
         | 
| 48 81 | 
             
                    """
         | 
| 49 | 
            -
                    Return app-specific user object  | 
| 82 | 
            +
                    Return app-specific user object after an mfa authentication or None.
         | 
| 50 83 | 
             
                    """
         | 
| 84 | 
            +
                    auth = await self.get_authentication_state()
         | 
| 85 | 
            +
                    match auth:
         | 
| 86 | 
            +
                        case Authenticated(identity):
         | 
| 87 | 
            +
                            return identity
         | 
| 88 | 
            +
                        case _:
         | 
| 89 | 
            +
                            return None
         | 
| 51 90 |  | 
| 52 91 | 
             
                @abc.abstractmethod
         | 
| 53 | 
            -
                async def  | 
| 92 | 
            +
                async def build_authentication_state(
         | 
| 93 | 
            +
                    self,
         | 
| 94 | 
            +
                ) -> AuthenticationState[TClaimedIdentity, TIdentity]:
         | 
| 54 95 | 
             
                    """
         | 
| 55 | 
            -
                    Return  | 
| 96 | 
            +
                    Return the authentication state for the current request.
         | 
| 56 97 | 
             
                    """
         | 
| 57 98 |  | 
| 58 99 | 
             
                @abc.abstractmethod
         | 
| @@ -62,7 +103,11 @@ class AbstractSecurityPolicy(abc.ABC, Generic[TUser, TRegistry]): | |
| 62 103 | 
             
                    """Allow access to everything if signed in."""
         | 
| 63 104 |  | 
| 64 105 | 
             
                @abc.abstractmethod
         | 
| 65 | 
            -
                async def  | 
| 106 | 
            +
                async def pre_remember(self, claimed_identity: TClaimedIdentity) -> None:
         | 
| 107 | 
            +
                    """Save the user identity in the request session."""
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                @abc.abstractmethod
         | 
| 110 | 
            +
                async def remember(self, identity: TIdentity) -> None:
         | 
| 66 111 | 
             
                    """Save the user identity in the request session."""
         | 
| 67 112 |  | 
| 68 113 | 
             
                @abc.abstractmethod
         | 
| @@ -70,7 +115,12 @@ class AbstractSecurityPolicy(abc.ABC, Generic[TUser, TRegistry]): | |
| 70 115 | 
             
                    """Destroy the request session."""
         | 
| 71 116 |  | 
| 72 117 |  | 
| 73 | 
            -
            class  | 
| 118 | 
            +
            class AbstractNoMFASecurityPolicy(AbstractSecurityPolicy[TRegistry, TIdentity, None]):
         | 
| 119 | 
            +
                async def pre_remember(self, claimed_identity: None) -> None:
         | 
| 120 | 
            +
                    """Do Nothing."""
         | 
| 121 | 
            +
             | 
| 122 | 
            +
             | 
| 123 | 
            +
            class InsecurePolicy(AbstractNoMFASecurityPolicy[Any, None]):
         | 
| 74 124 | 
             
                """
         | 
| 75 125 | 
             
                An implementation of the security policy made for explicit unsecured access.
         | 
| 76 126 |  | 
| @@ -79,13 +129,10 @@ class InsecurePolicy(AbstractSecurityPolicy[None, Any]): | |
| 79 129 | 
             
                or your own reason, the InsecurePolicy has to be set to the configurator.
         | 
| 80 130 | 
             
                """
         | 
| 81 131 |  | 
| 82 | 
            -
                async def  | 
| 83 | 
            -
                     | 
| 84 | 
            -
             | 
| 85 | 
            -
             | 
| 86 | 
            -
                async def authenticated_userid(self) -> str | UUID:
         | 
| 87 | 
            -
                    """An uuid mades of 0."""
         | 
| 88 | 
            -
                    return UUID(int=0)
         | 
| 132 | 
            +
                async def build_authentication_state(
         | 
| 133 | 
            +
                    self,
         | 
| 134 | 
            +
                ) -> AuthenticationState[None, None]:
         | 
| 135 | 
            +
                    return Anonymous
         | 
| 89 136 |  | 
| 90 137 | 
             
                async def has_permission(
         | 
| 91 138 | 
             
                    self, permission: str
         | 
| @@ -93,7 +140,7 @@ class InsecurePolicy(AbstractSecurityPolicy[None, Any]): | |
| 93 140 | 
             
                    """Access is allways granted."""
         | 
| 94 141 | 
             
                    return Allowed
         | 
| 95 142 |  | 
| 96 | 
            -
                async def remember(self,  | 
| 143 | 
            +
                async def remember(self, identity: None) -> None:
         | 
| 97 144 | 
             
                    """Do nothing."""
         | 
| 98 145 |  | 
| 99 146 | 
             
                async def forget(self) -> None:
         | 
| @@ -1,13 +1,13 @@ | |
| 1 | 
            -
            CHANGELOG.md,sha256= | 
| 2 | 
            -
            fastlife/__init__.py,sha256= | 
| 1 | 
            +
            CHANGELOG.md,sha256=Ap14kfVx07rFhITGDmtQfwEsJowg5osxKETE0w478lU,7801
         | 
| 2 | 
            +
            fastlife/__init__.py,sha256=cx3BScbBelH-Tm63VPdAp5siW1fBZ0uOMwNW_SPs2xQ,2126
         | 
| 3 3 | 
             
            fastlife/adapters/__init__.py,sha256=imPD1hImpgrYkvUJRhHA5kVyGAua7VbP2WGkhSWKJT8,93
         | 
| 4 4 | 
             
            fastlife/adapters/fastapi/__init__.py,sha256=1goV1FGFP04TGyskJBLKZam4Gvt1yoAvLMNs4ekWSSQ,243
         | 
| 5 5 | 
             
            fastlife/adapters/fastapi/form.py,sha256=csxsDI6RK-g41pMwFhaVQCLDhF7dAZzgUp-VcrC3NFY,823
         | 
| 6 6 | 
             
            fastlife/adapters/fastapi/form_data.py,sha256=2DQ0o-RvY6iROUKQjS-UJdNYEVSsNPd-AjpergI3w54,4473
         | 
| 7 7 | 
             
            fastlife/adapters/fastapi/localizer.py,sha256=XD1kCJuAlkGevivmvAJEcGMCBWMef9rAfTOGmt3PVWU,436
         | 
| 8 | 
            -
            fastlife/adapters/fastapi/request.py,sha256= | 
| 8 | 
            +
            fastlife/adapters/fastapi/request.py,sha256=COOoSMZAm4VhyJgM7dlqJ7YdGjeGI7qs93PtBsriEPc,1115
         | 
| 9 9 | 
             
            fastlife/adapters/fastapi/routing/__init__.py,sha256=8EMnQE5n8oA4J9_c3nxzwKDVt3tefZ6fGH0d2owE8mo,195
         | 
| 10 | 
            -
            fastlife/adapters/fastapi/routing/route.py,sha256= | 
| 10 | 
            +
            fastlife/adapters/fastapi/routing/route.py,sha256=XnDPvd5V0Zl7Ke6bBErEtUCjmNQPcV2U_w1dWpx6qM4,1476
         | 
| 11 11 | 
             
            fastlife/adapters/fastapi/routing/router.py,sha256=jzrnU_Lyywu21e3spPaWQw8ujZh_Yy_EJOojcCi6ew4,499
         | 
| 12 12 | 
             
            fastlife/adapters/itsdangerous/__init__.py,sha256=7ocGY7v0cxooZBKQYjA2JkmzRqiBvcU1uzA84UsTVAI,84
         | 
| 13 13 | 
             
            fastlife/adapters/itsdangerous/session.py,sha256=9h_WRsXqZbytHZOv5B_K3OWD5mbfYzxHulXoOf6D2MI,1685
         | 
| @@ -1686,7 +1686,7 @@ fastlife/components/pydantic_form/FatalError.jinja,sha256=lFVlNrXzBR6ExMahq77h0t | |
| 1686 1686 | 
             
            fastlife/components/pydantic_form/Hint.jinja,sha256=8leBpfMGDmalc_KAjr2paTojr_rwq-luS6m_1BGj7Tw,202
         | 
| 1687 1687 | 
             
            fastlife/components/pydantic_form/Widget.jinja,sha256=PgguUpvhG6CY9AW6H8qQMjKqjlybjDCAaFFAOHzrzVQ,418
         | 
| 1688 1688 | 
             
            fastlife/config/__init__.py,sha256=5qpuaVYqi-AS0GgsfggM6rFsSwXgrqrLBo9jH6dVroc,407
         | 
| 1689 | 
            -
            fastlife/config/configurator.py,sha256= | 
| 1689 | 
            +
            fastlife/config/configurator.py,sha256=SURXmBrdTghHoG2f9R2BUF6TKZXg1lNmwP3ZbuApJ3M,24722
         | 
| 1690 1690 | 
             
            fastlife/config/exceptions.py,sha256=9MdBnbfy-Aw-KaIFzju0Kh8Snk41-v9LqK2w48Tdy1s,1169
         | 
| 1691 1691 | 
             
            fastlife/config/openapiextra.py,sha256=rYoerrn9sni2XwnO3gIWqaz7M0aDZPhVLjzqhDxue0o,514
         | 
| 1692 1692 | 
             
            fastlife/config/resources.py,sha256=u6OgnbHfGkC5idH-YPNkIPf8GJnZpJoGVZ-Ym022BCo,8533
         | 
| @@ -1696,8 +1696,8 @@ fastlife/domain/model/__init__.py,sha256=aoBjaSpDscuFXvtknJHwiNyoJRUpE-v4X54h_wN | |
| 1696 1696 | 
             
            fastlife/domain/model/asgi.py,sha256=RSTnfTsofOmCaWzHNuRGowjlyHYmoDCrXFbvNY_B55k,129
         | 
| 1697 1697 | 
             
            fastlife/domain/model/csrf.py,sha256=BUiWK-S7rVciWHO1qTkM8e_KxzpF6gGC4MMJK1v6iDo,414
         | 
| 1698 1698 | 
             
            fastlife/domain/model/form.py,sha256=WriBT1qUUIbf5x5iewo9ChEcr6k0en8jMTD0iaei5Pk,3253
         | 
| 1699 | 
            -
            fastlife/domain/model/request.py,sha256= | 
| 1700 | 
            -
            fastlife/domain/model/security_policy.py,sha256= | 
| 1699 | 
            +
            fastlife/domain/model/request.py,sha256=ZRHZW_MOmtO_DFHt2UYu_aUmtoMdD14085A8Z8_eS8s,2678
         | 
| 1700 | 
            +
            fastlife/domain/model/security_policy.py,sha256=f9SLi54vvRU-KSPJ5K0unoqYpkxIyzuZjKf2Ylwf5Rg,4796
         | 
| 1701 1701 | 
             
            fastlife/domain/model/template.py,sha256=z9oxdKme1hMPuvk7mBiKR_tuVY8TqH77aTYqMgvEGl8,876
         | 
| 1702 1702 | 
             
            fastlife/domain/model/types.py,sha256=64jJKFAi5x0e3vr8naHU1m_as0Qy8MS-s9CG0z6K1qc,381
         | 
| 1703 1703 | 
             
            fastlife/middlewares/__init__.py,sha256=C3DUOzR5EhlAv5Zq7h-Abyvkd7bUsJohTRSB2wpRYQE,220
         | 
| @@ -1709,11 +1709,11 @@ fastlife/middlewares/session/middleware.py,sha256=ituZ5hNipDMkgCXNE4zbnmOcWEF151 | |
| 1709 1709 | 
             
            fastlife/middlewares/session/serializer.py,sha256=nbJGiCJ_ryZxkW1I28kmK6hD3U98D4ZlUQA7B8_tngQ,635
         | 
| 1710 1710 | 
             
            fastlife/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 1711 1711 | 
             
            fastlife/service/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 1712 | 
            -
            fastlife/service/check_permission.py,sha256=- | 
| 1712 | 
            +
            fastlife/service/check_permission.py,sha256=-TsI58YZJtWIw5bsm0fVpfuaCMUx4cmoLTKGXeyQPDk,1809
         | 
| 1713 1713 | 
             
            fastlife/service/csrf.py,sha256=wC1PaKOmZ3il0FF_kevxnlg9PxDqruRdLrNnOA3ZHrU,1886
         | 
| 1714 | 
            -
            fastlife/service/locale_negociator.py,sha256= | 
| 1714 | 
            +
            fastlife/service/locale_negociator.py,sha256=JUqzTukxDqTJVOR-CNI7Vqo6kvdvwxYvZQe8P3V9S2U,796
         | 
| 1715 1715 | 
             
            fastlife/service/registry.py,sha256=B6n5b_b0RgxJj0qFOpnrJFmG7_MPtvShwV6yH9V6vi0,2098
         | 
| 1716 | 
            -
            fastlife/service/security_policy.py,sha256= | 
| 1716 | 
            +
            fastlife/service/security_policy.py,sha256=qYXs4mhfz_u4x59NhUkirqKYKQbFv9YrzyRuXj7mxE0,4688
         | 
| 1717 1717 | 
             
            fastlife/service/templates.py,sha256=QPAIUbbZiekazz_jV3q4JCwQd6Q4KA6a4RDek2RWuhE,2548
         | 
| 1718 1718 | 
             
            fastlife/service/translations.py,sha256=D-1D3pVNytEcps1u-0K7FmgQ8Wo6Yu4XVHvZrPhBmAI,5795
         | 
| 1719 1719 | 
             
            fastlife/settings.py,sha256=q-rz4CEF2RQGow5-m-yZJOvdh3PPb2c1Q_ZLJGnu4VQ,3647
         | 
| @@ -1728,9 +1728,9 @@ fastlife/testing/session.py,sha256=LEFFbiR67_x_g-ioudkY0C7PycHdbDfaIaoo_G7GXQ8,2 | |
| 1728 1728 | 
             
            fastlife/testing/testclient.py,sha256=JTIgeMKooA8L4gEodeC3gy4Lo27y3WNswSEIKLlVVPs,6745
         | 
| 1729 1729 | 
             
            fastlife/views/__init__.py,sha256=zG8gveL8e2zBdYx6_9jtZfpQ6qJT-MFnBY3xXkLwHZI,22
         | 
| 1730 1730 | 
             
            fastlife/views/pydantic_form.py,sha256=o7EUItciAGL1OSaGNHo-3BTrYAk34GuWE7zGikjiAGY,1486
         | 
| 1731 | 
            -
            fastlifeweb-0. | 
| 1732 | 
            -
            fastlifeweb-0. | 
| 1733 | 
            -
            fastlifeweb-0. | 
| 1734 | 
            -
            fastlifeweb-0. | 
| 1731 | 
            +
            fastlifeweb-0.23.0.dist-info/METADATA,sha256=Eww8hBxH7oR5_EeqtALpdmUmJr2f1vs1E0EijXaxM30,3663
         | 
| 1732 | 
            +
            fastlifeweb-0.23.0.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
         | 
| 1733 | 
            +
            fastlifeweb-0.23.0.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
         | 
| 1734 | 
            +
            fastlifeweb-0.23.0.dist-info/licenses/LICENSE,sha256=NlRX9Z-dcv8X1VFW9odlIQBbgNN9pcO94XzvKp2R16o,1075
         | 
| 1735 1735 | 
             
            tailwind.config.js,sha256=EN3EahBDmQBbmJvkw3SdGWNOkfkzw0cg-QvBikOhkrw,1348
         | 
| 1736 | 
            -
            fastlifeweb-0. | 
| 1736 | 
            +
            fastlifeweb-0.23.0.dist-info/RECORD,,
         | 
| 
            File without changes
         | 
| 
            File without changes
         | 
| 
            File without changes
         |