fastlifeweb 0.20.1__py3-none-any.whl → 0.22.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.
Files changed (93) hide show
  1. CHANGELOG.md +18 -1
  2. fastlife/__init__.py +45 -13
  3. fastlife/adapters/__init__.py +1 -1
  4. fastlife/adapters/fastapi/__init__.py +9 -0
  5. fastlife/adapters/fastapi/form.py +26 -0
  6. fastlife/{request → adapters/fastapi}/form_data.py +1 -1
  7. fastlife/{request → adapters/fastapi}/localizer.py +4 -2
  8. fastlife/adapters/fastapi/request.py +33 -0
  9. fastlife/{routing → adapters/fastapi/routing}/route.py +3 -3
  10. fastlife/{routing → adapters/fastapi/routing}/router.py +1 -1
  11. fastlife/adapters/itsdangerous/__init__.py +3 -0
  12. fastlife/adapters/itsdangerous/session.py +50 -0
  13. fastlife/adapters/jinjax/jinjax_ext/inspectable_component.py +7 -7
  14. fastlife/adapters/jinjax/jinjax_ext/jinjax_doc.py +1 -1
  15. fastlife/adapters/jinjax/renderer.py +9 -57
  16. fastlife/adapters/jinjax/widget_factory/bool_builder.py +2 -2
  17. fastlife/adapters/jinjax/widget_factory/emailstr_builder.py +5 -4
  18. fastlife/adapters/jinjax/widget_factory/enum_builder.py +2 -2
  19. fastlife/adapters/jinjax/widget_factory/factory.py +32 -23
  20. fastlife/adapters/jinjax/widget_factory/literal_builder.py +7 -6
  21. fastlife/adapters/jinjax/widget_factory/model_builder.py +3 -3
  22. fastlife/adapters/jinjax/widget_factory/secretstr_builder.py +2 -2
  23. fastlife/adapters/jinjax/widget_factory/sequence_builder.py +3 -3
  24. fastlife/adapters/jinjax/widget_factory/set_builder.py +2 -2
  25. fastlife/adapters/jinjax/widget_factory/simpletype_builder.py +7 -8
  26. fastlife/adapters/jinjax/widget_factory/union_builder.py +3 -3
  27. fastlife/adapters/jinjax/widgets/base.py +36 -36
  28. fastlife/adapters/jinjax/widgets/boolean.py +13 -34
  29. fastlife/adapters/jinjax/widgets/checklist.py +36 -42
  30. fastlife/adapters/jinjax/widgets/dropdown.py +32 -38
  31. fastlife/adapters/jinjax/widgets/hidden.py +7 -15
  32. fastlife/adapters/jinjax/widgets/model.py +36 -43
  33. fastlife/adapters/jinjax/widgets/sequence.py +63 -42
  34. fastlife/adapters/jinjax/widgets/text.py +39 -78
  35. fastlife/adapters/jinjax/widgets/union.py +51 -58
  36. fastlife/components/CsrfToken.jinja +1 -1
  37. fastlife/components/Form.jinja +1 -1
  38. fastlife/components/pydantic_form/FatalError.jinja +8 -0
  39. fastlife/components/pydantic_form/Widget.jinja +4 -3
  40. fastlife/config/__init__.py +3 -6
  41. fastlife/config/configurator.py +80 -32
  42. fastlife/config/exceptions.py +0 -2
  43. fastlife/config/resources.py +1 -2
  44. fastlife/config/views.py +2 -4
  45. fastlife/domain/__init__.py +1 -0
  46. fastlife/domain/model/__init__.py +1 -0
  47. fastlife/domain/model/asgi.py +3 -0
  48. fastlife/domain/model/csrf.py +19 -0
  49. fastlife/{request → domain/model}/form.py +13 -22
  50. fastlife/{request → domain/model}/request.py +26 -30
  51. fastlife/domain/model/security_policy.py +105 -0
  52. fastlife/{templates/inline.py → domain/model/template.py} +8 -0
  53. fastlife/domain/model/types.py +17 -0
  54. fastlife/middlewares/base.py +1 -1
  55. fastlife/middlewares/reverse_proxy/x_forwarded.py +1 -2
  56. fastlife/middlewares/session/__init__.py +2 -2
  57. fastlife/middlewares/session/middleware.py +4 -3
  58. fastlife/middlewares/session/serializer.py +0 -44
  59. fastlife/{services/policy.py → service/check_permission.py} +1 -1
  60. fastlife/{security → service}/csrf.py +5 -15
  61. fastlife/{services → service}/locale_negociator.py +5 -8
  62. fastlife/{config → service}/registry.py +13 -7
  63. fastlife/service/security_policy.py +100 -0
  64. fastlife/{services → service}/templates.py +10 -48
  65. fastlife/{services → service}/translations.py +15 -0
  66. fastlife/{config/settings.py → settings.py} +6 -12
  67. fastlife/shared_utils/infer.py +24 -1
  68. fastlife/{templates/constants.py → template_globals.py} +2 -2
  69. fastlife/testing/testclient.py +2 -2
  70. fastlife/views/__init__.py +1 -0
  71. fastlife/views/pydantic_form.py +6 -0
  72. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/METADATA +1 -1
  73. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/RECORD +79 -80
  74. tailwind.config.js +1 -1
  75. fastlife/components/pydantic_form/Boolean.jinja +0 -13
  76. fastlife/components/pydantic_form/Checklist.jinja +0 -21
  77. fastlife/components/pydantic_form/Dropdown.jinja +0 -18
  78. fastlife/components/pydantic_form/Hidden.jinja +0 -3
  79. fastlife/components/pydantic_form/Model.jinja +0 -30
  80. fastlife/components/pydantic_form/Sequence.jinja +0 -47
  81. fastlife/components/pydantic_form/Text.jinja +0 -11
  82. fastlife/components/pydantic_form/Textarea.jinja +0 -38
  83. fastlife/components/pydantic_form/Union.jinja +0 -34
  84. fastlife/request/__init__.py +0 -5
  85. fastlife/security/__init__.py +0 -1
  86. fastlife/security/policy.py +0 -188
  87. fastlife/templates/__init__.py +0 -12
  88. fastlife/templates/binding.py +0 -52
  89. /fastlife/{routing → adapters/fastapi/routing}/__init__.py +0 -0
  90. /fastlife/{services → service}/__init__.py +0 -0
  91. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/WHEEL +0 -0
  92. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/entry_points.txt +0 -0
  93. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/licenses/LICENSE +0 -0
