fastlifeweb 0.14.0__py3-none-any.whl → 0.15.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.
- fastlife/__init__.py +2 -3
- fastlife/adapters/jinjax/widgets/__init__.py +1 -0
- fastlife/adapters/jinjax/widgets/base.py +1 -0
- fastlife/components/Tbody.jinja +1 -1
- fastlife/components/Tfoot.jinja +1 -1
- fastlife/components/Thead.jinja +1 -1
- fastlife/components/Tr.jinja +1 -1
- fastlife/config/configurator.py +84 -29
- fastlife/config/openapiextra.py +22 -0
- fastlife/config/registry.py +0 -4
- fastlife/config/resources.py +3 -1
- fastlife/config/settings.py +0 -3
- fastlife/request/request.py +28 -1
- fastlife/security/policy.py +158 -13
- fastlife/services/policy.py +39 -0
- {fastlifeweb-0.14.0.dist-info → fastlifeweb-0.15.1.dist-info}/METADATA +6 -5
- {fastlifeweb-0.14.0.dist-info → fastlifeweb-0.15.1.dist-info}/RECORD +19 -17
- {fastlifeweb-0.14.0.dist-info → fastlifeweb-0.15.1.dist-info}/LICENSE +0 -0
- {fastlifeweb-0.14.0.dist-info → fastlifeweb-0.15.1.dist-info}/WHEEL +0 -0
fastlife/__init__.py
CHANGED
@@ -12,14 +12,13 @@ from .config import (
|
|
12
12
|
from .request import Request
|
13
13
|
|
14
14
|
# from .request.form_data import model
|
15
|
-
from .templates import
|
15
|
+
from .services.templates import TemplateParams
|
16
16
|
|
17
17
|
__all__ = [
|
18
18
|
# Config
|
19
19
|
"configure",
|
20
20
|
"Configurator",
|
21
|
-
"
|
22
|
-
"Template",
|
21
|
+
"TemplateParams",
|
23
22
|
"Registry",
|
24
23
|
"Settings",
|
25
24
|
"view_config",
|
@@ -0,0 +1 @@
|
|
1
|
+
"""HTML Form generation using widgets."""
|
fastlife/components/Tbody.jinja
CHANGED
fastlife/components/Tfoot.jinja
CHANGED
fastlife/components/Thead.jinja
CHANGED
fastlife/components/Tr.jinja
CHANGED
fastlife/config/configurator.py
CHANGED
@@ -28,20 +28,22 @@ from fastapi import Response
|
|
28
28
|
from fastapi.params import Depends as DependsType
|
29
29
|
from fastapi.staticfiles import StaticFiles
|
30
30
|
from fastapi.types import IncEx
|
31
|
-
from pydantic import BaseModel, Field
|
32
31
|
|
32
|
+
from fastlife.config.openapiextra import OpenApiTag
|
33
33
|
from fastlife.middlewares.base import AbstractMiddleware
|
34
34
|
from fastlife.request.request import Request
|
35
35
|
from fastlife.routing.route import Route
|
36
36
|
from fastlife.routing.router import Router
|
37
37
|
from fastlife.security.csrf import check_csrf
|
38
|
+
from fastlife.services.policy import check_permission
|
38
39
|
from fastlife.shared_utils.resolver import resolve
|
39
40
|
|
40
41
|
from .settings import Settings
|
41
42
|
|
42
43
|
if TYPE_CHECKING:
|
44
|
+
from fastlife.security.policy import AbstractSecurityPolicy # coverage: ignore
|
43
45
|
from fastlife.services.templates import (
|
44
|
-
AbstractTemplateRendererFactory, # coverage: ignore
|
46
|
+
AbstractTemplateRendererFactory, # coverage: ignore
|
45
47
|
)
|
46
48
|
|
47
49
|
from .registry import AppRegistry, LocaleNegociator # coverage: ignore
|
@@ -58,24 +60,53 @@ class ConfigurationError(Exception):
|
|
58
60
|
"""
|
59
61
|
|
60
62
|
|
61
|
-
|
62
|
-
"""
|
63
|
+
def rebuild_router(router: Router) -> Router:
|
64
|
+
"""
|
65
|
+
Fix the router.
|
63
66
|
|
64
|
-
|
65
|
-
"""link's description."""
|
66
|
-
url: str
|
67
|
-
"""link's URL."""
|
67
|
+
FastAPI routers has dependencies that are injected to routes where they are added.
|
68
68
|
|
69
|
+
It means that if you add a dependencies in the router after the route has
|
70
|
+
been added, then the dependencies is missing in the route added before.
|
69
71
|
|
70
|
-
|
71
|
-
"""OpenAPI tag object."""
|
72
|
+
To prenvents issues, we rebuild the router route with the dependency.
|
72
73
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
74
|
+
:param router: the router to rebuild
|
75
|
+
:return: a new router with fixed routes.
|
76
|
+
"""
|
77
|
+
if not router.dependencies:
|
78
|
+
return router
|
79
|
+
_router = Router(prefix=router.prefix)
|
80
|
+
_router.dependencies = router.dependencies
|
81
|
+
route: Route
|
82
|
+
for route in router.routes: # type: ignore
|
83
|
+
dependencies = [
|
84
|
+
dep for dep in route.dependencies if dep not in _router.dependencies
|
85
|
+
]
|
86
|
+
_router.add_api_route(
|
87
|
+
path=route.path,
|
88
|
+
endpoint=route.endpoint,
|
89
|
+
response_model=route.response_model,
|
90
|
+
status_code=route.status_code,
|
91
|
+
tags=route.tags,
|
92
|
+
dependencies=dependencies,
|
93
|
+
summary=route.summary,
|
94
|
+
description=route.description,
|
95
|
+
response_description=route.response_description,
|
96
|
+
deprecated=route.deprecated,
|
97
|
+
methods=route.methods,
|
98
|
+
operation_id=route.operation_id,
|
99
|
+
response_model_include=route.response_model_include,
|
100
|
+
response_model_exclude=route.response_model_exclude,
|
101
|
+
response_model_by_alias=route.response_model_by_alias,
|
102
|
+
response_model_exclude_unset=route.response_model_exclude_unset,
|
103
|
+
response_model_exclude_defaults=route.response_model_exclude_defaults,
|
104
|
+
response_model_exclude_none=route.response_model_exclude_none,
|
105
|
+
include_in_schema=route.include_in_schema,
|
106
|
+
name=route.name,
|
107
|
+
openapi_extra=route.openapi_extra,
|
108
|
+
)
|
109
|
+
return _router
|
79
110
|
|
80
111
|
|
81
112
|
class Configurator:
|
@@ -107,6 +138,7 @@ class Configurator:
|
|
107
138
|
|
108
139
|
self._route_prefix: str = ""
|
109
140
|
self._routers: dict[str, Router] = defaultdict(Router)
|
141
|
+
self._security_policies: dict[str, "type[AbstractSecurityPolicy[Any]]"] = {}
|
110
142
|
|
111
143
|
self.scanner = venusian.Scanner(fastlife=self)
|
112
144
|
self.include("fastlife.views")
|
@@ -145,8 +177,6 @@ class Configurator:
|
|
145
177
|
else None,
|
146
178
|
)
|
147
179
|
app.router.route_class = Route
|
148
|
-
for prefix, router in self._routers.items():
|
149
|
-
app.include_router(router, prefix=prefix)
|
150
180
|
|
151
181
|
for middleware_class, options in self.middlewares:
|
152
182
|
app.add_middleware(middleware_class, **options) # type: ignore
|
@@ -154,6 +184,9 @@ class Configurator:
|
|
154
184
|
for status_code_or_exc, exception_handler in self.exception_handlers:
|
155
185
|
app.add_exception_handler(status_code_or_exc, exception_handler)
|
156
186
|
|
187
|
+
for prefix, router in self._routers.items():
|
188
|
+
app.include_router(rebuild_router(router), prefix=prefix)
|
189
|
+
|
157
190
|
for route_path, directory, name in self.mounts:
|
158
191
|
app.mount(route_path, StaticFiles(directory=directory), name=name)
|
159
192
|
return app
|
@@ -262,6 +295,27 @@ class Configurator:
|
|
262
295
|
self.middlewares.append((middleware_class, options))
|
263
296
|
return self
|
264
297
|
|
298
|
+
def set_security_policy(
|
299
|
+
self, security_policy: "type[AbstractSecurityPolicy[Any]]"
|
300
|
+
) -> Self:
|
301
|
+
"""
|
302
|
+
Set a security policy for the application.
|
303
|
+
|
304
|
+
```{important}
|
305
|
+
The security policy is **per route_prefix**.
|
306
|
+
It means that if the application is splitted via multiple
|
307
|
+
route_prefix using the {meth}`Configurator.include`, they
|
308
|
+
all have a distinct security policy. A secutity policy has
|
309
|
+
to be install by all of those include call.
|
310
|
+
|
311
|
+
:param security_policy: The security policy that will applied for the app
|
312
|
+
portion behind the route prefix.
|
313
|
+
```
|
314
|
+
"""
|
315
|
+
self._security_policies[self._route_prefix] = security_policy
|
316
|
+
self._current_router.dependencies.append(Depends(security_policy))
|
317
|
+
return self
|
318
|
+
|
265
319
|
def add_api_route(
|
266
320
|
self,
|
267
321
|
name: str,
|
@@ -311,11 +365,9 @@ class Configurator:
|
|
311
365
|
:param path: path of the route, use `{curly_brace}` to inject FastAPI Path
|
312
366
|
parameters.
|
313
367
|
:param endpoint: the function that will reveive the request.
|
314
|
-
:param permission: a permission to validate by the
|
315
|
-
:attr:`fastlife.config.settings.Settings.check_permission` function.
|
316
|
-
|
368
|
+
:param permission: a permission to validate by the security policy.
|
317
369
|
:param methods: restrict route to a list of http methods.
|
318
|
-
:param description:OpenAPI description for the route.
|
370
|
+
:param description:{term}`OpenAPI` description for the route.
|
319
371
|
:param summary: OpenAPI summary for the route.
|
320
372
|
:param response_description: OpenAPI description for the response.
|
321
373
|
:param operation_id: OpenAPI optional unique string used to identify an
|
@@ -333,13 +385,13 @@ class Configurator:
|
|
333
385
|
:param response_model_exclude_none: exclude fields instead of serialize to
|
334
386
|
null value.
|
335
387
|
:param include_in_schema: expose or not the route in the doc.
|
336
|
-
:param openapi_extra:
|
388
|
+
:param openapi_extra: OpenAPI documentation extra fields.
|
337
389
|
|
338
390
|
:return: the configurator.
|
339
391
|
"""
|
340
392
|
dependencies: list[DependsType] = []
|
341
393
|
if permission:
|
342
|
-
dependencies.append(Depends(
|
394
|
+
dependencies.append(Depends(check_permission(permission)))
|
343
395
|
|
344
396
|
self._current_router.add_api_route(
|
345
397
|
path,
|
@@ -394,15 +446,13 @@ class Configurator:
|
|
394
446
|
:param path: path of the route, use `{curly_brace}` to inject FastAPI Path
|
395
447
|
parameters.
|
396
448
|
:param endpoint: the function that will reveive the request.
|
397
|
-
:param permission: a permission to validate by the
|
398
|
-
:attr:`fastlife.config.settings.Settings.check_permission` function.
|
399
|
-
|
449
|
+
:param permission: a permission to validate by the security policy.
|
400
450
|
:param methods: restrict route to a list of http methods.
|
401
451
|
:return: the configurator.
|
402
452
|
"""
|
403
453
|
dependencies: list[DependsType] = []
|
404
454
|
if permission:
|
405
|
-
dependencies.append(Depends(
|
455
|
+
dependencies.append(Depends(check_permission(permission)))
|
406
456
|
|
407
457
|
if template:
|
408
458
|
|
@@ -459,6 +509,11 @@ class Configurator:
|
|
459
509
|
"""
|
460
510
|
|
461
511
|
def exception_handler(request: BaseRequest, exc: Exception) -> Any:
|
512
|
+
# FastAPI exception handler does not provide our request object
|
513
|
+
# it seems like it is rebuild from the asgi scope. Even the router
|
514
|
+
# class is wrong.
|
515
|
+
# Until we store a security policy per rooter, we rebuild an
|
516
|
+
# incomplete request here.
|
462
517
|
request = Request(self.registry, request)
|
463
518
|
resp = handler(request, exc)
|
464
519
|
if isinstance(resp, Response):
|
@@ -494,7 +549,7 @@ class Configurator:
|
|
494
549
|
self.registry.renderers[f".{file_ext.lstrip('.')}"] = renderer # type: ignore
|
495
550
|
return self
|
496
551
|
|
497
|
-
def add_template_search_path(self, path: str) -> Self:
|
552
|
+
def add_template_search_path(self, path: str | Path) -> Self:
|
498
553
|
"""
|
499
554
|
Add a template search path directly from the code.
|
500
555
|
|
@@ -0,0 +1,22 @@
|
|
1
|
+
"""Types for OpenAPI documentation."""
|
2
|
+
from pydantic import BaseModel, Field
|
3
|
+
|
4
|
+
|
5
|
+
class ExternalDocs(BaseModel):
|
6
|
+
"""OpenAPI externalDocs object."""
|
7
|
+
|
8
|
+
description: str
|
9
|
+
"""link's description."""
|
10
|
+
url: str
|
11
|
+
"""link's URL."""
|
12
|
+
|
13
|
+
|
14
|
+
class OpenApiTag(BaseModel):
|
15
|
+
"""OpenAPI tag object."""
|
16
|
+
|
17
|
+
name: str
|
18
|
+
"""name of the tag."""
|
19
|
+
description: str
|
20
|
+
"""explanation of the tag."""
|
21
|
+
external_docs: ExternalDocs | None = Field(alias="externalDocs", default=None)
|
22
|
+
"""external link to the doc."""
|
fastlife/config/registry.py
CHANGED
@@ -5,9 +5,7 @@ from fastapi import Depends
|
|
5
5
|
from fastapi import Request as FastAPIRequest
|
6
6
|
|
7
7
|
from fastlife.request.request import Request
|
8
|
-
from fastlife.security.policy import CheckPermission
|
9
8
|
from fastlife.services.translations import LocalizerFactory
|
10
|
-
from fastlife.shared_utils.resolver import resolve
|
11
9
|
|
12
10
|
if TYPE_CHECKING:
|
13
11
|
from fastlife.services.templates import ( # coverage: ignore
|
@@ -34,13 +32,11 @@ class AppRegistry:
|
|
34
32
|
|
35
33
|
settings: Settings
|
36
34
|
renderers: Mapping[str, "AbstractTemplateRendererFactory"]
|
37
|
-
check_permission: CheckPermission
|
38
35
|
locale_negociator: LocaleNegociator
|
39
36
|
localizer: LocalizerFactory
|
40
37
|
|
41
38
|
def __init__(self, settings: Settings) -> None:
|
42
39
|
self.settings = settings
|
43
|
-
self.check_permission = resolve(settings.check_permission)
|
44
40
|
self.locale_negociator = _default_negociator(self.settings)
|
45
41
|
self.renderers = {}
|
46
42
|
self.localizer = LocalizerFactory()
|
fastlife/config/resources.py
CHANGED
@@ -13,11 +13,12 @@ from typing import Any, Callable
|
|
13
13
|
import venusian
|
14
14
|
from fastapi.types import IncEx
|
15
15
|
|
16
|
+
from fastlife.config.openapiextra import ExternalDocs
|
17
|
+
|
16
18
|
from .configurator import (
|
17
19
|
VENUSIAN_CATEGORY,
|
18
20
|
ConfigurationError,
|
19
21
|
Configurator,
|
20
|
-
ExternalDocs,
|
21
22
|
OpenApiTag,
|
22
23
|
)
|
23
24
|
|
@@ -143,6 +144,7 @@ def resource(
|
|
143
144
|
|
144
145
|
|
145
146
|
def resource_view(
|
147
|
+
*,
|
146
148
|
permission: str | None = None,
|
147
149
|
status_code: int | None = None,
|
148
150
|
summary: str | None = None,
|
fastlife/config/settings.py
CHANGED
@@ -106,9 +106,6 @@ class Settings(BaseSettings):
|
|
106
106
|
domain_name: str = Field(default="", title="domain name where the app is served")
|
107
107
|
"""Domain name whre the app is served."""
|
108
108
|
|
109
|
-
check_permission: str = Field(default="fastlife.security.policy:check_permission")
|
110
|
-
"""Handler for checking permission set on any views using the configurator."""
|
111
|
-
|
112
109
|
decode_reverse_proxy_headers: bool = Field(default=True)
|
113
110
|
"""Ensure that the request object has information based on http proxy headers."""
|
114
111
|
|
fastlife/request/request.py
CHANGED
@@ -1,11 +1,15 @@
|
|
1
1
|
"""HTTP Request representation in a python object."""
|
2
2
|
|
3
|
-
from typing import TYPE_CHECKING
|
3
|
+
from typing import TYPE_CHECKING, Any
|
4
4
|
|
5
5
|
from fastapi import Request as FastAPIRequest
|
6
6
|
|
7
7
|
if TYPE_CHECKING:
|
8
8
|
from fastlife.config.registry import AppRegistry # coverage: ignore
|
9
|
+
from fastlife.security.policy import ( # coverage: ignore
|
10
|
+
AbstractSecurityPolicy,
|
11
|
+
HasPermission,
|
12
|
+
)
|
9
13
|
|
10
14
|
|
11
15
|
class Request(FastAPIRequest):
|
@@ -16,7 +20,30 @@ class Request(FastAPIRequest):
|
|
16
20
|
locale_name: str
|
17
21
|
"""Request locale used for the i18n of the response."""
|
18
22
|
|
23
|
+
security_policy: "AbstractSecurityPolicy[Any] | None"
|
24
|
+
"""Request locale used for the i18n of the response."""
|
25
|
+
|
19
26
|
def __init__(self, registry: "AppRegistry", request: FastAPIRequest) -> None:
|
20
27
|
super().__init__(request.scope, request.receive)
|
21
28
|
self.registry = registry
|
22
29
|
self.locale_name = registry.locale_negociator(self)
|
30
|
+
self.security_policy = None # build it from the ? registry
|
31
|
+
|
32
|
+
async def has_permission(
|
33
|
+
self, permission: str
|
34
|
+
) -> "HasPermission | type[HasPermission]":
|
35
|
+
"""
|
36
|
+
A helper to check that a user has the given permission.
|
37
|
+
|
38
|
+
Not that this method does not raised, it return a boolean like object.
|
39
|
+
It allows batch permission checks.
|
40
|
+
You might need to check multiple permissions in different contexts or
|
41
|
+
for different resources before raising an http error.
|
42
|
+
"""
|
43
|
+
if self.security_policy is None:
|
44
|
+
raise RuntimeError(
|
45
|
+
f"Request {self.url.path} require a security policy, "
|
46
|
+
"explicit fastlife.security.policy.InsecurePolicy is required."
|
47
|
+
)
|
48
|
+
|
49
|
+
return await self.security_policy.has_permission(permission)
|
fastlife/security/policy.py
CHANGED
@@ -1,26 +1,171 @@
|
|
1
1
|
"""Security policy."""
|
2
|
-
|
2
|
+
|
3
|
+
import abc
|
4
|
+
import logging
|
5
|
+
from typing import Any, Callable, Coroutine, Literal, TypeVar
|
6
|
+
from uuid import UUID
|
7
|
+
|
8
|
+
from fastapi import HTTPException
|
9
|
+
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
|
10
|
+
from typing_extensions import Generic
|
11
|
+
|
12
|
+
from fastlife import Request
|
3
13
|
|
4
14
|
CheckPermissionHook = Callable[..., Coroutine[Any, Any, None]] | Callable[..., None]
|
5
15
|
CheckPermission = Callable[[str], CheckPermissionHook]
|
6
16
|
|
17
|
+
TUser = TypeVar("TUser")
|
18
|
+
|
19
|
+
log = logging.getLogger(__name__)
|
20
|
+
|
21
|
+
|
22
|
+
class Unauthorized(HTTPException):
|
23
|
+
"""An exception raised to stop a request exectution and return an HTTP Error."""
|
24
|
+
|
25
|
+
def __init__(
|
26
|
+
self,
|
27
|
+
status_code: int = HTTP_401_UNAUTHORIZED,
|
28
|
+
detail: str = "Unauthorized",
|
29
|
+
headers: dict[str, str] | None = None,
|
30
|
+
) -> None:
|
31
|
+
super().__init__(status_code, detail, headers)
|
32
|
+
|
33
|
+
|
34
|
+
class Forbidden(HTTPException):
|
35
|
+
"""An exception raised to stop a request exectution and return an HTTP Error."""
|
36
|
+
|
37
|
+
def __init__(
|
38
|
+
self,
|
39
|
+
status_code: int = HTTP_403_FORBIDDEN,
|
40
|
+
detail: str = "Forbidden",
|
41
|
+
headers: dict[str, str] | None = None,
|
42
|
+
) -> None:
|
43
|
+
super().__init__(status_code, detail, headers)
|
44
|
+
|
45
|
+
|
46
|
+
class BoolMeta(type):
|
47
|
+
def __bool__(cls) -> bool:
|
48
|
+
return cls.kind == "allowed" # type: ignore
|
49
|
+
|
50
|
+
def __repr__(cls) -> str:
|
51
|
+
return cls.reason # type: ignore
|
52
|
+
|
53
|
+
|
54
|
+
class HasPermission(int, metaclass=BoolMeta):
|
55
|
+
kind: Literal["allowed", "unauthenticated", "denied"]
|
56
|
+
reason: str
|
57
|
+
|
58
|
+
def __new__(cls, reason: str) -> "HasPermission":
|
59
|
+
instance = super().__new__(cls)
|
60
|
+
instance.reason = reason
|
61
|
+
return instance
|
62
|
+
|
63
|
+
def __repr__(self) -> str:
|
64
|
+
return self.reason
|
65
|
+
|
66
|
+
def __bool__(self) -> bool:
|
67
|
+
return self.kind == "allowed"
|
68
|
+
|
69
|
+
|
70
|
+
class Allowed(HasPermission):
|
71
|
+
"""Represent a permission check result that is allowed."""
|
72
|
+
|
73
|
+
kind = "allowed"
|
74
|
+
reason = "Allowed"
|
75
|
+
|
7
76
|
|
8
|
-
|
77
|
+
class Unauthenticated(HasPermission):
|
78
|
+
"""
|
79
|
+
Represent a permission check result that is not allowed due to
|
80
|
+
missing authentication mechanism.
|
9
81
|
"""
|
10
|
-
A closure that check that a user as the given permission_name.
|
11
82
|
|
12
|
-
|
13
|
-
|
83
|
+
kind = "unauthenticated"
|
84
|
+
reason = "Authentication required"
|
14
85
|
|
15
|
-
When the check permission is properly set in the settings., the hook is called
|
16
|
-
for every route added with a permission keyword.
|
17
|
-
{meth}`fastlife.config.configurator.Configurator.add_route`
|
18
86
|
|
19
|
-
|
20
|
-
|
87
|
+
class Denied(HasPermission):
|
88
|
+
"""
|
89
|
+
Represent a permission check result that is not allowed due to lack of permission.
|
21
90
|
"""
|
22
91
|
|
23
|
-
|
24
|
-
|
92
|
+
kind = "denied"
|
93
|
+
reason = "Access denied to this resource"
|
94
|
+
|
95
|
+
|
96
|
+
class AbstractSecurityPolicy(abc.ABC, Generic[TUser]):
|
97
|
+
"""Security policy base classe."""
|
98
|
+
|
99
|
+
Forbidden = Forbidden
|
100
|
+
"""The exception raised if the user identified is not granted."""
|
101
|
+
Unauthorized = Unauthorized
|
102
|
+
"""The exception raised if no user has been identified."""
|
103
|
+
|
104
|
+
def __init__(self, request: Request):
|
105
|
+
"""
|
106
|
+
Build the security policy.
|
107
|
+
|
108
|
+
When implementing a security policy, multiple parameters can be added
|
109
|
+
to the constructor as FastAPI dependencies, using the `Depends` FastAPI
|
110
|
+
annotation.
|
111
|
+
The security policy is installed has a depenency of the router that hold
|
112
|
+
a route prefix of the application.
|
113
|
+
"""
|
114
|
+
self.request = request
|
115
|
+
self.request.security_policy = self # we do backref to implement has_permission
|
116
|
+
|
117
|
+
@abc.abstractmethod
|
118
|
+
async def identity(self) -> TUser | None:
|
119
|
+
"""
|
120
|
+
Return app-specific user object or raise an HTTPException.
|
121
|
+
"""
|
122
|
+
|
123
|
+
@abc.abstractmethod
|
124
|
+
async def authenticated_userid(self) -> str | UUID | None:
|
125
|
+
"""
|
126
|
+
Return app-specific user object or raise an HTTPException.
|
127
|
+
"""
|
128
|
+
|
129
|
+
@abc.abstractmethod
|
130
|
+
async def has_permission(
|
131
|
+
self, permission: str
|
132
|
+
) -> HasPermission | type[HasPermission]:
|
133
|
+
"""Allow access to everything if signed in."""
|
134
|
+
|
135
|
+
@abc.abstractmethod
|
136
|
+
async def remember(self, user: TUser) -> None:
|
137
|
+
"""Save the user identity in the request session."""
|
138
|
+
|
139
|
+
@abc.abstractmethod
|
140
|
+
async def forget(self) -> None:
|
141
|
+
"""Destroy the request session."""
|
142
|
+
|
143
|
+
|
144
|
+
class InsecurePolicy(AbstractSecurityPolicy[None]):
|
145
|
+
"""
|
146
|
+
An implementation of the security policy made for explicit unsecured access.
|
147
|
+
|
148
|
+
Setting a permission on a view require a security policy, if not set, accessing
|
149
|
+
to a view will raise a RuntimeError. To bypass this error for testing purpose
|
150
|
+
or your own reason, the InsecurePolicy has to be set to the configurator.
|
151
|
+
"""
|
152
|
+
|
153
|
+
async def identity(self) -> None:
|
154
|
+
"""Nobodies is identified."""
|
155
|
+
return None
|
156
|
+
|
157
|
+
async def authenticated_userid(self) -> str | UUID:
|
158
|
+
"""An uuid mades of 0."""
|
159
|
+
return UUID(int=0)
|
160
|
+
|
161
|
+
async def has_permission(
|
162
|
+
self, permission: str
|
163
|
+
) -> HasPermission | type[HasPermission]:
|
164
|
+
"""Access is allways granted."""
|
165
|
+
return Allowed
|
166
|
+
|
167
|
+
async def remember(self, user: None) -> None:
|
168
|
+
"""Do nothing."""
|
25
169
|
|
26
|
-
|
170
|
+
async def forget(self) -> None:
|
171
|
+
"""Do nothing."""
|
@@ -0,0 +1,39 @@
|
|
1
|
+
"""Security policy."""
|
2
|
+
|
3
|
+
from typing import Any, Callable, Coroutine
|
4
|
+
|
5
|
+
CheckPermissionHook = Callable[..., Coroutine[Any, Any, None]] | Callable[..., None]
|
6
|
+
CheckPermission = Callable[[str], CheckPermissionHook]
|
7
|
+
|
8
|
+
|
9
|
+
def check_permission(permission_name: str) -> CheckPermissionHook:
|
10
|
+
"""
|
11
|
+
A closure that check that a user as the given permission_name.
|
12
|
+
|
13
|
+
Adding a permission on the route requires that a security policy has been
|
14
|
+
added using the method
|
15
|
+
{meth}`fastlife.config.configurator.Configurator.set_security_policy`
|
16
|
+
|
17
|
+
:param permission_name: a permission name set in a view to check access.
|
18
|
+
:return: a function that raise http exceptions or any configured exception here.
|
19
|
+
"""
|
20
|
+
# the check_permission is called by the configurator
|
21
|
+
# and the request is exposed in the public module creating a circular dependency.
|
22
|
+
from fastlife import Request # a type must be resolved to inject a dependency.
|
23
|
+
|
24
|
+
async def depencency_injection(request: Request) -> None:
|
25
|
+
if request.security_policy is None:
|
26
|
+
raise RuntimeError(
|
27
|
+
f"Request {request.url.path} require a security policy, "
|
28
|
+
"explicit fastlife.security.policy.InsecurePolicy is required"
|
29
|
+
)
|
30
|
+
allowed = await request.security_policy.has_permission(permission_name)
|
31
|
+
match allowed.kind:
|
32
|
+
case "allowed":
|
33
|
+
return
|
34
|
+
case "denied":
|
35
|
+
raise request.security_policy.Forbidden(detail=allowed.reason)
|
36
|
+
case "unauthenticated":
|
37
|
+
raise request.security_policy.Unauthorized(detail=allowed.reason)
|
38
|
+
|
39
|
+
return depencency_injection
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fastlifeweb
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.15.1
|
4
4
|
Summary: High-level web framework
|
5
5
|
Home-page: https://github.com/mardiros/fastlife
|
6
6
|
License: BSD-derived
|
@@ -17,8 +17,9 @@ Classifier: Programming Language :: Python :: 3.11
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
18
18
|
Classifier: Topic :: Internet :: WWW/HTTP
|
19
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
20
|
+
Provides-Extra: testing
|
20
21
|
Requires-Dist: babel (>=2.16.0,<3.0.0)
|
21
|
-
Requires-Dist: beautifulsoup4
|
22
|
+
Requires-Dist: beautifulsoup4 (>=4.12.2,<5.0.0) ; extra == "testing"
|
22
23
|
Requires-Dist: fastapi[standard] (>=0.115.0,<0.116.0)
|
23
24
|
Requires-Dist: itsdangerous (>=2.1.2,<3.0.0)
|
24
25
|
Requires-Dist: jinjax (>=0.44,<0.45)
|
@@ -42,12 +43,12 @@ Description-Content-Type: text/markdown
|
|
42
43
|
> Please note that this project is still in active development. Features and APIs may change frequently.
|
43
44
|
> Even the name is not definitive.
|
44
45
|
|
45
|
-
An opinionated
|
46
|
+
An opinionated Python web framework (based on FastAPI).
|
46
47
|
|
47
48
|
## Purpose
|
48
49
|
|
49
|
-
Fastlife helps at building Web Application with
|
50
|
-
using customizable widget.
|
50
|
+
Fastlife helps at building Web Application with session, security, html test client,
|
51
|
+
and html form generated from pydantic schema using customizable widget.
|
51
52
|
|
52
53
|
Templates are made using [JinjaX](https://jinjax.scaletti.dev/) and an extensible [set of
|
53
54
|
component](https://mardiros.github.io/fastlife/components/index.html) is available
|
@@ -1,9 +1,9 @@
|
|
1
|
-
fastlife/__init__.py,sha256=
|
1
|
+
fastlife/__init__.py,sha256=sOuA910DThakr7Pn4VY7rGVMbbjU0JP8QQ3VqEpVzkM,538
|
2
2
|
fastlife/adapters/__init__.py,sha256=WYjEN8gp4r7LCHqmIO5VzzvsT8QGRE3w4G47UwYDtAo,94
|
3
3
|
fastlife/adapters/jinjax/__init__.py,sha256=jy88zyqk7nFlaY-0lmgAoe0HyO5r_NKckQb3faQiUv4,137
|
4
4
|
fastlife/adapters/jinjax/renderer.py,sha256=tKX_C0fGnooMcMXBz4_8Ym2VLCKEkRk9_AXq6HrN11I,13040
|
5
|
-
fastlife/adapters/jinjax/widgets/__init__.py,sha256=
|
6
|
-
fastlife/adapters/jinjax/widgets/base.py,sha256=
|
5
|
+
fastlife/adapters/jinjax/widgets/__init__.py,sha256=HERnX9xiXUbTDz3XtlnHWABTBjhIq_kkBgWs5E6ZIMY,42
|
6
|
+
fastlife/adapters/jinjax/widgets/base.py,sha256=JoB1bLI97ZW2ycfBcHgrPxiOszE9t_SFFK5L1bR-cSo,4015
|
7
7
|
fastlife/adapters/jinjax/widgets/boolean.py,sha256=w4hZMo_8xDoThStlIUR4eVfLm8JwUp0-TaGCjGSyCbA,1145
|
8
8
|
fastlife/adapters/jinjax/widgets/checklist.py,sha256=VIIfJ8JB7ZAISwFFiGZ7jfGuNJ9sjkhRSVFqDTr-RR4,1655
|
9
9
|
fastlife/adapters/jinjax/widgets/dropdown.py,sha256=x2Y9BOfHfSuzWD_HNrvCkiJtKDxl8Vs05Uk8QKvRxyY,1622
|
@@ -34,13 +34,13 @@ fastlife/components/Radio.jinja,sha256=ohucEGNofDsC4-Hp6Ovexi5XW812-kfAzsFCcOyVF
|
|
34
34
|
fastlife/components/Select.jinja,sha256=aPqdPrCh6ooxj3XkeptefwvzFqC5XC4FLtW8XLj3CpM,556
|
35
35
|
fastlife/components/Summary.jinja,sha256=EFVG2d7JJu7j5RRLNqKHxXcWAEJmv3Wif1aI5HXq1Zc,853
|
36
36
|
fastlife/components/Table.jinja,sha256=X87pJkqp-fl4Uno52Ijj1Lw4MFYsqr4Jc6Oi4KGQVhM,390
|
37
|
-
fastlife/components/Tbody.jinja,sha256=
|
37
|
+
fastlife/components/Tbody.jinja,sha256=FqldzLFhV6dfG0UsB55kDtALixpnECdIISXGWXiXxEM,331
|
38
38
|
fastlife/components/Td.jinja,sha256=NTV86bdtm8t_2_2Pj1X8vsCqm4NSE4H5NPUoSt1i8cA,374
|
39
39
|
fastlife/components/Textarea.jinja,sha256=0WsTi-yxVwYXndmJJN93WblQVqM0h0NCSMUDwoC3kdc,636
|
40
|
-
fastlife/components/Tfoot.jinja,sha256=
|
40
|
+
fastlife/components/Tfoot.jinja,sha256=Hw_eFhmbHcOx0dCHrNkyi4q1lrKye5RpihHBwjaT3zY,331
|
41
41
|
fastlife/components/Th.jinja,sha256=tuIlLlwtvZiBHhQD_J7p8V-An-EeA4mzm_nRxUoGDzQ,375
|
42
|
-
fastlife/components/Thead.jinja,sha256=
|
43
|
-
fastlife/components/Tr.jinja,sha256=
|
42
|
+
fastlife/components/Thead.jinja,sha256=TpnnbtbgYe97e0QDjeJulbh0_-Cbu3z_Qh9dsraQNb4,330
|
43
|
+
fastlife/components/Tr.jinja,sha256=kWy-jx4ShT2dv9vuHqJtMnc3O2pZ3y8KCWEAifMpjrU,322
|
44
44
|
fastlife/components/icons/AcademicCap.jinja,sha256=0yJiwNF09SXGmssEHFkEE_2GPb2LFN1BIR4DChDRBls,528
|
45
45
|
fastlife/components/icons/AdjustmentsHorizontal.jinja,sha256=eaAOfrTqfmkJmNaFeCiBSzSCohu6_kqNSuFIB1Vz1RE,568
|
46
46
|
fastlife/components/icons/AdjustmentsVertical.jinja,sha256=n7jBi400_q6KnhMABtUwKBBM_7n1IXFtdtaNwamrfxs,560
|
@@ -1666,11 +1666,12 @@ fastlife/components/pydantic_form/Textarea.jinja,sha256=NzfCi5agRUSVcb5RXw0QamM8
|
|
1666
1666
|
fastlife/components/pydantic_form/Union.jinja,sha256=czTska54z9KCZKu-FaycLmOvtH6y6CGUFQ8DHnkjrJk,1461
|
1667
1667
|
fastlife/components/pydantic_form/Widget.jinja,sha256=EXskDqt22D5grpGVwlZA3ndve2Wr_6yQH4qVE9c31Og,397
|
1668
1668
|
fastlife/config/__init__.py,sha256=O_Mw2XOxo55SArHdGKRhlTrroRN8ymwfzYKlHG0eV_s,418
|
1669
|
-
fastlife/config/configurator.py,sha256=
|
1669
|
+
fastlife/config/configurator.py,sha256=jOZvlUVZH7jE1ay9iwVVTudApN1uYNXGQcWT3xaJ5lg,21236
|
1670
1670
|
fastlife/config/exceptions.py,sha256=2MS2MFgb3mDbtdHEwnC-QYubob3Rl0v8O8y615LY0ds,1180
|
1671
|
-
fastlife/config/
|
1672
|
-
fastlife/config/
|
1673
|
-
fastlife/config/
|
1671
|
+
fastlife/config/openapiextra.py,sha256=_9rBYeTqB7nVuzvUHMwZU387bTfYFHYLlP05NP0vEDs,513
|
1672
|
+
fastlife/config/registry.py,sha256=Zm3i9ZfQZOP3vNKNKQ8nar7XevI91bqy2tUXX492Fuk,1749
|
1673
|
+
fastlife/config/resources.py,sha256=pM0j5VKVbVak4Z5mFRHBjAjUqORP4TAtCnZM3res5o8,8776
|
1674
|
+
fastlife/config/settings.py,sha256=FepHFZVHPLy3yA3dQux79GiH65MkipqWyT-zgXUBOKE,4028
|
1674
1675
|
fastlife/config/views.py,sha256=Dxi6lO4gFs6GriAW7Rh5GDvebwbrpS2HzYhf30pXJiE,2058
|
1675
1676
|
fastlife/middlewares/__init__.py,sha256=C3DUOzR5EhlAv5Zq7h-Abyvkd7bUsJohTRSB2wpRYQE,220
|
1676
1677
|
fastlife/middlewares/base.py,sha256=9OYqByRuVoIrLt353NOedPQTLdr7LSmxhb2BZcp20qk,638
|
@@ -1684,14 +1685,15 @@ fastlife/request/__init__.py,sha256=C5lZAsUZJDw0TfTegRyN5BEhzgfiqvfc8YXu2vp0siU,
|
|
1684
1685
|
fastlife/request/form.py,sha256=FucGua79LCKqNBP6Ycle7-5JU6EMI6SrHCgoJBcvGY4,3532
|
1685
1686
|
fastlife/request/form_data.py,sha256=yoP-AYF-dSClpCQuZNRTY-c1OnDga5MoTjBKIzgpTs8,4459
|
1686
1687
|
fastlife/request/localizer.py,sha256=9MXAcsod-Po5qeg4lttD3dyumiI0y5vGHCwSSmt9or8,349
|
1687
|
-
fastlife/request/request.py,sha256=
|
1688
|
+
fastlife/request/request.py,sha256=NiRtEV_iw8BuESQPf7gqXxiHsQwLiyOu1yzQy7Ck9eM,1779
|
1688
1689
|
fastlife/routing/__init__.py,sha256=8EMnQE5n8oA4J9_c3nxzwKDVt3tefZ6fGH0d2owE8mo,195
|
1689
1690
|
fastlife/routing/route.py,sha256=8RMnAqun8GTfWp6PFIWmWu-q-K89M3SiVSi2A-DYqyM,1405
|
1690
1691
|
fastlife/routing/router.py,sha256=bLZ4k2aDs4L423znwGnw-X2LnM36O8fjhDWc8q1WewI,481
|
1691
1692
|
fastlife/security/__init__.py,sha256=QYDcJ3oXQzqXQxoDD_6biGAtercFrtePttoifiL1j34,25
|
1692
1693
|
fastlife/security/csrf.py,sha256=PvC9Fqdb6c0IzzsnaMx2quQdjjKrb-nOPoAHfcwoAe8,2141
|
1693
|
-
fastlife/security/policy.py,sha256=
|
1694
|
+
fastlife/security/policy.py,sha256=3aENmA_plxHbnUKzsIWtCAboT3oyVew0dcJKDrrILqE,4880
|
1694
1695
|
fastlife/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
1696
|
+
fastlife/services/policy.py,sha256=ZK4K3fVGT1fUeBdo5sSWnEcJg49CZuaI3z2gyg3KguQ,1653
|
1695
1697
|
fastlife/services/templates.py,sha256=7gPJxGWD-XqputbZpy_Icsz3WHKJaWg2JgkVOeKrjfA,3840
|
1696
1698
|
fastlife/services/translations.py,sha256=Bo5CIjdbQ3g_ihbv4Bz60hzd8VOtqEEPOyhJEbGcvP4,4363
|
1697
1699
|
fastlife/shared_utils/__init__.py,sha256=i66ytuf-Ezo7jSiNQHIsBMVIcB-tDX0tg28-pUOlhzE,26
|
@@ -1704,7 +1706,7 @@ fastlife/testing/__init__.py,sha256=vuqwoNUd3BuIp3fm7nkvmYkIGjIimf5zUGhDkeWrg2s,
|
|
1704
1706
|
fastlife/testing/testclient.py,sha256=BC7lLQ_jc59UmknAKzgRtW9a3cpX_V_QLp9Mg2ScLA8,20546
|
1705
1707
|
fastlife/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
1706
1708
|
fastlife/views/pydantic_form.py,sha256=fvjk_5-JAugpBPxwD5GkxxsQiz9eAxzWeCSU9kiyc6s,1438
|
1707
|
-
fastlifeweb-0.
|
1708
|
-
fastlifeweb-0.
|
1709
|
-
fastlifeweb-0.
|
1710
|
-
fastlifeweb-0.
|
1709
|
+
fastlifeweb-0.15.1.dist-info/LICENSE,sha256=F75xSseSKMwqzFj8rswYU6NWS3VoWOc_gY3fJYf9_LI,1504
|
1710
|
+
fastlifeweb-0.15.1.dist-info/METADATA,sha256=3OB6Kxf1HPxQD0Qi1GFRt2_LvH8VTEWzZn1WDjbyI4o,3345
|
1711
|
+
fastlifeweb-0.15.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
1712
|
+
fastlifeweb-0.15.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|