fastlifeweb 0.22.1__py3-none-any.whl → 0.23.1__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 +15 -0
- fastlife/__init__.py +20 -1
- fastlife/adapters/fastapi/localizer.py +1 -1
- fastlife/adapters/fastapi/request.py +3 -3
- fastlife/adapters/fastapi/routing/route.py +1 -1
- fastlife/config/configurator.py +4 -4
- fastlife/domain/model/request.py +5 -3
- fastlife/domain/model/security_policy.py +78 -5
- fastlife/service/check_permission.py +2 -0
- fastlife/service/locale_negociator.py +4 -2
- fastlife/service/registry.py +2 -2
- fastlife/service/security_policy.py +67 -20
- fastlife/service/translatablestring.py +1 -0
- fastlife/service/translations.py +45 -8
- fastlife/testing/testclient.py +1 -1
- {fastlifeweb-0.22.1.dist-info → fastlifeweb-0.23.1.dist-info}/METADATA +1 -1
- {fastlifeweb-0.22.1.dist-info → fastlifeweb-0.23.1.dist-info}/RECORD +20 -19
- {fastlifeweb-0.22.1.dist-info → fastlifeweb-0.23.1.dist-info}/WHEEL +0 -0
- {fastlifeweb-0.22.1.dist-info → fastlifeweb-0.23.1.dist-info}/entry_points.txt +0 -0
- {fastlifeweb-0.22.1.dist-info → fastlifeweb-0.23.1.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
+
## 0.23.1 - Released on 2025-01-14
|
2
|
+
* Fix typing issue
|
3
|
+
* Update docs
|
4
|
+
|
5
|
+
## 0.23.0 - Released on 2024-12-04
|
6
|
+
* Update Request type.
|
7
|
+
* Breaking changes: Request[TUser, TRegistry] -> Request[TRegistry, TIdentity, TClaimedIdentity].
|
8
|
+
* Update SecurityPolicy, designed for MFA by default.
|
9
|
+
* Breaking changes: new abstract method added. build_authentication_state.
|
10
|
+
* Breaking changes: there is no more get_authenticated_userid method.
|
11
|
+
* The identity method is not abstract anymore, result comes from the build_authentication_state.
|
12
|
+
* New method get_authentication_state, claimed_identity and pre_remember.
|
13
|
+
* Add a AbstractNoMFASecurityPolicy that build a AbstractSecurityPolicy without TClaimedIdentity as None.
|
14
|
+
* New ACL type added to raise 401 errors due to missing MFA which may not be same url as tu login/password.
|
15
|
+
|
1
16
|
## 0.22.1 - Released on 2024-11-27
|
2
17
|
* Improve Request typing
|
3
18
|
|
fastlife/__init__.py
CHANGED
@@ -27,9 +27,15 @@ from .domain.model.form import FormModel
|
|
27
27
|
from .domain.model.request import GenericRequest
|
28
28
|
from .domain.model.security_policy import (
|
29
29
|
Allowed,
|
30
|
+
Anonymous,
|
31
|
+
Authenticated,
|
32
|
+
AuthenticationState,
|
30
33
|
Denied,
|
31
34
|
Forbidden,
|
32
35
|
HasPermission,
|
36
|
+
NoMFAAuthenticationState,
|
37
|
+
PendingMFA,
|
38
|
+
PreAuthenticated,
|
33
39
|
Unauthenticated,
|
34
40
|
Unauthorized,
|
35
41
|
)
|
@@ -37,7 +43,12 @@ from .domain.model.template import JinjaXTemplate
|
|
37
43
|
|
38
44
|
# from .request.form_data import model
|
39
45
|
from .service.registry import DefaultRegistry, GenericRegistry, TRegistry, TSettings
|
40
|
-
from .service.security_policy import
|
46
|
+
from .service.security_policy import (
|
47
|
+
AbstractNoMFASecurityPolicy,
|
48
|
+
AbstractSecurityPolicy,
|
49
|
+
InsecurePolicy,
|
50
|
+
)
|
51
|
+
from .service.translations import TranslatableStringFactory
|
41
52
|
from .settings import Settings
|
42
53
|
|
43
54
|
__all__ = [
|
@@ -69,15 +80,23 @@ __all__ = [
|
|
69
80
|
"RedirectResponse",
|
70
81
|
# Security
|
71
82
|
"AbstractSecurityPolicy",
|
83
|
+
"AbstractNoMFASecurityPolicy",
|
72
84
|
"HasPermission",
|
73
85
|
"Unauthenticated",
|
86
|
+
"PreAuthenticated",
|
74
87
|
"Allowed",
|
75
88
|
"Denied",
|
76
89
|
"Unauthorized",
|
77
90
|
"Forbidden",
|
78
91
|
"InsecurePolicy",
|
92
|
+
"Anonymous",
|
93
|
+
"PendingMFA",
|
94
|
+
"Authenticated",
|
95
|
+
"AuthenticationState",
|
96
|
+
"NoMFAAuthenticationState",
|
79
97
|
# Template
|
80
98
|
"JinjaXTemplate",
|
81
99
|
# i18n
|
82
100
|
"Localizer",
|
101
|
+
"TranslatableStringFactory",
|
83
102
|
]
|
@@ -8,7 +8,7 @@ from fastlife.service.translations import Localizer as RequestLocalizer
|
|
8
8
|
|
9
9
|
def get_localizer(request: Request) -> RequestLocalizer:
|
10
10
|
"""Return the localizer for the given request."""
|
11
|
-
return request.registry.localizer(request)
|
11
|
+
return request.registry.localizer(request.locale_name)
|
12
12
|
|
13
13
|
|
14
14
|
Localizer = Annotated[RequestLocalizer, Depends(get_localizer)]
|
@@ -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, 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[Any,
|
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
20
|
# basically a issubclass(Request), does not work with Generic[T].
|
21
21
|
|
22
22
|
|
23
|
-
AnyRequest = Annotated[GenericRequest[Any, 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[Any, Any](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.
|
@@ -473,7 +473,7 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
473
473
|
* `gettext`, `ngettext`, `dgettext`, `dngettext`, `pgettext`, `dpgettext`,
|
474
474
|
`npgettext`, `dnpgettext` methods are installed for i18n purpose.
|
475
475
|
"""
|
476
|
-
lczr = request.registry.localizer(request)
|
476
|
+
lczr = request.registry.localizer(request.locale_name)
|
477
477
|
custom_globals = {}
|
478
478
|
for key, (val, evaluate) in self._renderer_globals.items():
|
479
479
|
if evaluate and callable(val):
|
@@ -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[Any,
|
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,7 +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
|
8
|
+
from fastlife.domain.model.security_policy import TClaimedIdentity, TIdentity
|
9
9
|
from fastlife.service.registry import TRegistry
|
10
10
|
|
11
11
|
if TYPE_CHECKING:
|
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
|
15
15
|
)
|
16
16
|
|
17
17
|
|
18
|
-
class GenericRequest(BaseRequest, Generic[
|
18
|
+
class GenericRequest(BaseRequest, Generic[TRegistry, TIdentity, TClaimedIdentity]):
|
19
19
|
"""HTTP Request representation."""
|
20
20
|
|
21
21
|
registry: TRegistry
|
@@ -23,7 +23,9 @@ class GenericRequest(BaseRequest, Generic[TUser, TRegistry]):
|
|
23
23
|
locale_name: str
|
24
24
|
"""Request locale used for the i18n of the response."""
|
25
25
|
|
26
|
-
security_policy:
|
26
|
+
security_policy: (
|
27
|
+
"AbstractSecurityPolicy[TRegistry, TIdentity, TClaimedIdentity] | None"
|
28
|
+
)
|
27
29
|
"""Request locale used for the i18n of the response."""
|
28
30
|
|
29
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, 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
|
fastlife/service/registry.py
CHANGED
@@ -22,7 +22,7 @@ class GenericRegistry(Generic[TSettings]):
|
|
22
22
|
It is initialized by the configurator and accessed by the `fastlife.Registry`.
|
23
23
|
"""
|
24
24
|
|
25
|
-
settings:
|
25
|
+
settings: TSettings
|
26
26
|
"""Application settings."""
|
27
27
|
renderers: Mapping[str, "AbstractTemplateRendererFactory"]
|
28
28
|
"""Registered template engine."""
|
@@ -31,7 +31,7 @@ class GenericRegistry(Generic[TSettings]):
|
|
31
31
|
localizer: "LocalizerFactory"
|
32
32
|
"""Used to localized message."""
|
33
33
|
|
34
|
-
def __init__(self, settings:
|
34
|
+
def __init__(self, settings: TSettings) -> None:
|
35
35
|
from fastlife.service.locale_negociator import default_negociator
|
36
36
|
from fastlife.service.translations import LocalizerFactory
|
37
37
|
|
@@ -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[
|
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:
|
@@ -0,0 +1 @@
|
|
1
|
+
|
fastlife/service/translations.py
CHANGED
@@ -3,18 +3,52 @@ from collections import defaultdict
|
|
3
3
|
from collections.abc import Callable, Iterator
|
4
4
|
from gettext import GNUTranslations
|
5
5
|
from io import BufferedReader
|
6
|
-
from typing import TYPE_CHECKING
|
7
6
|
|
8
7
|
from fastlife.shared_utils.resolver import resolve_path
|
9
8
|
|
10
|
-
if TYPE_CHECKING:
|
11
|
-
from fastlife import Request # coverage: ignore
|
12
|
-
|
13
9
|
LocaleName = str
|
14
10
|
Domain = str
|
15
11
|
CONTEXT_ENCODING = "%s\x04%s"
|
16
12
|
|
17
13
|
|
14
|
+
class TranslatableString(str):
|
15
|
+
"""
|
16
|
+
Create a string made for translation associated to a domain.
|
17
|
+
This class is instanciated by the
|
18
|
+
:class:`fastlife.service.translations.TranslatableStringFactory` class.
|
19
|
+
"""
|
20
|
+
|
21
|
+
__slots__ = ("domain",)
|
22
|
+
|
23
|
+
def __new__(cls, msgid: str, domain: str) -> "TranslatableString":
|
24
|
+
self = str.__new__(cls, msgid)
|
25
|
+
self.domain = domain # type: ignore
|
26
|
+
return self
|
27
|
+
|
28
|
+
|
29
|
+
class TranslatableStringFactory:
|
30
|
+
"""Create a catalog of string associated to a domain."""
|
31
|
+
|
32
|
+
def __init__(self, domain: str):
|
33
|
+
self.domain = domain
|
34
|
+
|
35
|
+
def __call__(self, msgid: str) -> str:
|
36
|
+
"""
|
37
|
+
Use to generate the translatable string.
|
38
|
+
|
39
|
+
usually:
|
40
|
+
|
41
|
+
```python
|
42
|
+
_ = TranslatableStringFactory("mydomain")
|
43
|
+
mymessage = _("translatable")
|
44
|
+
```
|
45
|
+
|
46
|
+
Note that the string is associated to mydomain, so the babel extraction has
|
47
|
+
to be initialized with that particular domain.
|
48
|
+
"""
|
49
|
+
return TranslatableString(msgid, self.domain)
|
50
|
+
|
51
|
+
|
18
52
|
def find_mo_files(root_path: str) -> Iterator[tuple[LocaleName, Domain, pathlib.Path]]:
|
19
53
|
"""
|
20
54
|
Find .mo files in a locales directory.
|
@@ -68,7 +102,10 @@ class Localizer:
|
|
68
102
|
return self.gettext(message, mapping)
|
69
103
|
|
70
104
|
def gettext(self, message: str, mapping: dict[str, str] | None = None) -> str:
|
71
|
-
|
105
|
+
if isinstance(message, TranslatableString):
|
106
|
+
ret = self.translations[message.domain].gettext(message) # type: ignore
|
107
|
+
else:
|
108
|
+
ret = self.global_translations.gettext(message)
|
72
109
|
if mapping:
|
73
110
|
ret = ret.format(**mapping)
|
74
111
|
return ret
|
@@ -177,8 +214,8 @@ class LocalizerFactory:
|
|
177
214
|
root_path = resolve_path(path)
|
178
215
|
self._translations.load(root_path)
|
179
216
|
|
180
|
-
def __call__(self,
|
217
|
+
def __call__(self, locale_name: LocaleName) -> Localizer:
|
181
218
|
"""Create the translation context for the given request."""
|
182
|
-
if
|
219
|
+
if locale_name not in self._translations:
|
183
220
|
return self.null_localizer
|
184
|
-
return self._translations.get(
|
221
|
+
return self._translations.get(locale_name)
|
fastlife/testing/testclient.py
CHANGED
@@ -1,13 +1,13 @@
|
|
1
|
-
CHANGELOG.md,sha256=
|
2
|
-
fastlife/__init__.py,sha256=
|
1
|
+
CHANGELOG.md,sha256=gvl1gBNgVNhdz4fSMSIsPwWpArqetbDebCW2PAB6yoE,7871
|
2
|
+
fastlife/__init__.py,sha256=nXWE4AbhkhG_yBjPJU-XnKDMTsU9ebv7Vj4eIciWQI0,2219
|
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
|
-
fastlife/adapters/fastapi/localizer.py,sha256=
|
8
|
-
fastlife/adapters/fastapi/request.py,sha256=
|
7
|
+
fastlife/adapters/fastapi/localizer.py,sha256=Efn6rrf-SnSfM4TqqE_5chacrxaPpupxbvIqXipXEEw,448
|
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=gxa3aTfBXvGqRLLAaZ0CoNAjwHlQIhx4yr1DfY_z2IQ,24734
|
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,13 +1709,14 @@ 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=
|
1715
|
-
fastlife/service/registry.py,sha256=
|
1716
|
-
fastlife/service/security_policy.py,sha256=
|
1714
|
+
fastlife/service/locale_negociator.py,sha256=JUqzTukxDqTJVOR-CNI7Vqo6kvdvwxYvZQe8P3V9S2U,796
|
1715
|
+
fastlife/service/registry.py,sha256=sdLJ-1K7R1_RwVNpQUcyktPOORhDm_pyKw8jehhR5VM,2100
|
1716
|
+
fastlife/service/security_policy.py,sha256=qYXs4mhfz_u4x59NhUkirqKYKQbFv9YrzyRuXj7mxE0,4688
|
1717
1717
|
fastlife/service/templates.py,sha256=QPAIUbbZiekazz_jV3q4JCwQd6Q4KA6a4RDek2RWuhE,2548
|
1718
|
-
fastlife/service/
|
1718
|
+
fastlife/service/translatablestring.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
1719
|
+
fastlife/service/translations.py,sha256=s6qFZSXR-1vYxSr7RRH-mS-VjNaa8OTxR7-k6Ib7h0E,6878
|
1719
1720
|
fastlife/settings.py,sha256=q-rz4CEF2RQGow5-m-yZJOvdh3PPb2c1Q_ZLJGnu4VQ,3647
|
1720
1721
|
fastlife/shared_utils/__init__.py,sha256=i66ytuf-Ezo7jSiNQHIsBMVIcB-tDX0tg28-pUOlhzE,26
|
1721
1722
|
fastlife/shared_utils/infer.py,sha256=0GflLkaWJ-4LZ1Ig3moR-_o55wwJ_p_vJ4xo-yi3lyA,1406
|
@@ -1725,12 +1726,12 @@ fastlife/testing/__init__.py,sha256=VpxkS3Zp3t_hH8dBiLaGFGhsvt511dhBS_8fMoFXdmU,
|
|
1725
1726
|
fastlife/testing/dom.py,sha256=dVzDoZokn-ii681UaEwAr-khM5KE-CHgXSSLSo24oH0,4489
|
1726
1727
|
fastlife/testing/form.py,sha256=ST0xNCoUqz_oD92cWHzQ6CbJ5hFopvu_NNKpOfiuYWY,7874
|
1727
1728
|
fastlife/testing/session.py,sha256=LEFFbiR67_x_g-ioudkY0C7PycHdbDfaIaoo_G7GXQ8,2226
|
1728
|
-
fastlife/testing/testclient.py,sha256=
|
1729
|
+
fastlife/testing/testclient.py,sha256=TWGbxagxwFEm7_SooItjLchhF5ptHwhyKUh7uhWN_CA,6731
|
1729
1730
|
fastlife/views/__init__.py,sha256=zG8gveL8e2zBdYx6_9jtZfpQ6qJT-MFnBY3xXkLwHZI,22
|
1730
1731
|
fastlife/views/pydantic_form.py,sha256=o7EUItciAGL1OSaGNHo-3BTrYAk34GuWE7zGikjiAGY,1486
|
1731
|
-
fastlifeweb-0.
|
1732
|
-
fastlifeweb-0.
|
1733
|
-
fastlifeweb-0.
|
1734
|
-
fastlifeweb-0.
|
1732
|
+
fastlifeweb-0.23.1.dist-info/METADATA,sha256=O3yeVKCL9s-zEyG6Gsl6Z8UmrX9T9cMwkkOgz0bkw58,3663
|
1733
|
+
fastlifeweb-0.23.1.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
|
1734
|
+
fastlifeweb-0.23.1.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
1735
|
+
fastlifeweb-0.23.1.dist-info/licenses/LICENSE,sha256=NlRX9Z-dcv8X1VFW9odlIQBbgNN9pcO94XzvKp2R16o,1075
|
1735
1736
|
tailwind.config.js,sha256=EN3EahBDmQBbmJvkw3SdGWNOkfkzw0cg-QvBikOhkrw,1348
|
1736
|
-
fastlifeweb-0.
|
1737
|
+
fastlifeweb-0.23.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|