fastloom 0.4.2__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.
- fastloom-0.4.2/PKG-INFO +155 -0
- fastloom-0.4.2/README.md +109 -0
- fastloom-0.4.2/fastloom/__init__.py +0 -0
- fastloom-0.4.2/fastloom/auth/__init__.py +5 -0
- fastloom-0.4.2/fastloom/auth/depends.py +145 -0
- fastloom-0.4.2/fastloom/auth/schemas.py +69 -0
- fastloom-0.4.2/fastloom/cache/__init__.py +0 -0
- fastloom-0.4.2/fastloom/cache/base.py +33 -0
- fastloom-0.4.2/fastloom/cache/gate.py +67 -0
- fastloom-0.4.2/fastloom/cache/healthcheck.py +22 -0
- fastloom-0.4.2/fastloom/cache/lifehooks.py +37 -0
- fastloom-0.4.2/fastloom/cache/settings.py +13 -0
- fastloom-0.4.2/fastloom/crypto.py +20 -0
- fastloom-0.4.2/fastloom/date.py +24 -0
- fastloom-0.4.2/fastloom/db/__init__.py +0 -0
- fastloom-0.4.2/fastloom/db/healthcheck.py +22 -0
- fastloom-0.4.2/fastloom/db/lifehooks.py +83 -0
- fastloom-0.4.2/fastloom/db/schemas.py +89 -0
- fastloom-0.4.2/fastloom/db/settings.py +6 -0
- fastloom-0.4.2/fastloom/db/signals.py +135 -0
- fastloom-0.4.2/fastloom/db/transactions.py +48 -0
- fastloom-0.4.2/fastloom/file/__init__.py +0 -0
- fastloom-0.4.2/fastloom/file/models.py +46 -0
- fastloom-0.4.2/fastloom/file/schema.py +112 -0
- fastloom-0.4.2/fastloom/file/signals.py +32 -0
- fastloom-0.4.2/fastloom/file/utils.py +19 -0
- fastloom-0.4.2/fastloom/healthcheck/__init__.py +0 -0
- fastloom-0.4.2/fastloom/healthcheck/handler.py +26 -0
- fastloom-0.4.2/fastloom/i18n/__init__.py +0 -0
- fastloom-0.4.2/fastloom/i18n/base.py +65 -0
- fastloom-0.4.2/fastloom/i18n/handler.py +70 -0
- fastloom-0.4.2/fastloom/i18n/settings.py +7 -0
- fastloom-0.4.2/fastloom/i18n/types.py +24 -0
- fastloom-0.4.2/fastloom/launcher/__init__.py +0 -0
- fastloom-0.4.2/fastloom/launcher/main.py +108 -0
- fastloom-0.4.2/fastloom/launcher/schemas.py +164 -0
- fastloom-0.4.2/fastloom/launcher/settings.py +13 -0
- fastloom-0.4.2/fastloom/launcher/utils.py +93 -0
- fastloom-0.4.2/fastloom/meta.py +57 -0
- fastloom-0.4.2/fastloom/monitoring.py +301 -0
- fastloom-0.4.2/fastloom/observability/__init__.py +0 -0
- fastloom-0.4.2/fastloom/observability/settings.py +10 -0
- fastloom-0.4.2/fastloom/py.typed +0 -0
- fastloom-0.4.2/fastloom/settings/__init__.py +0 -0
- fastloom-0.4.2/fastloom/settings/base.py +38 -0
- fastloom-0.4.2/fastloom/settings/utils.py +14 -0
- fastloom-0.4.2/fastloom/signals/__init__.py +0 -0
- fastloom-0.4.2/fastloom/signals/depends.py +311 -0
- fastloom-0.4.2/fastloom/signals/healthcheck.py +21 -0
- fastloom-0.4.2/fastloom/signals/lifehooks.py +47 -0
- fastloom-0.4.2/fastloom/signals/middlewares.py +60 -0
- fastloom-0.4.2/fastloom/signals/settings.py +7 -0
- fastloom-0.4.2/fastloom/tenant/__init__.py +3 -0
- fastloom-0.4.2/fastloom/tenant/depends.py +218 -0
- fastloom-0.4.2/fastloom/tenant/handler.py +63 -0
- fastloom-0.4.2/fastloom/tenant/protocols.py +16 -0
- fastloom-0.4.2/fastloom/tenant/schemas.py +15 -0
- fastloom-0.4.2/fastloom/tenant/settings.py +212 -0
- fastloom-0.4.2/fastloom/tenant/utils.py +79 -0
- fastloom-0.4.2/fastloom/tests.py +50 -0
- fastloom-0.4.2/fastloom/types.py +88 -0
- fastloom-0.4.2/fastloom/utils.py +27 -0
- fastloom-0.4.2/pyproject.toml +103 -0
fastloom-0.4.2/PKG-INFO
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: fastloom
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: Core package
|
|
5
|
+
Requires-Python: >=3.12,<3.14
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
+
Provides-Extra: celery
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Provides-Extra: fastapi
|
|
12
|
+
Provides-Extra: httpx
|
|
13
|
+
Provides-Extra: mongodb
|
|
14
|
+
Provides-Extra: openai
|
|
15
|
+
Provides-Extra: rabbit
|
|
16
|
+
Provides-Extra: redis
|
|
17
|
+
Provides-Extra: requests
|
|
18
|
+
Requires-Dist: babel (>=2.17.0,<3.0.0)
|
|
19
|
+
Requires-Dist: beanie (>=2.0.0,<3.0.0) ; extra == "mongodb"
|
|
20
|
+
Requires-Dist: celery (>=5.5.3,<6.0.0) ; extra == "celery"
|
|
21
|
+
Requires-Dist: fastapi (>=0,<1) ; extra == "fastapi" or extra == "rabbit"
|
|
22
|
+
Requires-Dist: faststream[otel,rabbit] (>=0.6.0,<0.7.0) ; extra == "rabbit"
|
|
23
|
+
Requires-Dist: httpx (>=0.28.0,<0.29.0) ; extra == "httpx"
|
|
24
|
+
Requires-Dist: ipykernel (>=6.30.0,<7.0.0) ; extra == "dev"
|
|
25
|
+
Requires-Dist: jdatetime (>=4.1.1,<5.0.0)
|
|
26
|
+
Requires-Dist: jinja2 (>=3.1.6,<4.0.0)
|
|
27
|
+
Requires-Dist: logfire[asgi,celery,fastapi,httpx,openai,pymongo,redis,requests,system-metrics] (>=4.0.0,<5.0.0)
|
|
28
|
+
Requires-Dist: mypy (>=1.17.0,<2.0.0) ; extra == "dev"
|
|
29
|
+
Requires-Dist: openai (>=1,<2) ; extra == "openai"
|
|
30
|
+
Requires-Dist: opentelemetry-distro
|
|
31
|
+
Requires-Dist: opentelemetry-exporter-otlp
|
|
32
|
+
Requires-Dist: opentelemetry-instrumentation-aio-pika ; extra == "rabbit"
|
|
33
|
+
Requires-Dist: orjson (==3.10.14)
|
|
34
|
+
Requires-Dist: pre-commit (>=4.2.0,<5.0.0) ; extra == "dev"
|
|
35
|
+
Requires-Dist: pydantic[email] (>=2.12,<3.0)
|
|
36
|
+
Requires-Dist: python-jose (>=3.5.0,<4.0.0)
|
|
37
|
+
Requires-Dist: python-multipart ; extra == "fastapi"
|
|
38
|
+
Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
|
|
39
|
+
Requires-Dist: redis-om (>=1.0,<2.0) ; extra == "redis"
|
|
40
|
+
Requires-Dist: requests (>=2.32.0,<3.0.0) ; extra == "requests"
|
|
41
|
+
Requires-Dist: ruff (>=0.12.5,<0.13.0) ; extra == "dev"
|
|
42
|
+
Requires-Dist: sentry-sdk[fastapi] (>=2.0.0,<3.0.0)
|
|
43
|
+
Requires-Dist: uvicorn (>=0.35.0,<0.36.0) ; extra == "fastapi" or extra == "rabbit"
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# Fastloom – The Open Foundation for Building Event-Driven Services
|
|
47
|
+
|
|
48
|
+
Fastloom is a lightweight, batteries-included foundation for building modern backends. Define your settings, schemas, and endpoints; Fastloom wires up the rest: FastAPI, Mongo (Beanie), Rabbit (FastStream), metrics/traces/logs/errors, and more.
|
|
49
|
+
|
|
50
|
+
Think of it as the glue for your stack: web, messaging, caching, DB, observability, and integrations with best-in-class tools.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Why FastLoom
|
|
55
|
+
|
|
56
|
+
- No boilerplate: minimal scaffolding/templating; most wiring is handled inside Core.
|
|
57
|
+
- Composable: opt into only what you need (`FastAPI`, `Rabbit`, `MongoDB`, `Redis`, `OpenAI`).
|
|
58
|
+
- Pydantic-first: type-safe models, validators, and clear input/output contracts.
|
|
59
|
+
- Multi-tenant by design: tenant context flows through DI and storage.
|
|
60
|
+
- AuthN/Z via DI: OIDC token introspection and pluggable PDP (ABAC/RBAC/ReBAC) hooks.
|
|
61
|
+
- Event-driven ready: publish/subscribe with routing keys and health.
|
|
62
|
+
- Observability-native: metrics, traces, logs from day one.
|
|
63
|
+
- Self-hostable: production parity with a cloud/aaS setup.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Integrated Services (the platform)
|
|
68
|
+
|
|
69
|
+
Core plugs into a family of self-hostable services:
|
|
70
|
+
|
|
71
|
+
- IAM → OIDC/SSO, authN/Z, RBAC/ABAC/ReBAC.
|
|
72
|
+
- Notify → realtime notifications, Pusher-compatible API.
|
|
73
|
+
- Pulse → user activity + event tracking with OpenTelemetry hooks.
|
|
74
|
+
- File → object storage on MinIO (S3-compatible).
|
|
75
|
+
- Finance, Subscription, SMS/Email, Meet, Persona → optional services you can wire in.
|
|
76
|
+
|
|
77
|
+
Each service is:
|
|
78
|
+
- self-hostable (Docker Compose or Helm),
|
|
79
|
+
- BaaS-available,
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Quick start
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Install core
|
|
87
|
+
poetry add core-bluprint -E FastAPI
|
|
88
|
+
|
|
89
|
+
# Scaffold a new service
|
|
90
|
+
launch init myservice --stack fastapi
|
|
91
|
+
|
|
92
|
+
# Run it
|
|
93
|
+
cd myservice
|
|
94
|
+
launch dev
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
See pyproject extras for the full list.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## What you get out of the box
|
|
102
|
+
|
|
103
|
+
- App orchestrator (`core_bluprint.launcher`)
|
|
104
|
+
- Loads your routes, models, signals, and healthchecks
|
|
105
|
+
- Exposes settings and health endpoints (public toggle)
|
|
106
|
+
- FastAPI-native
|
|
107
|
+
- Dependency-injected request/tenant context and guards
|
|
108
|
+
- Clear routing, OpenAPI, and dependency injection patterns
|
|
109
|
+
- Auth & Access
|
|
110
|
+
- DI-based guards with OIDC token introspection
|
|
111
|
+
- Pluggable PDP for ABAC/RBAC/ReBAC decisions
|
|
112
|
+
- Multi-tenancy
|
|
113
|
+
- Tenant-aware DI context across web, DB, and messaging
|
|
114
|
+
- Automatic per-tenant settings endpoint backed by DB + cache
|
|
115
|
+
- Database layer (MongoDB via Beanie)
|
|
116
|
+
- Created/updated mixins, pagination utilities, typed helpers
|
|
117
|
+
- Helper classes/methods for common patterns (queries, projections, pagination)
|
|
118
|
+
- Auto model discovery for DB init
|
|
119
|
+
- Signals / Messaging (Rabbit via FastStream)
|
|
120
|
+
- Event-driven publish/subscribe integration with DI and retries
|
|
121
|
+
- Subscriber wiring and healthchecks
|
|
122
|
+
- Observability
|
|
123
|
+
- OpenTelemetry distro + OTLP exporter, Logfire, Sentry (error/bug tracking)
|
|
124
|
+
- I18N
|
|
125
|
+
- Exception handler and template utils with Babel/Jinja2
|
|
126
|
+
- Healthchecks
|
|
127
|
+
- Automatic app/DB/messaging checks + system routes
|
|
128
|
+
- Pydantic-native schemas and validators
|
|
129
|
+
- SchemaIn/Out validation for request/response contracts
|
|
130
|
+
- Common types and validators (`core_bluprint.types`)
|
|
131
|
+
|
|
132
|
+
Dive deeper in the docs below.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Documentation
|
|
137
|
+
|
|
138
|
+
- Auth → docs/auth.md
|
|
139
|
+
- Tenant → docs/tenant.md
|
|
140
|
+
- DB (Mongo/Beanie) → docs/db.md
|
|
141
|
+
- Signals (Rabbit) → docs/signals.md
|
|
142
|
+
- Observability → docs/observability.md
|
|
143
|
+
- File storage → docs/file.md
|
|
144
|
+
- I18N → docs/i18n.md
|
|
145
|
+
- Settings & Configs → docs/settings.md
|
|
146
|
+
- Launcher & App model → docs/launcher.md
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Roadmap
|
|
151
|
+
|
|
152
|
+
- More CLI scaffolds and blueprints.
|
|
153
|
+
- Automatic `pydantic ai` agentic tool creation from apis
|
|
154
|
+
- Migrate PDP to [`OPAL`](https://github.com/permitio/opal) [opa](https://github.com/open-policy-agent/opa) based
|
|
155
|
+
|
fastloom-0.4.2/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Fastloom – The Open Foundation for Building Event-Driven Services
|
|
2
|
+
|
|
3
|
+
Fastloom is a lightweight, batteries-included foundation for building modern backends. Define your settings, schemas, and endpoints; Fastloom wires up the rest: FastAPI, Mongo (Beanie), Rabbit (FastStream), metrics/traces/logs/errors, and more.
|
|
4
|
+
|
|
5
|
+
Think of it as the glue for your stack: web, messaging, caching, DB, observability, and integrations with best-in-class tools.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why FastLoom
|
|
10
|
+
|
|
11
|
+
- No boilerplate: minimal scaffolding/templating; most wiring is handled inside Core.
|
|
12
|
+
- Composable: opt into only what you need (`FastAPI`, `Rabbit`, `MongoDB`, `Redis`, `OpenAI`).
|
|
13
|
+
- Pydantic-first: type-safe models, validators, and clear input/output contracts.
|
|
14
|
+
- Multi-tenant by design: tenant context flows through DI and storage.
|
|
15
|
+
- AuthN/Z via DI: OIDC token introspection and pluggable PDP (ABAC/RBAC/ReBAC) hooks.
|
|
16
|
+
- Event-driven ready: publish/subscribe with routing keys and health.
|
|
17
|
+
- Observability-native: metrics, traces, logs from day one.
|
|
18
|
+
- Self-hostable: production parity with a cloud/aaS setup.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Integrated Services (the platform)
|
|
23
|
+
|
|
24
|
+
Core plugs into a family of self-hostable services:
|
|
25
|
+
|
|
26
|
+
- IAM → OIDC/SSO, authN/Z, RBAC/ABAC/ReBAC.
|
|
27
|
+
- Notify → realtime notifications, Pusher-compatible API.
|
|
28
|
+
- Pulse → user activity + event tracking with OpenTelemetry hooks.
|
|
29
|
+
- File → object storage on MinIO (S3-compatible).
|
|
30
|
+
- Finance, Subscription, SMS/Email, Meet, Persona → optional services you can wire in.
|
|
31
|
+
|
|
32
|
+
Each service is:
|
|
33
|
+
- self-hostable (Docker Compose or Helm),
|
|
34
|
+
- BaaS-available,
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Install core
|
|
42
|
+
poetry add core-bluprint -E FastAPI
|
|
43
|
+
|
|
44
|
+
# Scaffold a new service
|
|
45
|
+
launch init myservice --stack fastapi
|
|
46
|
+
|
|
47
|
+
# Run it
|
|
48
|
+
cd myservice
|
|
49
|
+
launch dev
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
See pyproject extras for the full list.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## What you get out of the box
|
|
57
|
+
|
|
58
|
+
- App orchestrator (`core_bluprint.launcher`)
|
|
59
|
+
- Loads your routes, models, signals, and healthchecks
|
|
60
|
+
- Exposes settings and health endpoints (public toggle)
|
|
61
|
+
- FastAPI-native
|
|
62
|
+
- Dependency-injected request/tenant context and guards
|
|
63
|
+
- Clear routing, OpenAPI, and dependency injection patterns
|
|
64
|
+
- Auth & Access
|
|
65
|
+
- DI-based guards with OIDC token introspection
|
|
66
|
+
- Pluggable PDP for ABAC/RBAC/ReBAC decisions
|
|
67
|
+
- Multi-tenancy
|
|
68
|
+
- Tenant-aware DI context across web, DB, and messaging
|
|
69
|
+
- Automatic per-tenant settings endpoint backed by DB + cache
|
|
70
|
+
- Database layer (MongoDB via Beanie)
|
|
71
|
+
- Created/updated mixins, pagination utilities, typed helpers
|
|
72
|
+
- Helper classes/methods for common patterns (queries, projections, pagination)
|
|
73
|
+
- Auto model discovery for DB init
|
|
74
|
+
- Signals / Messaging (Rabbit via FastStream)
|
|
75
|
+
- Event-driven publish/subscribe integration with DI and retries
|
|
76
|
+
- Subscriber wiring and healthchecks
|
|
77
|
+
- Observability
|
|
78
|
+
- OpenTelemetry distro + OTLP exporter, Logfire, Sentry (error/bug tracking)
|
|
79
|
+
- I18N
|
|
80
|
+
- Exception handler and template utils with Babel/Jinja2
|
|
81
|
+
- Healthchecks
|
|
82
|
+
- Automatic app/DB/messaging checks + system routes
|
|
83
|
+
- Pydantic-native schemas and validators
|
|
84
|
+
- SchemaIn/Out validation for request/response contracts
|
|
85
|
+
- Common types and validators (`core_bluprint.types`)
|
|
86
|
+
|
|
87
|
+
Dive deeper in the docs below.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Documentation
|
|
92
|
+
|
|
93
|
+
- Auth → docs/auth.md
|
|
94
|
+
- Tenant → docs/tenant.md
|
|
95
|
+
- DB (Mongo/Beanie) → docs/db.md
|
|
96
|
+
- Signals (Rabbit) → docs/signals.md
|
|
97
|
+
- Observability → docs/observability.md
|
|
98
|
+
- File storage → docs/file.md
|
|
99
|
+
- I18N → docs/i18n.md
|
|
100
|
+
- Settings & Configs → docs/settings.md
|
|
101
|
+
- Launcher & App model → docs/launcher.md
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Roadmap
|
|
106
|
+
|
|
107
|
+
- More CLI scaffolds and blueprints.
|
|
108
|
+
- Automatic `pydantic ai` agentic tool creation from apis
|
|
109
|
+
- Migrate PDP to [`OPAL`](https://github.com/permitio/opal) [opa](https://github.com/open-policy-agent/opa) based
|
|
File without changes
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from collections.abc import Callable, Coroutine
|
|
2
|
+
from typing import Annotated, Any
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
from fastapi import Depends, HTTPException, Request
|
|
6
|
+
from fastapi.security import OAuth2, OpenIdConnect
|
|
7
|
+
from jose.jwt import get_unverified_claims
|
|
8
|
+
|
|
9
|
+
from fastloom.auth import Claims
|
|
10
|
+
from fastloom.auth.schemas import (
|
|
11
|
+
IntrospectionResponse,
|
|
12
|
+
UserClaims,
|
|
13
|
+
)
|
|
14
|
+
from fastloom.settings.base import IAMSettings
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OptionalJWTAuth:
|
|
18
|
+
settings: IAMSettings
|
|
19
|
+
_security_scheme: OAuth2 | OpenIdConnect | None = None
|
|
20
|
+
|
|
21
|
+
def __init__(self, settings: IAMSettings):
|
|
22
|
+
self.settings = settings
|
|
23
|
+
|
|
24
|
+
if self.settings.oidc_enabled:
|
|
25
|
+
assert self.settings.OIDC_URL is not None
|
|
26
|
+
self._security_scheme = OpenIdConnect(
|
|
27
|
+
openIdConnectUrl=self.settings.OIDC_URL,
|
|
28
|
+
scheme_name="OIDC",
|
|
29
|
+
auto_error=False,
|
|
30
|
+
)
|
|
31
|
+
elif self.settings.oauth2_enabled:
|
|
32
|
+
self._security_scheme = OAuth2(
|
|
33
|
+
flows=self.settings.flows, auto_error=False
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
async def _introspect(
|
|
37
|
+
self, token: Annotated[str, Depends(_security_scheme)]
|
|
38
|
+
):
|
|
39
|
+
async with httpx.AsyncClient() as client:
|
|
40
|
+
response: httpx.Response = await client.post(
|
|
41
|
+
f"{self.settings.IAM_SIDECAR_URL}/introspect",
|
|
42
|
+
json=dict(token=token),
|
|
43
|
+
)
|
|
44
|
+
if response.status_code != 200:
|
|
45
|
+
raise HTTPException(status_code=403, detail=response.text)
|
|
46
|
+
data = IntrospectionResponse.model_validate(response.json())
|
|
47
|
+
if not data.active:
|
|
48
|
+
raise HTTPException(status_code=403, detail="Inactive token")
|
|
49
|
+
|
|
50
|
+
def _transform_bearer(self, token: str) -> str:
|
|
51
|
+
if token.startswith("Bearer "):
|
|
52
|
+
return token.removeprefix("Bearer ").strip()
|
|
53
|
+
return token
|
|
54
|
+
|
|
55
|
+
async def _acl(
|
|
56
|
+
self,
|
|
57
|
+
request: Request,
|
|
58
|
+
token: Annotated[str, Depends(_security_scheme)],
|
|
59
|
+
) -> None:
|
|
60
|
+
async with httpx.AsyncClient() as client:
|
|
61
|
+
response: httpx.Response = await client.post(
|
|
62
|
+
url=f"{self.settings.IAM_SIDECAR_URL}/acl",
|
|
63
|
+
json={
|
|
64
|
+
"token": token,
|
|
65
|
+
"endpoint": request.url.path,
|
|
66
|
+
"method": request.method,
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if response.status_code != 200:
|
|
71
|
+
raise HTTPException(status_code=403, detail=response.text)
|
|
72
|
+
if not response.json():
|
|
73
|
+
raise HTTPException(status_code=403)
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def _parse_token(cls, token: str) -> UserClaims:
|
|
77
|
+
return UserClaims.model_validate(get_unverified_claims(token))
|
|
78
|
+
|
|
79
|
+
async def _validate_token(
|
|
80
|
+
self, token: str, request: Request
|
|
81
|
+
) -> UserClaims:
|
|
82
|
+
token = self._transform_bearer(token)
|
|
83
|
+
if self.settings.INTROSPECT:
|
|
84
|
+
await self._introspect(token)
|
|
85
|
+
if self.settings.ACL:
|
|
86
|
+
await self._acl(request, token)
|
|
87
|
+
claims = self._parse_token(token)
|
|
88
|
+
Claims.set(claims)
|
|
89
|
+
return claims
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def get_token(
|
|
93
|
+
self,
|
|
94
|
+
) -> Callable[..., Coroutine[Any, Any, str | None]]:
|
|
95
|
+
async def _inner(
|
|
96
|
+
token: Annotated[str | None, Depends(self._security_scheme)],
|
|
97
|
+
) -> str | None:
|
|
98
|
+
if token is None:
|
|
99
|
+
return None
|
|
100
|
+
return self._transform_bearer(token)
|
|
101
|
+
|
|
102
|
+
return _inner
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def get_claims(
|
|
106
|
+
self,
|
|
107
|
+
) -> Callable[..., Coroutine[Any, Any, UserClaims | None]]:
|
|
108
|
+
async def _inner(
|
|
109
|
+
request: Request,
|
|
110
|
+
token: Annotated[str | None, Depends(self._security_scheme)],
|
|
111
|
+
) -> UserClaims | None:
|
|
112
|
+
if token is None:
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
return await self._validate_token(token, request)
|
|
116
|
+
|
|
117
|
+
return _inner
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class JWTAuth(OptionalJWTAuth):
|
|
121
|
+
def __init__(self, settings: IAMSettings):
|
|
122
|
+
super().__init__(settings)
|
|
123
|
+
assert self._security_scheme is not None
|
|
124
|
+
self._security_scheme.auto_error = True
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def get_claims(self) -> Callable[..., Coroutine[Any, Any, UserClaims]]:
|
|
128
|
+
async def _inner(
|
|
129
|
+
request: Request,
|
|
130
|
+
token: Annotated[str, Depends(self._security_scheme)],
|
|
131
|
+
) -> UserClaims:
|
|
132
|
+
return await self._validate_token(token, request)
|
|
133
|
+
|
|
134
|
+
return _inner
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def get_token(
|
|
138
|
+
self,
|
|
139
|
+
) -> Callable[..., Coroutine[Any, Any, str]]:
|
|
140
|
+
async def _inner(
|
|
141
|
+
token: Annotated[str, Depends(self._security_scheme)],
|
|
142
|
+
) -> str:
|
|
143
|
+
return self._transform_bearer(token)
|
|
144
|
+
|
|
145
|
+
return _inner
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from fastapi.openapi.models import OAuthFlow, OAuthFlows
|
|
2
|
+
from pydantic import BaseModel, Field, HttpUrl, computed_field, field_validator
|
|
3
|
+
|
|
4
|
+
from fastloom.types import Str
|
|
5
|
+
|
|
6
|
+
ADMIN_ROLE = "ADMIN"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OAuth2MergedScheme(OAuthFlow):
|
|
10
|
+
authorizationUrl: Str[HttpUrl] | None = None
|
|
11
|
+
tokenUrl: Str[HttpUrl] | None = None
|
|
12
|
+
|
|
13
|
+
@computed_field # type: ignore[prop-decorator]
|
|
14
|
+
@property
|
|
15
|
+
def flows(self) -> OAuthFlows:
|
|
16
|
+
if self.authorizationUrl is None and self.tokenUrl is None:
|
|
17
|
+
return OAuthFlows()
|
|
18
|
+
return OAuthFlows.model_validate(
|
|
19
|
+
dict(
|
|
20
|
+
authorizationCode=self.model_dump(
|
|
21
|
+
exclude_computed_fields=True
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
# ^ implicit & ROPC are deprecated in OAUTH2.1
|
|
26
|
+
|
|
27
|
+
@computed_field # type: ignore[prop-decorator]
|
|
28
|
+
@property
|
|
29
|
+
def oauth2_enabled(self) -> bool:
|
|
30
|
+
return self.authorizationUrl is not None and self.tokenUrl is not None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class OIDCCScheme(BaseModel):
|
|
34
|
+
OIDC_URL: Str[HttpUrl] | None = None
|
|
35
|
+
|
|
36
|
+
@computed_field # type: ignore[misc]
|
|
37
|
+
@property
|
|
38
|
+
def oidc_enabled(self) -> bool:
|
|
39
|
+
return self.OIDC_URL is not None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class IntrospectionResponse(BaseModel):
|
|
43
|
+
active: bool
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Role(BaseModel):
|
|
47
|
+
name: str
|
|
48
|
+
users: list[str] | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class UserClaims(BaseModel):
|
|
52
|
+
tenant: str = Field(alias="owner")
|
|
53
|
+
id: str
|
|
54
|
+
username: str = Field(..., validation_alias="name")
|
|
55
|
+
email: str | None = None
|
|
56
|
+
phone: str | None = None
|
|
57
|
+
roles: list[Role] = Field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
@field_validator("roles", mode="before")
|
|
60
|
+
@classmethod
|
|
61
|
+
def validate_roles(cls, v: list[Role] | None) -> list[Role]:
|
|
62
|
+
if not v:
|
|
63
|
+
return []
|
|
64
|
+
return v
|
|
65
|
+
|
|
66
|
+
@computed_field # type: ignore[misc]
|
|
67
|
+
@property
|
|
68
|
+
def is_admin(self) -> bool:
|
|
69
|
+
return any(role.name == ADMIN_ROLE for role in self.roles or [])
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
from aredis_om import Field, JsonModel
|
|
5
|
+
else:
|
|
6
|
+
try:
|
|
7
|
+
from aredis_om import Field, JsonModel
|
|
8
|
+
except ImportError:
|
|
9
|
+
from pydantic import BaseModel as JsonModel
|
|
10
|
+
from pydantic import Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseCache(JsonModel):
|
|
14
|
+
class Meta:
|
|
15
|
+
global_key_prefix = "cache"
|
|
16
|
+
model_key_prefix = "base"
|
|
17
|
+
# ^should be overriden in sub
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
async def invalidate(self):
|
|
21
|
+
return await self.expire(0)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BaseTenantSettingCache(BaseCache):
|
|
25
|
+
id: str = Field(primary_key=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class HostTenantMapping(BaseCache, index=True): # type: ignore[call-arg]
|
|
29
|
+
host: str = Field(primary_key=True)
|
|
30
|
+
tenant: str = Field(index=True)
|
|
31
|
+
|
|
32
|
+
class Meta:
|
|
33
|
+
model_key_prefix = "host_mapping"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from collections.abc import Awaitable
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from os import getpid
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from fastloom.cache.lifehooks import RedisHandler
|
|
7
|
+
from fastloom.settings.base import ProjectSettings
|
|
8
|
+
from fastloom.tenant.settings import ConfigAlias as Configs
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RedisGuardGate:
|
|
12
|
+
"""
|
|
13
|
+
- *context manager*:
|
|
14
|
+
```
|
|
15
|
+
async with RedisGuardGate("boostrap", ttl=30, grace=10) as acquired:
|
|
16
|
+
if acquired:
|
|
17
|
+
await lifespan_init()
|
|
18
|
+
```
|
|
19
|
+
- *decorator*:
|
|
20
|
+
```
|
|
21
|
+
@RedisGuardGate("boostrap", ttl=30)
|
|
22
|
+
async def lifespan_init():
|
|
23
|
+
...
|
|
24
|
+
```
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
key: str
|
|
28
|
+
ttl: int
|
|
29
|
+
grace: int
|
|
30
|
+
_acquired: bool = False
|
|
31
|
+
|
|
32
|
+
def __init__(self, key: str, ttl: int = 60, grace: int = 0):
|
|
33
|
+
self.key = (
|
|
34
|
+
f"{Configs[ProjectSettings].general.PROJECT_NAME}:{key}:leader" # type: ignore[misc]
|
|
35
|
+
)
|
|
36
|
+
self.ttl = ttl
|
|
37
|
+
self.grace = grace
|
|
38
|
+
|
|
39
|
+
def __call__[T, **P](self, func: Callable[P, Awaitable[T]]):
|
|
40
|
+
@wraps(func)
|
|
41
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T | None:
|
|
42
|
+
async with self as acquired:
|
|
43
|
+
if acquired:
|
|
44
|
+
return await func(*args, **kwargs)
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
return wrapper
|
|
48
|
+
|
|
49
|
+
async def _acquire(self):
|
|
50
|
+
acquired = await RedisHandler.redis.set(
|
|
51
|
+
self.key,
|
|
52
|
+
str(getpid()),
|
|
53
|
+
nx=True,
|
|
54
|
+
ex=self.ttl,
|
|
55
|
+
)
|
|
56
|
+
return acquired is not None
|
|
57
|
+
|
|
58
|
+
async def _release(self):
|
|
59
|
+
await RedisHandler.redis.expire(self.key, self.grace)
|
|
60
|
+
|
|
61
|
+
async def __aenter__(self) -> bool:
|
|
62
|
+
self._acquired = await self._acquire()
|
|
63
|
+
return self._acquired
|
|
64
|
+
|
|
65
|
+
async def __aexit__(self, exc_type, exc, tb):
|
|
66
|
+
if self._acquired:
|
|
67
|
+
await self._release()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from collections.abc import Callable, Coroutine
|
|
2
|
+
from functools import partial
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RedisConnectionError(Exception): ...
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def check_redis_connection(redis_url: str) -> None:
|
|
10
|
+
from redis.asyncio import Redis
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
client: Redis = Redis.from_url(redis_url)
|
|
14
|
+
await client.ping()
|
|
15
|
+
except Exception as er:
|
|
16
|
+
raise RedisConnectionError(f"Redis connection error: {er}") from er
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_healthcheck(
|
|
20
|
+
redis_url: str,
|
|
21
|
+
) -> Callable[[], Coroutine[Any, Any, None]]:
|
|
22
|
+
return partial(check_redis_connection, redis_url=redis_url)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from fastloom.cache.settings import RedisSettings
|
|
5
|
+
from fastloom.meta import SelfSustaining
|
|
6
|
+
|
|
7
|
+
_HAS_REDIS = False
|
|
8
|
+
with suppress(ImportError):
|
|
9
|
+
from aredis_om import get_redis_connection
|
|
10
|
+
from redis import Redis as SyncRedis
|
|
11
|
+
from redis.asyncio import Redis
|
|
12
|
+
from redis.exceptions import ConnectionError
|
|
13
|
+
|
|
14
|
+
_HAS_REDIS = True
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from redis import Redis as SyncRedis
|
|
18
|
+
from redis.asyncio import Redis
|
|
19
|
+
|
|
20
|
+
if not TYPE_CHECKING and not _HAS_REDIS:
|
|
21
|
+
SyncRedis = None
|
|
22
|
+
Redis = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RedisHandler(SelfSustaining):
|
|
26
|
+
enabled: bool = False
|
|
27
|
+
redis: Redis
|
|
28
|
+
sync_redis: SyncRedis
|
|
29
|
+
|
|
30
|
+
def __init__(self, settings: RedisSettings) -> None:
|
|
31
|
+
super().__init__()
|
|
32
|
+
if not _HAS_REDIS:
|
|
33
|
+
return
|
|
34
|
+
self.redis = get_redis_connection(url=str(settings.REDIS_URL))
|
|
35
|
+
self.sync_redis = SyncRedis.from_url(url=str(settings.REDIS_URL))
|
|
36
|
+
with suppress(ConnectionError):
|
|
37
|
+
self.enabled = self.sync_redis.ping()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def generate_token(length: int):
|
|
5
|
+
"""
|
|
6
|
+
Generates a n-digits numeral token, that used for OTP
|
|
7
|
+
"""
|
|
8
|
+
min_value = 10 ** (length - 1)
|
|
9
|
+
max_value = 10**length - 1
|
|
10
|
+
return str(secrets.randbelow(max_value - min_value + 1) + min_value).zfill(
|
|
11
|
+
length
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def generate_alphanumeric_token(length: int):
|
|
16
|
+
"""
|
|
17
|
+
Generates a n-digits alphanumeric token, that used for OTP
|
|
18
|
+
"""
|
|
19
|
+
alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
20
|
+
return "".join(secrets.choice(alphabet) for _ in range(length))
|