@@ -11,7 +11,6 @@ from .configurator import VENUSIAN_CATEGORY, Configurator
11
11
  def exception_handler(
12
12
  exception: type[Exception],
13
13
  *,
14
- template: str | None = None,
15
14
  status_code: int | None = None,
16
15
  ) -> Callable[..., Any]:
17
16
  """
@@ -34,7 +33,6 @@ def exception_handler(
34
33
  config.add_exception_handler(
35
34
  exception,
36
35
  wrapped,
37
- template=template,
38
36
  **({} if status_code is None else {"status_code": status_code}),
39
37
  )
40
38
 
@@ -170,8 +170,7 @@ def resource_view(
170
170
  `collection_head`, `collection_options`, `get`, `post`, `put`, `patch`, `delete`,
171
171
  `head` or `options`.
172
172
 
173
- :param permission: a permission to validate by the
174
- :attr:`fastlife.config.settings.Settings.check_permission` function.
173
+ :param permission: a permission to validate by the security policy.
175
174
  :param status_code: returned status_code
176
175
  :param summary: OpenAPI summary for the route.
177
176
  :param description:OpenAPI description for the route.
fastlife/config/views.py CHANGED
@@ -31,7 +31,6 @@ def view_config(
31
31
  path: str,
32
32
  *,
33
33
  permission: str | None = None,
34
- template: str | None = None,
35
34
  status_code: int | None = None,
36
35
  methods: list[str] | None = None,
37
36
  ) -> Callable[..., Any]:
@@ -46,9 +45,9 @@ def view_config(
46
45
  :param path: path of the route, use `{curly_brace}` to inject FastAPI Path
47
46
  parameters.
48
47
  :param template: the template rendered by the
49
- {class}`fastlife.services.templates.AbstractTemplateRenderer`.
48
+ {class}`fastlife.service.templates.AbstractTemplateRenderer`.
50
49
  :param permission: a permission to validate by the
51
- {class}`Security Policy <fastlife.security.policy.AbstractSecurityPolicy>`.
50
+ {class}`Security Policy <fastlife.service.security_policy.AbstractSecurityPolicy>`.
52
51
  :param status_code: customize response status code.
53
52
  :param methods: restrict route to a list of http methods.
54
53
 
@@ -72,7 +71,6 @@ def view_config(
72
71
  permission=permission,
73
72
  status_code=status_code,
74
73
  methods=methods,
75
- template=template,
76
74
  )
77
75
 
78
76
  venusian.attach(wrapped, callback, category=VENUSIAN_CATEGORY) # type: ignore
@@ -0,0 +1 @@
1
+ """Framework core domain."""
@@ -0,0 +1 @@
1
+ """Core Domain Classes."""
@@ -0,0 +1,3 @@
1
+ from starlette.types import ASGIApp, Message, Receive, Scope, Send
2
+
3
+ __all__ = ["ASGIApp", "Message", "Receive", "Scope", "Send"]
@@ -0,0 +1,19 @@
1
+ """Models relative to the security."""
2
+
3
+ import secrets
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ def create_csrf_token() -> str:
9
+ """A helper that create a csrf token."""
10
+ return secrets.token_urlsafe(5)
11
+
12
+
13
+ class CSRFToken(BaseModel):
14
+ """Represent the CSRF Token"""
15
+
16
+ name: str
17
+ """Name of the token while serialized."""
18
+ value: str
19
+ """Value that must match between parts, cookie and posted form."""
@@ -1,13 +1,10 @@
1
1
  """HTTP Form serialization."""
2
2
 
3
- from collections.abc import Callable, Mapping
3
+ from collections.abc import Mapping
4
4
  from typing import Any, Generic, TypeVar, get_origin
5
5
 
6
- from fastapi import Depends
7
6
  from pydantic import BaseModel, ValidationError
8
7
 
9
- from fastlife import Registry
10
- from fastlife.request.form_data import MappingFormData
11
8
  from fastlife.shared_utils.infer import is_union
12
9
 
13
10
  T = TypeVar("T", bound=BaseModel)
@@ -17,14 +14,16 @@ T = TypeVar("T", bound=BaseModel)
17
14
  class FormModel(Generic[T]):
18
15
  prefix: str
19
16
  model: T
20
- errors: Mapping[str, str]
17
+ fatal_error: str
18
+ errors: dict[str, str]
21
19
  is_valid: bool
22
20
 
23
21
  def __init__(
24
- self, prefix: str, model: T, errors: Mapping[str, Any], is_valid: bool = False
22
+ self, prefix: str, model: T, errors: dict[str, Any], is_valid: bool = False
25
23
  ) -> None:
26
24
  self.prefix = prefix
27
25
  self.model = model
26
+ self.fatal_error = ""
28
27
  self.errors = errors
29
28
  self.is_valid = is_valid
30
29
 
@@ -32,6 +31,14 @@ class FormModel(Generic[T]):
32
31
  def default(cls, prefix: str, pydantic_type: type[T]) -> "FormModel[T]":
33
32
  return cls(prefix, pydantic_type.model_construct(), {})
34
33
 
34
+ def set_fatal_error(self, value: str) -> None:
35
+ self.fatal_error = value
36
+ self.is_valid = False
37
+
38
+ def add_error(self, field: str, value: str) -> None:
39
+ self.errors[f"{self.prefix}.{field}"] = value
40
+ self.is_valid = False
41
+
35
42
  def edit(self, pydantic_type: T) -> None:
36
43
  """
