future-framework 3.1.1__tar.gz → 3.2.0__tar.gz
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.
- {future_framework-3.1.1 → future_framework-3.2.0}/PKG-INFO +2 -3
- {future_framework-3.1.1 → future_framework-3.2.0}/README.md +1 -2
- {future_framework-3.1.1 → future_framework-3.2.0}/future/application.py +52 -17
- future_framework-3.2.0/future/authentication/Auth0Authentication.py +15 -0
- future_framework-3.2.0/future/authentication/AuthenticationComponent.py +78 -0
- future_framework-3.2.0/future/authentication/AzureADAuthentication.py +15 -0
- future_framework-3.2.0/future/authentication/BasicAuthentication.py +48 -0
- future_framework-3.2.0/future/authentication/Identity.py +34 -0
- future_framework-3.2.0/future/authentication/KerberosAuthentication.py +15 -0
- future_framework-3.2.0/future/authentication/KeycloakAuthentication.py +15 -0
- future_framework-3.2.0/future/authentication/OAuth2Authentication.py +15 -0
- future_framework-3.2.0/future/authentication/OpenIdConnectAuthentication.py +15 -0
- future_framework-3.2.0/future/authentication/SAMLAuthentication.py +15 -0
- future_framework-3.2.0/future/authentication/__init__.py +3 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/cli.py +49 -3
- future_framework-3.2.0/future/container.py +180 -0
- future_framework-3.2.0/future/interfaces/IAuthentication.py +11 -0
- future_framework-3.2.0/future/interfaces/IMiddleware.py +49 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/logger.py +43 -26
- future_framework-3.2.0/future/middleware/AuthenticationMiddleware.py +27 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/middleware/ScopeValidationMiddleware.py +5 -6
- future_framework-3.2.0/future/middleware/__init__.py +7 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/request.py +7 -1
- {future_framework-3.1.1 → future_framework-3.2.0}/future/routing.py +10 -10
- {future_framework-3.1.1 → future_framework-3.2.0}/future/types.py +1 -1
- {future_framework-3.1.1 → future_framework-3.2.0}/pyproject.toml +1 -1
- {future_framework-3.1.1 → future_framework-3.2.0}/pyproject.toml.orig +1 -1
- future_framework-3.1.1/future/authentication/Auth0Authentication.py +0 -9
- future_framework-3.1.1/future/authentication/AzureADAuthentication.py +0 -9
- future_framework-3.1.1/future/authentication/BasicAuthentication.py +0 -9
- future_framework-3.1.1/future/authentication/KerberosAuthentication.py +0 -9
- future_framework-3.1.1/future/authentication/KeycloakAuthentication.py +0 -9
- future_framework-3.1.1/future/authentication/OAuth2Authentication.py +0 -9
- future_framework-3.1.1/future/authentication/OpenIdConnectAuthentication.py +0 -9
- future_framework-3.1.1/future/authentication/SAMLAuthentication.py +0 -9
- future_framework-3.1.1/future/interfaces/IAuthentication.py +0 -5
- future_framework-3.1.1/future/interfaces/IMiddleware.py +0 -26
- future_framework-3.1.1/future/plugins/__init__.py +0 -0
- future_framework-3.1.1/future/tasks/__init__.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/LICENSE +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/__init__.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/controllers/DebugController.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/controllers/GraphQLController.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/controllers/OpenAPIController.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/controllers/WebSocketController.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/controllers/WelcomeController.py +0 -0
- {future_framework-3.1.1/future/authentication → future_framework-3.2.0/future/controllers}/__init__.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/database.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/databases/ClickhouseDatabase.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/databases/ElasticsearchDatabase.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/databases/MongoDBDatabase.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/databases/MySQLDatabase.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/databases/PostgresDatabase.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/databases/RedisDatabase.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/databases/SQLiteDatabase.py +0 -0
- {future_framework-3.1.1/future/controllers → future_framework-3.2.0/future/databases}/__init__.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/exceptions.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/interfaces/IController.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/interfaces/IDatabase.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/interfaces/IModel.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/interfaces/IPlugin.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/interfaces/ITask.py +0 -0
- {future_framework-3.1.1/future/databases → future_framework-3.2.0/future/interfaces}/__init__.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/interfacing.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/lifespan.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/middleware/BruteforcePreventionMiddleware.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/middleware/CORSMiddleware.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/middleware/CSRFMiddleware.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/middleware/GZipMiddleware.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/middleware/HTAccessConfuserMiddleware.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/middleware/HeaderConfuserMiddleware.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/middleware/RateLimitMiddleware.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/middleware/ResponseCodeConfuserMiddleware.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/middleware/SQLiConfuserMiddleware.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/middleware/SessionMiddleware.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/middleware/TestMiddlewareRequest.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/middleware/TestMiddlewareResponse.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/middleware/WebServerConfuserMiddleware.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/migrations.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/models/PostModel.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/models/Query.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/models/UserModel.py +0 -0
- {future_framework-3.1.1/future/interfaces → future_framework-3.2.0/future/models}/__init__.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/openapi.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/plugins/ElasticsearchPlugin.py +0 -0
- {future_framework-3.1.1/future/middleware → future_framework-3.2.0/future/plugins}/__init__.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/response.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/scheduling.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/seeder.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/settings.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/tasks/CheckDNSTask.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/tasks/CheckDiskUsageTask.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/tasks/CheckHttpStatusTask.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/tasks/CheckMemoryUsageTask.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/tasks/CheckSSHBannerTask.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/tasks/CheckSystemUptimeTask.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/tasks/DailyBackupTask.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/tasks/SyncTaskExample.py +0 -0
- {future_framework-3.1.1/future/models → future_framework-3.2.0/future/tasks}/__init__.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/taskscheduler.py +0 -0
- {future_framework-3.1.1 → future_framework-3.2.0}/future/testclient.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: future-framework
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: ASGI-based framework for web APIs
|
|
5
5
|
Author: nicolaipre
|
|
6
6
|
License-Expression: MIT
|
|
@@ -50,7 +50,6 @@ Import name is `future` (`from future.application import Future`).
|
|
|
50
50
|
```python
|
|
51
51
|
from future.application import Future
|
|
52
52
|
from future.interfaces.IController import IController
|
|
53
|
-
from future.lifespan import Lifespan
|
|
54
53
|
from future.response import Response
|
|
55
54
|
from future.routing import Get, RouteGroup
|
|
56
55
|
|
|
@@ -58,7 +57,7 @@ class HomeController(IController):
|
|
|
58
57
|
async def index(self) -> Response:
|
|
59
58
|
return self.response.json({"ok": True})
|
|
60
59
|
|
|
61
|
-
app = Future(
|
|
60
|
+
app = Future(config={"APP_DOMAIN": "", "APP_NAME": "Demo"})
|
|
62
61
|
app.add_routes([RouteGroup(name="Main", routes=[Get("/", HomeController.index, "home")])])
|
|
63
62
|
app.run(host="127.0.0.1", port=8000)
|
|
64
63
|
```
|
|
@@ -20,7 +20,6 @@ Import name is `future` (`from future.application import Future`).
|
|
|
20
20
|
```python
|
|
21
21
|
from future.application import Future
|
|
22
22
|
from future.interfaces.IController import IController
|
|
23
|
-
from future.lifespan import Lifespan
|
|
24
23
|
from future.response import Response
|
|
25
24
|
from future.routing import Get, RouteGroup
|
|
26
25
|
|
|
@@ -28,7 +27,7 @@ class HomeController(IController):
|
|
|
28
27
|
async def index(self) -> Response:
|
|
29
28
|
return self.response.json({"ok": True})
|
|
30
29
|
|
|
31
|
-
app = Future(
|
|
30
|
+
app = Future(config={"APP_DOMAIN": "", "APP_NAME": "Demo"})
|
|
32
31
|
app.add_routes([RouteGroup(name="Main", routes=[Get("/", HomeController.index, "home")])])
|
|
33
32
|
app.run(host="127.0.0.1", port=8000)
|
|
34
33
|
```
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import platform
|
|
2
2
|
import sys
|
|
3
3
|
import logging
|
|
4
|
+
import inspect
|
|
4
5
|
import traceback
|
|
5
6
|
|
|
6
7
|
from collections.abc import Sequence
|
|
@@ -16,6 +17,8 @@ from rich.table import Table
|
|
|
16
17
|
from rich.text import Text
|
|
17
18
|
|
|
18
19
|
from future.exceptions import ErrorHandler, HTTPException
|
|
20
|
+
from future.authentication.AuthenticationComponent import AuthenticationComponent
|
|
21
|
+
from future.container import Container
|
|
19
22
|
from future.logger import create_uvicorn_logging_config, log
|
|
20
23
|
from future.interfaces.IMiddleware import IMiddleware
|
|
21
24
|
from future.openapi import get_openapi_config, is_docs_path, openapi_routes, rebuild_spec_from_routes, set_openapi_config, spec_path
|
|
@@ -72,16 +75,28 @@ while True:
|
|
|
72
75
|
|
|
73
76
|
|
|
74
77
|
class Future:
|
|
75
|
-
def __init__(self, lifespan: Any, config: dict[str, Any] | None = None) -> None:
|
|
78
|
+
def __init__(self, lifespan: Any | None = None, config: dict[str, Any] | None = None) -> None:
|
|
76
79
|
# spawn background tasks in the lifespan startup process... or database connections etc...
|
|
77
80
|
# on shutdown, save shit, send info about shutdown etc...
|
|
81
|
+
if lifespan is None:
|
|
82
|
+
from future.lifespan import Lifespan
|
|
83
|
+
lifespan = Lifespan()
|
|
78
84
|
self.lifespan = lifespan
|
|
79
85
|
self.routes: dict[str, dict[str, RouteConfig]] = {}
|
|
80
86
|
self.config = config or {}
|
|
87
|
+
self._container = Container(self.config)
|
|
88
|
+
self._container.instance(Container, self._container)
|
|
89
|
+
self._container.configure_bindings(self.config.get("BINDINGS"))
|
|
81
90
|
self.databases = self.config.get("DATABASES")
|
|
82
91
|
if self.databases:
|
|
83
92
|
from future.database import Database
|
|
84
|
-
Database()
|
|
93
|
+
database = Database()
|
|
94
|
+
database.set_connection_details(self.databases)
|
|
95
|
+
self._container.component(database, Database)
|
|
96
|
+
|
|
97
|
+
authentication = self.config.get("AUTHENTICATION")
|
|
98
|
+
if authentication:
|
|
99
|
+
self._container.component(AuthenticationComponent(self._container, authentication), AuthenticationComponent)
|
|
85
100
|
|
|
86
101
|
domain = self.config.get("APP_DOMAIN", "")
|
|
87
102
|
|
|
@@ -117,9 +132,26 @@ class Future:
|
|
|
117
132
|
|
|
118
133
|
def set_config(self, config: dict[str, Any]) -> None:
|
|
119
134
|
self.config = config
|
|
135
|
+
self._container.config = config
|
|
120
136
|
set_openapi_config(self.config)
|
|
121
137
|
log.setLevel(logging.DEBUG if bool(self.config.get("APP_DEBUG")) else logging.INFO)
|
|
122
138
|
|
|
139
|
+
def override(self, dependency: Any, value: Any) -> None:
|
|
140
|
+
"""Replace a resolved dependency, primarily for application tests."""
|
|
141
|
+
self._container.override(dependency, value)
|
|
142
|
+
|
|
143
|
+
def _middleware_priority(self, middleware: Any) -> int:
|
|
144
|
+
return int(getattr(middleware, "priority", 0))
|
|
145
|
+
|
|
146
|
+
def _bind_middleware(self, definition: Any, request: Request, response: Response) -> IMiddleware:
|
|
147
|
+
if inspect.isclass(definition):
|
|
148
|
+
middleware = self._container.resolve(definition, provided={Request: request, Response: response})
|
|
149
|
+
else:
|
|
150
|
+
middleware = definition
|
|
151
|
+
if not isinstance(middleware, IMiddleware):
|
|
152
|
+
raise TypeError(f"Middleware must extend IMiddleware, got {type(middleware).__name__}")
|
|
153
|
+
return middleware.bind(request, response, self._container)
|
|
154
|
+
|
|
123
155
|
def _resolve_controller_action(self, endpoint: Any) -> tuple[Any, str | None]:
|
|
124
156
|
qual = getattr(endpoint, "__qualname__", "") or ""
|
|
125
157
|
mod_name = getattr(endpoint, "__module__", None)
|
|
@@ -149,8 +181,8 @@ class Future:
|
|
|
149
181
|
|
|
150
182
|
middleware_classes: list[Any] = []
|
|
151
183
|
if parent_middlewares:
|
|
152
|
-
middleware_classes.extend(sorted(parent_middlewares, key=
|
|
153
|
-
middleware_classes.extend(sorted(route.middlewares, key=
|
|
184
|
+
middleware_classes.extend(sorted(parent_middlewares, key=self._middleware_priority))
|
|
185
|
+
middleware_classes.extend(sorted(route.middlewares, key=self._middleware_priority))
|
|
154
186
|
|
|
155
187
|
controller_cls, action = self._resolve_controller_action(route.endpoint)
|
|
156
188
|
|
|
@@ -378,15 +410,18 @@ class Future:
|
|
|
378
410
|
|
|
379
411
|
started = False
|
|
380
412
|
try:
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
413
|
+
await self._container.start()
|
|
414
|
+
try:
|
|
415
|
+
self.lifespan.app = app
|
|
416
|
+
async with self.lifespan as state:
|
|
417
|
+
if state is not None:
|
|
418
|
+
scope.setdefault("state", {}).update(state)
|
|
419
|
+
await send({"type": AsgiEventType.LIFESPAN_STARTUP_COMPLETE})
|
|
420
|
+
started = True
|
|
421
|
+
message = await receive()
|
|
422
|
+
assert message["type"] == AsgiEventType.LIFESPAN_SHUTDOWN
|
|
423
|
+
finally:
|
|
424
|
+
await self._container.stop()
|
|
390
425
|
|
|
391
426
|
except BaseException:
|
|
392
427
|
event_type = AsgiEventType.LIFESPAN_SHUTDOWN_FAILED if started else AsgiEventType.LIFESPAN_STARTUP_FAILED
|
|
@@ -446,7 +481,7 @@ class Future:
|
|
|
446
481
|
|
|
447
482
|
request.route = matched_route.get("route")
|
|
448
483
|
middleware_classes = matched_route["middleware"].get("classes") or []
|
|
449
|
-
middleware_instances = [
|
|
484
|
+
middleware_instances = [self._bind_middleware(middleware, request, response) for middleware in middleware_classes]
|
|
450
485
|
for mw in middleware_instances:
|
|
451
486
|
early = await mw.before()
|
|
452
487
|
if early is not None:
|
|
@@ -456,7 +491,7 @@ class Future:
|
|
|
456
491
|
controller_cls = matched_route.get("controller")
|
|
457
492
|
action = matched_route.get("action")
|
|
458
493
|
if controller_cls and action:
|
|
459
|
-
ctrl = controller_cls
|
|
494
|
+
ctrl = self._container.resolve(controller_cls, provided={Request: request, Response: response})
|
|
460
495
|
method = getattr(ctrl, action)
|
|
461
496
|
if route_params:
|
|
462
497
|
result = await method(**route_params)
|
|
@@ -527,7 +562,7 @@ class Future:
|
|
|
527
562
|
request.route = matched_route.get("route")
|
|
528
563
|
try:
|
|
529
564
|
middleware_classes = matched_route["middleware"].get("classes") or []
|
|
530
|
-
middleware_instances = [
|
|
565
|
+
middleware_instances = [self._bind_middleware(middleware, request, response) for middleware in middleware_classes]
|
|
531
566
|
for mw in middleware_instances:
|
|
532
567
|
early = await mw.before()
|
|
533
568
|
if early is not None:
|
|
@@ -537,7 +572,7 @@ class Future:
|
|
|
537
572
|
controller_cls = matched_route.get("controller")
|
|
538
573
|
action = matched_route.get("action")
|
|
539
574
|
if controller_cls and action:
|
|
540
|
-
ctrl = controller_cls
|
|
575
|
+
ctrl = self._container.resolve(controller_cls, provided={Request: request, Response: response})
|
|
541
576
|
method = getattr(ctrl, action)
|
|
542
577
|
if route_params:
|
|
543
578
|
result = await method(**route_params)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auth0 authentication.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from future.interfaces.IAuthentication import IAuthentication
|
|
6
|
+
from future.authentication.Identity import Identity
|
|
7
|
+
from future.request import Request
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Auth0Authentication(IAuthentication):
|
|
11
|
+
auth_type = "oauth"
|
|
12
|
+
|
|
13
|
+
async def authenticate(self, request: Request) -> Identity | None:
|
|
14
|
+
message = "Auth0 authentication is not implemented yet"
|
|
15
|
+
raise NotImplementedError(message)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from future.authentication.Identity import Identity
|
|
8
|
+
from future.container import Container
|
|
9
|
+
from future.request import Request
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthenticationComponent:
|
|
13
|
+
"""Registry and lifecycle owner for named authentication providers."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, container: Container, config: dict[str, Any] | None = None) -> None:
|
|
16
|
+
self._container = container
|
|
17
|
+
self.config = config or {}
|
|
18
|
+
self.default = self.config.get("default")
|
|
19
|
+
self.providers: dict[str, Any] = {}
|
|
20
|
+
for name, definition in self.config.get("providers", {}).items():
|
|
21
|
+
self.register(name, self._build_provider(name, definition))
|
|
22
|
+
if self.default is None and len(self.providers) == 1:
|
|
23
|
+
self.default = next(iter(self.providers))
|
|
24
|
+
|
|
25
|
+
def register(self, name: str, provider: Any) -> None:
|
|
26
|
+
if not name:
|
|
27
|
+
raise ValueError("Authentication provider names cannot be empty")
|
|
28
|
+
if not callable(getattr(provider, "authenticate", None)):
|
|
29
|
+
raise TypeError(f"Authentication provider {name!r} must define authenticate(request)")
|
|
30
|
+
self.providers[name] = provider
|
|
31
|
+
|
|
32
|
+
def get(self, name: str | None = None) -> Any:
|
|
33
|
+
selected = name or self.default
|
|
34
|
+
if not selected:
|
|
35
|
+
raise LookupError("No default authentication provider is configured")
|
|
36
|
+
try:
|
|
37
|
+
return self.providers[selected]
|
|
38
|
+
except KeyError as exc:
|
|
39
|
+
raise LookupError(f"Authentication provider {selected!r} is not configured") from exc
|
|
40
|
+
|
|
41
|
+
async def authenticate(self, request: Request, provider: str | None = None) -> Identity | None:
|
|
42
|
+
selected = provider or self.default
|
|
43
|
+
authenticator = self.get(selected)
|
|
44
|
+
result = authenticator.authenticate(request)
|
|
45
|
+
if inspect.isawaitable(result):
|
|
46
|
+
result = await result
|
|
47
|
+
if result is not None and not isinstance(result, Identity):
|
|
48
|
+
raise TypeError(f"Authentication provider {selected!r} must return Identity or None, got {type(result).__name__}")
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
async def start(self) -> None:
|
|
52
|
+
for provider in self.providers.values():
|
|
53
|
+
start = getattr(provider, "start", None)
|
|
54
|
+
if start is not None:
|
|
55
|
+
result = start()
|
|
56
|
+
if inspect.isawaitable(result):
|
|
57
|
+
await result
|
|
58
|
+
|
|
59
|
+
async def stop(self) -> None:
|
|
60
|
+
for provider in reversed(list(self.providers.values())):
|
|
61
|
+
stop = getattr(provider, "stop", None)
|
|
62
|
+
if stop is not None:
|
|
63
|
+
result = stop()
|
|
64
|
+
if inspect.isawaitable(result):
|
|
65
|
+
await result
|
|
66
|
+
|
|
67
|
+
def _build_provider(self, name: str, definition: Any) -> Any:
|
|
68
|
+
if not isinstance(definition, dict):
|
|
69
|
+
if inspect.isclass(definition):
|
|
70
|
+
return self._container.resolve(definition)
|
|
71
|
+
return definition
|
|
72
|
+
driver = definition.get("driver")
|
|
73
|
+
if driver is None:
|
|
74
|
+
raise ValueError(f"Authentication provider {name!r} is missing its driver")
|
|
75
|
+
options = definition.get("options") or {}
|
|
76
|
+
if not isinstance(options, dict):
|
|
77
|
+
raise TypeError(f"Authentication provider {name!r} options must be a dictionary")
|
|
78
|
+
return self._container.resolve(driver, options=options)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Azure AD authentication.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from future.interfaces.IAuthentication import IAuthentication
|
|
6
|
+
from future.authentication.Identity import Identity
|
|
7
|
+
from future.request import Request
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AzureADAuthentication(IAuthentication):
|
|
11
|
+
auth_type = "basic"
|
|
12
|
+
|
|
13
|
+
async def authenticate(self, request: Request) -> Identity | None:
|
|
14
|
+
message = "Azure AD authentication is not implemented yet"
|
|
15
|
+
raise NotImplementedError(message)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""HTTP Basic authentication backed by an application credential verifier."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import binascii
|
|
5
|
+
import inspect
|
|
6
|
+
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from future.authentication.Identity import Identity
|
|
11
|
+
from future.interfaces.IAuthentication import IAuthentication
|
|
12
|
+
from future.request import Request
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BasicAuthentication(IAuthentication):
|
|
16
|
+
auth_type = "basic"
|
|
17
|
+
|
|
18
|
+
def __init__(self, verify_credentials: Callable[[str, str], Any], realm: str = "Future") -> None:
|
|
19
|
+
self.verify_credentials = verify_credentials
|
|
20
|
+
self.challenge = f'Basic realm="{realm}"'
|
|
21
|
+
|
|
22
|
+
async def authenticate(self, request: Request) -> Identity | None:
|
|
23
|
+
authorization = request.headers.get("authorization", "")
|
|
24
|
+
scheme, _, encoded = authorization.partition(" ")
|
|
25
|
+
if scheme.lower() != "basic" or not encoded:
|
|
26
|
+
return None
|
|
27
|
+
try:
|
|
28
|
+
username, password = base64.b64decode(encoded, validate=True).decode("utf-8").split(":", 1)
|
|
29
|
+
except (binascii.Error, ValueError, UnicodeDecodeError):
|
|
30
|
+
return None
|
|
31
|
+
result = self.verify_credentials(username, password)
|
|
32
|
+
if inspect.isawaitable(result):
|
|
33
|
+
result = await result
|
|
34
|
+
if result is None or result is False:
|
|
35
|
+
return None
|
|
36
|
+
if isinstance(result, Identity):
|
|
37
|
+
return result
|
|
38
|
+
if result is True:
|
|
39
|
+
return Identity(subject=username, provider="basic")
|
|
40
|
+
if isinstance(result, str):
|
|
41
|
+
return Identity(subject=result, provider="basic")
|
|
42
|
+
if isinstance(result, dict):
|
|
43
|
+
claims = dict(result.get("claims") or {})
|
|
44
|
+
subject = str(result.get("subject") or result.get("id") or username)
|
|
45
|
+
roles = tuple(str(role) for role in result.get("roles", ()))
|
|
46
|
+
scopes = tuple(str(scope) for scope in result.get("scopes", ()))
|
|
47
|
+
return Identity(subject=subject, provider="basic", claims=claims, roles=roles, scopes=scopes)
|
|
48
|
+
raise TypeError(f"Basic credential verifier must return Identity, dict, str, bool, or None, got {type(result).__name__}")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Identity:
|
|
7
|
+
__slots__ = ("subject", "provider", "claims", "roles", "scopes", "_initialized")
|
|
8
|
+
subject: str
|
|
9
|
+
provider: str
|
|
10
|
+
claims: dict[str, Any]
|
|
11
|
+
roles: tuple[str, ...]
|
|
12
|
+
scopes: tuple[str, ...]
|
|
13
|
+
_initialized: bool
|
|
14
|
+
|
|
15
|
+
def __init__(self, subject: str, provider: str, claims: dict[str, Any] | None = None, roles: tuple[str, ...] = (), scopes: tuple[str, ...] = ()) -> None:
|
|
16
|
+
object.__setattr__(self, "subject", subject)
|
|
17
|
+
object.__setattr__(self, "provider", provider)
|
|
18
|
+
object.__setattr__(self, "claims", dict(claims or {}))
|
|
19
|
+
object.__setattr__(self, "roles", tuple(roles))
|
|
20
|
+
object.__setattr__(self, "scopes", tuple(scopes))
|
|
21
|
+
object.__setattr__(self, "_initialized", True)
|
|
22
|
+
|
|
23
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
24
|
+
if getattr(self, "_initialized", False):
|
|
25
|
+
raise AttributeError("Identity is immutable")
|
|
26
|
+
object.__setattr__(self, name, value)
|
|
27
|
+
|
|
28
|
+
def __eq__(self, other: object) -> bool:
|
|
29
|
+
if not isinstance(other, Identity):
|
|
30
|
+
return False
|
|
31
|
+
return (self.subject, self.provider, self.claims, self.roles, self.scopes) == (other.subject, other.provider, other.claims, other.roles, other.scopes)
|
|
32
|
+
|
|
33
|
+
def __repr__(self) -> str:
|
|
34
|
+
return f"Identity(subject={self.subject!r}, provider={self.provider!r}, claims={self.claims!r}, roles={self.roles!r}, scopes={self.scopes!r})"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kerberos Single Sign-On authentication.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from future.interfaces.IAuthentication import IAuthentication
|
|
6
|
+
from future.authentication.Identity import Identity
|
|
7
|
+
from future.request import Request
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class KerberosAuthentication(IAuthentication):
|
|
11
|
+
auth_type = "sso"
|
|
12
|
+
|
|
13
|
+
async def authenticate(self, request: Request) -> Identity | None:
|
|
14
|
+
message = "Kerberos authentication is not implemented yet"
|
|
15
|
+
raise NotImplementedError(message)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Keycloak authentication.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from future.interfaces.IAuthentication import IAuthentication
|
|
6
|
+
from future.authentication.Identity import Identity
|
|
7
|
+
from future.request import Request
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class KeycloakAuthentication(IAuthentication):
|
|
11
|
+
auth_type = "oauth"
|
|
12
|
+
|
|
13
|
+
async def authenticate(self, request: Request) -> Identity | None:
|
|
14
|
+
message = "Keycloak authentication is not implemented yet"
|
|
15
|
+
raise NotImplementedError(message)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OAuth 2 authentication.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from future.interfaces.IAuthentication import IAuthentication
|
|
6
|
+
from future.authentication.Identity import Identity
|
|
7
|
+
from future.request import Request
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OAuth2Authentication(IAuthentication):
|
|
11
|
+
auth_type = "oauth"
|
|
12
|
+
|
|
13
|
+
async def authenticate(self, request: Request) -> Identity | None:
|
|
14
|
+
message = "OAuth 2 authentication is not implemented yet"
|
|
15
|
+
raise NotImplementedError(message)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenID Connect authentication.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from future.interfaces.IAuthentication import IAuthentication
|
|
6
|
+
from future.authentication.Identity import Identity
|
|
7
|
+
from future.request import Request
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OpenIdConnectAuthentication(IAuthentication):
|
|
11
|
+
auth_type = "oidc"
|
|
12
|
+
|
|
13
|
+
async def authenticate(self, request: Request) -> Identity | None:
|
|
14
|
+
message = "OpenID Connect authentication is not implemented yet"
|
|
15
|
+
raise NotImplementedError(message)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SAML Single Sign-On authentication.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from future.interfaces.IAuthentication import IAuthentication
|
|
6
|
+
from future.authentication.Identity import Identity
|
|
7
|
+
from future.request import Request
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SAMLAuthentication(IAuthentication):
|
|
11
|
+
auth_type = "sso"
|
|
12
|
+
|
|
13
|
+
async def authenticate(self, request: Request) -> Identity | None:
|
|
14
|
+
message = "SAML authentication is not implemented yet"
|
|
15
|
+
raise NotImplementedError(message)
|
|
@@ -11,7 +11,7 @@ from typing import Any
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
APP_ROOT = "app"
|
|
14
|
-
APP_DIRS = ["config", "controllers", "middleware", "models", "plugins", "tasks"]
|
|
14
|
+
APP_DIRS = ["authentication", "config", "controllers", "interfaces", "middleware", "models", "plugins", "repositories", "services", "tasks"]
|
|
15
15
|
|
|
16
16
|
STUB_KINDS = {
|
|
17
17
|
"readme": {
|
|
@@ -172,7 +172,7 @@ STUB_KINDS = {
|
|
|
172
172
|
"template": dedent("""
|
|
173
173
|
from app.controllers.ExampleController import ExampleController
|
|
174
174
|
from app.middleware.ExampleMiddleware import ExampleMiddleware
|
|
175
|
-
from future.
|
|
175
|
+
from future.controllers.OpenAPIController import OpenAPIController
|
|
176
176
|
from future.routing import RouteGroup, Get
|
|
177
177
|
|
|
178
178
|
|
|
@@ -181,7 +181,14 @@ STUB_KINDS = {
|
|
|
181
181
|
name="Docs",
|
|
182
182
|
prefix="/api",
|
|
183
183
|
# middlewares=[ExampleMiddleware], # protect docs if needed
|
|
184
|
-
routes=
|
|
184
|
+
routes=[
|
|
185
|
+
Get("/openapi.json", OpenAPIController.openapi, "openapi"),
|
|
186
|
+
Get("/swagger-config", OpenAPIController.swagger_config, "swagger-config"),
|
|
187
|
+
Get("/docs", OpenAPIController.swagger, "docs"),
|
|
188
|
+
Get("/redoc", OpenAPIController.redoc, "redoc"),
|
|
189
|
+
Get("/scalar", OpenAPIController.scalar, "scalar"),
|
|
190
|
+
Get("/rapidoc", OpenAPIController.rapidoc, "rapidoc"),
|
|
191
|
+
],
|
|
185
192
|
),
|
|
186
193
|
RouteGroup(
|
|
187
194
|
name="Main",
|
|
@@ -332,6 +339,36 @@ STUB_KINDS = {
|
|
|
332
339
|
""").lstrip("\n"),
|
|
333
340
|
"init": {"class_name": "ExampleMiddleware"},
|
|
334
341
|
},
|
|
342
|
+
"authentication": {
|
|
343
|
+
"directory": "app/authentication",
|
|
344
|
+
"suffix": "Authentication",
|
|
345
|
+
"template": dedent("""
|
|
346
|
+
from future.authentication.Identity import Identity
|
|
347
|
+
from future.interfaces.IAuthentication import IAuthentication
|
|
348
|
+
from future.request import Request
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class {class_name}(IAuthentication):
|
|
352
|
+
async def authenticate(self, request: Request) -> Identity | None:
|
|
353
|
+
return None
|
|
354
|
+
""").lstrip("\n"),
|
|
355
|
+
},
|
|
356
|
+
"repository": {
|
|
357
|
+
"directory": "app/repositories",
|
|
358
|
+
"suffix": "Repository",
|
|
359
|
+
"template": dedent("""
|
|
360
|
+
class {class_name}:
|
|
361
|
+
pass
|
|
362
|
+
""").lstrip("\n"),
|
|
363
|
+
},
|
|
364
|
+
"service": {
|
|
365
|
+
"directory": "app/services",
|
|
366
|
+
"suffix": "Service",
|
|
367
|
+
"template": dedent("""
|
|
368
|
+
class {class_name}:
|
|
369
|
+
pass
|
|
370
|
+
""").lstrip("\n"),
|
|
371
|
+
},
|
|
335
372
|
"plugin": {
|
|
336
373
|
"directory": "app/plugins",
|
|
337
374
|
"suffix": "Plugin",
|
|
@@ -399,6 +436,9 @@ class StubMaker:
|
|
|
399
436
|
def make(self, kind, name):
|
|
400
437
|
if kind not in STUB_KINDS:
|
|
401
438
|
raise ValueError(f"Unknown stub kind: {kind}")
|
|
439
|
+
is_interface_name = name.startswith("I") and len(name) > 1 and name[1].isupper()
|
|
440
|
+
if is_interface_name:
|
|
441
|
+
raise ValueError("The I prefix is reserved for interfaces in app/interfaces")
|
|
402
442
|
config = STUB_KINDS[kind]
|
|
403
443
|
if "directory" not in config:
|
|
404
444
|
raise ValueError(f"Stub kind {kind!r} is not a make:* target")
|
|
@@ -651,6 +691,9 @@ examples:
|
|
|
651
691
|
future routes
|
|
652
692
|
future make:model Trade
|
|
653
693
|
future make:controller Trade
|
|
694
|
+
future make:service Trade
|
|
695
|
+
future make:repository Trade
|
|
696
|
+
future make:authentication LDAP
|
|
654
697
|
future make:migration Trade
|
|
655
698
|
future make:migrations
|
|
656
699
|
future make:seed Trade
|
|
@@ -690,10 +733,13 @@ examples:
|
|
|
690
733
|
subparsers.add_parser("make:seeds", help="Generate seeders for all annotated models (skips existing files)")
|
|
691
734
|
|
|
692
735
|
stub_help = {
|
|
736
|
+
"authentication": "Create app/authentication/<Name>Authentication.py",
|
|
693
737
|
"model": "Create app/models/<Name>.py",
|
|
694
738
|
"controller": "Create app/controllers/<Name>Controller.py",
|
|
695
739
|
"middleware": "Create app/middleware/<Name>Middleware.py",
|
|
696
740
|
"plugin": "Create app/plugins/<Name>Plugin.py",
|
|
741
|
+
"repository": "Create app/repositories/<Name>Repository.py",
|
|
742
|
+
"service": "Create app/services/<Name>Service.py",
|
|
697
743
|
"task": "Create app/tasks/<Name>.py",
|
|
698
744
|
}
|
|
699
745
|
for kind, help_text in stub_help.items():
|