37
44
  Load the form with the given model and consider it as valid for the user.
@@ -86,19 +93,3 @@ class FormModel(Generic[T]):
86
93
  errors[loc] = error["msg"]
87
94
  model = pydantic_type.model_construct(**data.get(prefix, {}))
88
95
  return cls(prefix, model, errors)
89
-
90
-
91
- def form_model(
92
- cls: type[T], name: str | None = None
93
- ) -> Callable[[Mapping[str, Any]], FormModel[T]]:
94
- """
95
- Build a model, a class of type T based on Pydandic Base Model from a form payload.
96
- """
97
-
98
- def to_model(data: MappingFormData, registry: Registry) -> FormModel[T]:
99
- prefix = name or registry.settings.form_data_model_prefix
100
- if not data:
101
- return FormModel[T].default(prefix, cls)
102
- return FormModel[T].from_payload(prefix, cls, data)
103
-
104
- return Depends(to_model)
@@ -1,20 +1,20 @@
1
1
  """HTTP Request representation in a python object."""
2
2
 
3
- from typing import TYPE_CHECKING, Annotated, Any, Generic
3
+ from typing import TYPE_CHECKING, Any, Generic
4
4
 
5
- from fastapi import Request as FastAPIRequest
6
- from fastapi.params import Depends
5
+ from starlette.requests import Request as BaseRequest
7
6
 
8
- from fastlife.config.registry import DefaultRegistry, TRegistry
7
+ from fastlife.domain.model.csrf import CSRFToken, create_csrf_token
8
+ from fastlife.service.registry import TRegistry
9
9
 
10
10
  if TYPE_CHECKING:
11
- from fastlife.security.policy import ( # coverage: ignore
11
+ from fastlife.service.security_policy import ( # coverage: ignore
12
12
  AbstractSecurityPolicy,
13
13
  HasPermission,
14
14
  )
15
15
 
16
16
 
17
- class GenericRequest(FastAPIRequest, Generic[TRegistry]):
17
+ class GenericRequest(BaseRequest, Generic[TRegistry]):
18
18
  """HTTP Request representation."""
19
19
 
20
20
  registry: TRegistry
@@ -25,11 +25,29 @@ class GenericRequest(FastAPIRequest, Generic[TRegistry]):
25
25
  security_policy: "AbstractSecurityPolicy[Any, TRegistry] | None"
26
26
  """Request locale used for the i18n of the response."""
27
27
 
28
- def __init__(self, registry: TRegistry, request: FastAPIRequest) -> None:
28
+ renderer_globals: dict[str, Any]
29
+
30
+ def __init__(self, registry: TRegistry, request: BaseRequest) -> None:
29
31
  super().__init__(request.scope, request.receive)
30
32
  self.registry = registry
31
33
  self.locale_name = registry.locale_negociator(self)
32
34
  self.security_policy = None # build it from the ? registry
35
+ self.renderer_globals = {}
36
+ self._csrf_token: CSRFToken | None = None
37
+
38
+ @property
39
+ def csrf_token(self) -> CSRFToken:
40
+ if self._csrf_token is None:
41
+ name = self.registry.settings.csrf_token_name
42
+ value = self.cookies.get(name) or create_csrf_token()
43
+ self._csrf_token = CSRFToken(name=name, value=value)
44
+ return self._csrf_token
45
+
46
+ def add_renderer_globals(self, **kwargs: Any) -> None:
47
+ """
48
+ Add global variables to the template renderer context for the current request.
49
+ """
50
+ self.renderer_globals.update(kwargs)
33
51
 
34
52
  async def has_permission(
35
53
  self, permission: str
@@ -45,29 +63,7 @@ class GenericRequest(FastAPIRequest, Generic[TRegistry]):
45
63
  if self.security_policy is None:
46
64
  raise RuntimeError(
47
65
  f"Request {self.url.path} require a security policy, "
48
- "explicit fastlife.security.policy.InsecurePolicy is required."
66
+ "explicit fastlife.service.security_policy.InsecurePolicy is required."
49
67
  )
50
68
 
51
69
  return await self.security_policy.has_permission(permission)
52
-
53
-
54
- def get_request(request: FastAPIRequest) -> GenericRequest[Any]:
55
- return request # type: ignore
56
-
57
-
58
- Request = Annotated[GenericRequest[DefaultRegistry], Depends(get_request)]
59
- """A request that is associated to the default registry."""
60
- # FastAPI handle its Request objects using a lenient_issubclass,
61
- # basically a issubclass(Request), doe to the Generic[T], it does not work.
62
-
63
-
64
- AnyRequest = Annotated[GenericRequest[Any], Depends(get_request)]
65
- """A request version that is associated to the any registry."""
66
-
67
-
68
- def get_registry(request: Request) -> DefaultRegistry:
69
- return request.registry
70
-
71
-
72
- Registry = Annotated[DefaultRegistry, Depends(get_registry)]
73
- """FastAPI dependency to access to the registry."""
@@ -0,0 +1,105 @@
1
+ """Security policy."""
2
+
3
+ import logging
4
+ from collections.abc import Callable, Coroutine
5
+ from typing import Any, Literal, TypeVar
6
+
7
+ from fastapi import HTTPException
8
+ from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
9
+
10
+ CheckPermissionHook = Callable[..., Coroutine[Any, Any, None]] | Callable[..., None]
11
+ CheckPermission = Callable[[str], CheckPermissionHook]
12
+
13
+ TUser = TypeVar("TUser")
14
+
15
+ log = logging.getLogger(__name__)
16
+
17
+
18
+ class Unauthorized(HTTPException):
19
+ """An exception raised to stop a request exectution and return an HTTP Error."""
20
+
21
+ def __init__(
22
+ self,
23
+ status_code: int = HTTP_401_UNAUTHORIZED,
24
+ detail: str = "Unauthorized",
25
+ headers: dict[str, str] | None = None,
26
+ ) -> None:
27
+ super().__init__(status_code, detail, headers)
28
+
29
+
30
+ class Forbidden(HTTPException):
31
+ """An exception raised to stop a request exectution and return an HTTP Error."""
32
+
33
+ def __init__(
34
+ self,
35
+ status_code: int = HTTP_403_FORBIDDEN,
36
+ detail: str = "Forbidden",
37
+ headers: dict[str, str] | None = None,
38
+ ) -> None:
39
+ super().__init__(status_code, detail, headers)
40
+
41
+
42
+ class BoolMeta(type):
43
+ def __bool__(cls) -> bool:
44
+ return cls.kind == "allowed" # type: ignore
45
+
46
+ def __repr__(cls) -> str:
47
+ return cls.reason # type: ignore
48
+
49
+
50
+ class HasPermission(int, metaclass=BoolMeta):
51
+ """
52
+ A type used to know if a permission is allowed or not.
53
+
54
+ It behave has a boolean, but 3 possibilities exists defind has 3 sub-types
55
+ {class}`Allowed` {class}`Unauthenticated` or {class}`Denied`.
56
+
57
+ In many cases Unauthenticated call may redirect to a login page,
58
+ where authenticated user are not redirected. they have an error message,
59
+ or the frontend may use the information to adapt its interface.
60
+ """
61
+
62
+ kind: Literal["allowed", "unauthenticated", "denied"]
63
+ """
64
+ Identified basic information of the response.
65
+ It distinguished unauthenticated and denied to eventually raised 401 over 403 error.
66
+ """
67
+ reason: str
68
+ """A human explanation of the response."""
69
+
70
+ def __new__(cls, reason: str) -> "HasPermission":
71
+ instance = super().__new__(cls)
72
+ instance.reason = reason
73
+ return instance
74
+
75
+ def __repr__(self) -> str:
76
+ return self.reason
77
+
78
+ def __bool__(self) -> bool:
79
+ return self.kind == "allowed"
80
+
81
+
82
+ class Allowed(HasPermission):
83
+ """Represent a permission check result that is allowed."""
84
+
85
+ kind = "allowed"
86
+ reason = "Allowed"
87
+
88
+
89
+ class Unauthenticated(HasPermission):
90
+ """
91
+ Represent a permission check result that is not allowed due to
92
+ missing authentication mechanism.
93
+ """
94
+
95
+ kind = "unauthenticated"
96
+ reason = "Authentication required"
97
+
98
+
99
+ class Denied(HasPermission):
100
+ """
101
+ Represent a permission check result that is not allowed due to lack of permission.
102
+ """
103
+
104
+ kind = "denied"
105
+ reason = "Access denied to this resource"
@@ -20,3 +20,11 @@ class InlineTemplate(BaseModel):
20
20
 
21
21
  template: ClassVar[str]
22
22
  """The template string to render."""
23
+ renderer: ClassVar[str]
24
+ """Template render engine to use."""
25
+
26
+
27
+ class JinjaXTemplate(InlineTemplate):
28
+ """Template that render JinjaX"""
29
+
30
+ renderer = ".jinja"
@@ -0,0 +1,17 @@
1
+ """Types that are serialized over HTTP and forms."""
2
+
3
+ from decimal import Decimal
4
+ from typing import Any
5
+ from uuid import UUID
6
+
7
+ from pydantic.networks import EmailStr
8
+
9
+ Builtins = str | int | str | float | Decimal | UUID | EmailStr
10
+ """Builtins types."""
11
+
12
+
13
+ AnyLiteral = Any
14
+ """
15
+ Something like Literal[...] or Literal[...] which does not exists
16
+ or Idon't know where it is hidden.
17
+ """
@@ -2,7 +2,7 @@
2
2
 
3
3
  import abc
4
4
 
5
- from starlette.types import Receive, Scope, Send
5
+ from fastlife.domain.model.asgi import Receive, Scope, Send
6
6
 
7
7
 
8
8
  class AbstractMiddleware(abc.ABC):
@@ -1,8 +1,7 @@
1
1
  import logging
2
2
  from collections.abc import Sequence
3
3
 
4
- from starlette.types import ASGIApp, Receive, Scope, Send
5
-
4
+ from fastlife.domain.model.asgi import ASGIApp, Receive, Scope, Send
6
5
  from fastlife.middlewares.base import AbstractMiddleware
7
6
 
8
7
  log = logging.getLogger(__name__)
@@ -15,9 +15,9 @@ from fastlife import Configurator, configure
15
15
  from fastlife.shared_utils.resolver import resolve
16
16
 
17
17
  from .middleware import SessionMiddleware
18
- from .serializer import AbsractSessionSerializer, SignedSessionSerializer
18
+ from .serializer import AbsractSessionSerializer
19
19
 
20
- __all__ = ["SessionMiddleware", "AbsractSessionSerializer", "SignedSessionSerializer"]
20
+ __all__ = ["AbsractSessionSerializer", "SessionMiddleware"]
21
21
 
22
22
 
23
23
  @configure
@@ -5,11 +5,11 @@ from typing import Literal
5
5
 
6
6
  from starlette.datastructures import MutableHeaders
7
7
  from starlette.requests import HTTPConnection
8
- from starlette.types import ASGIApp, Message, Receive, Scope, Send
9
8
 
9
+ from fastlife.domain.model.asgi import ASGIApp, Message, Receive, Scope, Send
10
10
  from fastlife.middlewares.base import AbstractMiddleware
11
11
 
12
- from .serializer import AbsractSessionSerializer, SignedSessionSerializer
12
+ from .serializer import AbsractSessionSerializer
13
13
 
14
14
 
15
15
  class SessionMiddleware(AbstractMiddleware):
@@ -17,6 +17,7 @@ class SessionMiddleware(AbstractMiddleware):
17
17
 
18
18
  def __init__(
19
19
  self,
20
+ *,
20
21
  app: ASGIApp,
21
22
  cookie_name: str,
22
23
  secret_key: str,
@@ -25,7 +26,7 @@ class SessionMiddleware(AbstractMiddleware):
25
26
  cookie_same_site: Literal["lax", "strict", "none"] = "lax",
26
27
  cookie_secure: bool = False,
27
28
  cookie_domain: str = "",
28
- serializer: type[AbsractSessionSerializer] = SignedSessionSerializer,
29
+ serializer: type[AbsractSessionSerializer],
29
30
  ) -> None:
30
31
  self.app = app
31
32
  self.max_age = int(duration.total_seconds())
@@ -1,13 +1,9 @@
1
1
  """Serialize session."""
2
2
 
3
3
  import abc
4
- import json
5
- from base64 import b64decode, b64encode
6
4
  from collections.abc import Mapping
7
5
  from typing import Any
8
6
 
9
- import itsdangerous
10
-
11
7
 
12
8
  class AbsractSessionSerializer(abc.ABC):
13
9
  """Session serializer base class"""
@@ -24,43 +20,3 @@ class AbsractSessionSerializer(abc.ABC):
24
20
  def deserialize(self, data: bytes) -> tuple[Mapping[str, Any], bool]:
25
21
  """Derialize the session raw bytes content and return it as a mapping."""
26
22
  ...
27
-
28
-
29
- class SignedSessionSerializer(AbsractSessionSerializer):
30
- """
31
- The default fastlife session serializer.
32
-
33
- It's based on the itsdangerous package to sign the session with a secret key.
34
-
35
- :param secret_key: a secret used to sign the session payload.
36
-
37
- :param max_age: session lifetime in seconds.
38
- """
39
-
40
- def __init__(self, secret_key: str, max_age: int) -> None:
41
- self.signer = itsdangerous.TimestampSigner(secret_key)
42
- self.max_age = max_age
43
-
44
- def serialize(self, data: Mapping[str, Any]) -> bytes:
45
- """Serialize and sign the session."""
46
- dump = json.dumps(data).encode("utf-8")
47
- encoded = b64encode(dump)
48
- signed = self.signer.sign(encoded)
49
- return signed
50
-
51
- def deserialize(self, data: bytes) -> tuple[Mapping[str, Any], bool]:
52
- """Deserialize the session.
53
-
54
- If the signature is incorect, the session restart from the begining.
55
- No exception raised.
56
- """
57
- try:
58
- data = self.signer.unsign(data, max_age=self.max_age)
59
- # We can't deserialize something wrong since the serialize
60
- # is signing the content.
61
- # If the signature key is compromise and we have invalid payload,
62
- # raising exceptions here is fine, it's dangerous afterall.
63
- session = json.loads(b64decode(data))
64
- except itsdangerous.BadSignature:
65
- return {}, True
66
- return session, False
@@ -26,7 +26,7 @@ def check_permission(permission_name: str) -> CheckPermissionHook:
26
26
  if request.security_policy is None:
27
27
  raise RuntimeError(
28
28
  f"Request {request.url.path} require a security policy, "
29
- "explicit fastlife.security.policy.InsecurePolicy is required"
29
+ "explicit fastlife.service.security_policy.InsecurePolicy is required"
30
30
  )
31
31
  allowed = await request.security_policy.has_permission(permission_name)
32
32
  match allowed.kind:
@@ -6,7 +6,7 @@ Fast life did not reinvent the wheel on CSRF Protection.
6
6
  It use the good old method. A CSRF token is saved in a cookie.
7
7
  Forms post the CSRF token, and the token in the cookies and the form must match
8
8
  to process the request, otherwise an exception
9
- {class}`fastlife.security.csrf.CSRFAttack` is raised.
9
+ {class}`fastlife.service.csrf.CSRFAttack` is raised.
10
10
 
11
11
  The cookie named is configurabllefia the settings
12
12
  :attr:`fastlife.config.settings.Settings.csrf_token_name`
@@ -18,11 +18,10 @@ no way to prevent to set the cookie in the request.
18
18
 
19
19
  """
20
20
 
21
- import secrets
22
21
  from collections.abc import Callable, Coroutine
23
22
  from typing import Any
24
23
 
25
- from fastlife.request import Request
24
+ from fastlife.adapters.fastapi.request import Request
26
25
 
27
26
 
28
27
  class CSRFAttack(Exception):
@@ -31,11 +30,6 @@ class CSRFAttack(Exception):
31
30
  """
32
31
 
33
32
 
34
- def create_csrf_token() -> str:
35
- """A helper that create a csrf token."""
36
- return secrets.token_urlsafe(5)
37
-
38
-
39
33
  def check_csrf() -> Callable[[Request], Coroutine[Any, Any, bool]]:
40
34
  """
41
35
  A global application dependency, that is always active.
@@ -56,15 +50,11 @@ def check_csrf() -> Callable[[Request], Coroutine[Any, Any, bool]]:
56
50
  != "application/x-www-form-urlencoded"
57
51
  ):
58
52
  return True
59
- csrf_token_name = request.registry.settings.csrf_token_name
60
-
61
- cookie = request.cookies.get(csrf_token_name)
62
- if not cookie:
63
- raise CSRFAttack("CSRF token did not match")
64
53
 
54
+ token = request.csrf_token
65
55
  form_data = await request.form()
66
- value = form_data.get(csrf_token_name)
67
- if value != cookie:
56
+ value = form_data.get(token.name)
57
+ if value != token.value:
68
58
  raise CSRFAttack("CSRF token did not match")
69
59
 
70
60
  return True
@@ -1,20 +1,17 @@
1
1
  """Find the localization gor the given request."""
2
2
 
3
3
  from collections.abc import Callable
4
- from typing import TYPE_CHECKING, Any
4
+ from typing import Any
5
5
 
6
- from fastlife.config.settings import Settings
6
+ from fastlife.settings import Settings
7
7
 
8
8
  LocaleName = str
9
9
  """The LocaleName is a locale such as en, fr that will be consume for translations."""
10
10
 
11
- if TYPE_CHECKING:
12
- from fastlife.request.request import GenericRequest # coverage: ignore
11
+ from fastlife.adapters.fastapi.request import GenericRequest # coverage: ignore
13
12
 
14
- LocaleNegociator = Callable[[GenericRequest[Any]], LocaleName] # coverage: ignore
15
- """Interface to implement to negociate a locale""" # coverage: ignore
16
- else:
17
- LocaleNegociator = Any
13
+ LocaleNegociator = Callable[[GenericRequest[Any]], LocaleName] # coverage: ignore
14
+ """Interface to implement to negociate a locale""" # coverage: ignore
18
15
 
19
16
 
20
17
  def default_negociator(settings: Settings) -> LocaleNegociator:
@@ -1,15 +1,14 @@
1
1
  from collections.abc import Mapping
2
2
  from typing import TYPE_CHECKING, Generic, TypeVar
3
3
 
4
- from fastlife.services.locale_negociator import LocaleNegociator, default_negociator
5
- from fastlife.services.translations import LocalizerFactory
6
-
7
4
  if TYPE_CHECKING:
8
- from fastlife.services.templates import ( # coverage: ignore
5
+ from fastlife.service.locale_negociator import LocaleNegociator # coverage: ignore
6
+ from fastlife.service.templates import ( # coverage: ignore
9
7
  AbstractTemplateRendererFactory, # coverage: ignore
10
8
  ) # coverage: ignore
9
+ from fastlife.service.translations import LocalizerFactory # coverage: ignore
11
10
 
12
- from .settings import Settings
11
+ from fastlife.settings import Settings
13
12
 
14
13
  TSettings = TypeVar("TSettings", bound=Settings, covariant=True)
15
14
  """
@@ -24,11 +23,18 @@ class GenericRegistry(Generic[TSettings]):
24
23
  """
25
24
 
26
25
  settings: Settings
26
+ """Application settings."""
27
27
  renderers: Mapping[str, "AbstractTemplateRendererFactory"]
28
- locale_negociator: LocaleNegociator
29
- localizer: LocalizerFactory
28
+ """Registered template engine."""
29
+ locale_negociator: "LocaleNegociator"
30
+ """Used to fine the best language for the response."""
31
+ localizer: "LocalizerFactory"
32
+ """Used to localized message."""
30
33
 
31
34
  def __init__(self, settings: Settings) -> None:
35
+ from fastlife.service.locale_negociator import default_negociator
36
+ from fastlife.service.translations import LocalizerFactory
37
+
32
38
  self.settings = settings
33
39
  self.locale_negociator = default_negociator(self.settings)
34
40
  self.renderers = {}