svc-infra 0.1.600__py3-none-any.whl → 0.1.664__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.
Potentially problematic release.
This version of svc-infra might be problematic. Click here for more details.
- svc_infra/api/fastapi/admin/__init__.py +3 -0
- svc_infra/api/fastapi/admin/add.py +231 -0
- svc_infra/api/fastapi/apf_payments/setup.py +0 -2
- svc_infra/api/fastapi/auth/add.py +0 -4
- svc_infra/api/fastapi/auth/routers/oauth_router.py +19 -4
- svc_infra/api/fastapi/billing/router.py +64 -0
- svc_infra/api/fastapi/billing/setup.py +19 -0
- svc_infra/api/fastapi/cache/add.py +9 -5
- svc_infra/api/fastapi/db/nosql/mongo/add.py +33 -27
- svc_infra/api/fastapi/db/sql/add.py +40 -18
- svc_infra/api/fastapi/db/sql/crud_router.py +176 -14
- svc_infra/api/fastapi/db/sql/session.py +16 -0
- svc_infra/api/fastapi/dependencies/ratelimit.py +57 -7
- svc_infra/api/fastapi/docs/add.py +160 -0
- svc_infra/api/fastapi/docs/landing.py +1 -1
- svc_infra/api/fastapi/docs/scoped.py +41 -6
- svc_infra/api/fastapi/middleware/errors/handlers.py +45 -7
- svc_infra/api/fastapi/middleware/graceful_shutdown.py +87 -0
- svc_infra/api/fastapi/middleware/ratelimit.py +59 -1
- svc_infra/api/fastapi/middleware/ratelimit_store.py +12 -6
- svc_infra/api/fastapi/middleware/timeout.py +148 -0
- svc_infra/api/fastapi/openapi/mutators.py +114 -0
- svc_infra/api/fastapi/ops/add.py +73 -0
- svc_infra/api/fastapi/pagination.py +3 -1
- svc_infra/api/fastapi/routers/ping.py +1 -0
- svc_infra/api/fastapi/setup.py +21 -13
- svc_infra/api/fastapi/tenancy/add.py +19 -0
- svc_infra/api/fastapi/tenancy/context.py +112 -0
- svc_infra/api/fastapi/versioned.py +101 -0
- svc_infra/app/README.md +5 -5
- svc_infra/billing/__init__.py +23 -0
- svc_infra/billing/async_service.py +147 -0
- svc_infra/billing/jobs.py +230 -0
- svc_infra/billing/models.py +131 -0
- svc_infra/billing/quotas.py +101 -0
- svc_infra/billing/schemas.py +33 -0
- svc_infra/billing/service.py +115 -0
- svc_infra/bundled_docs/README.md +5 -0
- svc_infra/bundled_docs/__init__.py +1 -0
- svc_infra/bundled_docs/getting-started.md +6 -0
- svc_infra/cache/__init__.py +4 -0
- svc_infra/cache/add.py +158 -0
- svc_infra/cache/backend.py +5 -2
- svc_infra/cache/decorators.py +19 -1
- svc_infra/cache/keys.py +24 -4
- svc_infra/cli/__init__.py +28 -8
- svc_infra/cli/cmds/__init__.py +8 -0
- svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +4 -3
- svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +4 -4
- svc_infra/cli/cmds/db/sql/alembic_cmds.py +80 -11
- svc_infra/cli/cmds/db/sql/sql_export_cmds.py +80 -0
- svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +3 -3
- svc_infra/cli/cmds/docs/docs_cmds.py +140 -0
- svc_infra/cli/cmds/dx/__init__.py +12 -0
- svc_infra/cli/cmds/dx/dx_cmds.py +99 -0
- svc_infra/cli/cmds/help.py +4 -0
- svc_infra/cli/cmds/obs/obs_cmds.py +4 -3
- svc_infra/cli/cmds/sdk/__init__.py +0 -0
- svc_infra/cli/cmds/sdk/sdk_cmds.py +102 -0
- svc_infra/data/add.py +61 -0
- svc_infra/data/backup.py +53 -0
- svc_infra/data/erasure.py +45 -0
- svc_infra/data/fixtures.py +40 -0
- svc_infra/data/retention.py +55 -0
- svc_infra/db/nosql/mongo/README.md +13 -13
- svc_infra/db/sql/repository.py +51 -11
- svc_infra/db/sql/resource.py +5 -0
- svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +7 -56
- svc_infra/db/sql/templates/setup/env_async.py.tmpl +34 -12
- svc_infra/db/sql/templates/setup/env_sync.py.tmpl +29 -7
- svc_infra/db/sql/tenant.py +79 -0
- svc_infra/db/sql/utils.py +18 -4
- svc_infra/docs/acceptance-matrix.md +88 -0
- svc_infra/docs/acceptance.md +44 -0
- svc_infra/docs/admin.md +425 -0
- svc_infra/docs/adr/0002-background-jobs-and-scheduling.md +40 -0
- svc_infra/docs/adr/0003-webhooks-framework.md +24 -0
- svc_infra/docs/adr/0004-tenancy-model.md +42 -0
- svc_infra/docs/adr/0005-data-lifecycle.md +86 -0
- svc_infra/docs/adr/0006-ops-slos-and-metrics.md +47 -0
- svc_infra/docs/adr/0007-docs-and-sdks.md +83 -0
- svc_infra/docs/adr/0008-billing-primitives.md +143 -0
- svc_infra/docs/adr/0009-acceptance-harness.md +40 -0
- svc_infra/docs/adr/0010-timeouts-and-resource-limits.md +54 -0
- svc_infra/docs/adr/0011-admin-scope-and-impersonation.md +73 -0
- svc_infra/docs/adr/0012-generic-file-storage.md +498 -0
- svc_infra/docs/api.md +186 -0
- svc_infra/docs/auth.md +11 -0
- svc_infra/docs/billing.md +190 -0
- svc_infra/docs/cache.md +76 -0
- svc_infra/docs/cli.md +74 -0
- svc_infra/docs/contributing.md +34 -0
- svc_infra/docs/data-lifecycle.md +52 -0
- svc_infra/docs/database.md +14 -0
- svc_infra/docs/docs-and-sdks.md +62 -0
- svc_infra/docs/environment.md +114 -0
- svc_infra/docs/getting-started.md +63 -0
- svc_infra/docs/idempotency.md +111 -0
- svc_infra/docs/jobs.md +67 -0
- svc_infra/docs/observability.md +16 -0
- svc_infra/docs/ops.md +37 -0
- svc_infra/docs/rate-limiting.md +125 -0
- svc_infra/docs/repo-review.md +48 -0
- svc_infra/docs/security.md +176 -0
- svc_infra/docs/storage.md +982 -0
- svc_infra/docs/tenancy.md +35 -0
- svc_infra/docs/timeouts-and-resource-limits.md +147 -0
- svc_infra/docs/versioned-integrations.md +146 -0
- svc_infra/docs/webhooks.md +112 -0
- svc_infra/dx/add.py +63 -0
- svc_infra/dx/changelog.py +74 -0
- svc_infra/dx/checks.py +67 -0
- svc_infra/http/__init__.py +13 -0
- svc_infra/http/client.py +72 -0
- svc_infra/jobs/builtins/webhook_delivery.py +14 -2
- svc_infra/jobs/queue.py +9 -1
- svc_infra/jobs/runner.py +75 -0
- svc_infra/jobs/worker.py +17 -1
- svc_infra/mcp/svc_infra_mcp.py +85 -28
- svc_infra/obs/add.py +54 -7
- svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
- svc_infra/security/headers.py +15 -2
- svc_infra/security/hibp.py +6 -2
- svc_infra/security/models.py +27 -7
- svc_infra/security/oauth_models.py +59 -0
- svc_infra/security/permissions.py +1 -0
- svc_infra/storage/__init__.py +93 -0
- svc_infra/storage/add.py +250 -0
- svc_infra/storage/backends/__init__.py +11 -0
- svc_infra/storage/backends/local.py +331 -0
- svc_infra/storage/backends/memory.py +214 -0
- svc_infra/storage/backends/s3.py +329 -0
- svc_infra/storage/base.py +239 -0
- svc_infra/storage/easy.py +182 -0
- svc_infra/storage/settings.py +192 -0
- svc_infra/webhooks/service.py +10 -2
- {svc_infra-0.1.600.dist-info → svc_infra-0.1.664.dist-info}/METADATA +45 -14
- {svc_infra-0.1.600.dist-info → svc_infra-0.1.664.dist-info}/RECORD +140 -52
- {svc_infra-0.1.600.dist-info → svc_infra-0.1.664.dist-info}/WHEEL +0 -0
- {svc_infra-0.1.600.dist-info → svc_infra-0.1.664.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# svc-infra
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/svc-infra/)
|
|
4
|
+
[](.)
|
|
5
|
+
|
|
6
|
+
svc-infra packages the shared building blocks we use to ship production FastAPI services fast—HTTP APIs with secure auth, durable persistence, background execution, cache, observability, and webhook plumbing that all share the same batteries-included defaults.
|
|
7
|
+
|
|
8
|
+
## Helper index
|
|
9
|
+
|
|
10
|
+
| Area | What it covers | Guide |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| Getting Started | Overview and entry points | [This page](getting-started.md) |
|
|
13
|
+
| Environment | Feature switches and env vars | [Environment](environment.md) |
|
|
14
|
+
| API | FastAPI bootstrap, middleware, docs wiring | [API guide](api.md) |
|
|
15
|
+
| Auth | Sessions, OAuth/OIDC, MFA, SMTP delivery | [Auth](auth.md) |
|
|
16
|
+
| Security | Password policy, lockout, signed cookies, headers | [Security](security.md) |
|
|
17
|
+
| Database | SQL + Mongo wiring, Alembic helpers, inbox/outbox patterns | [Database](database.md) |
|
|
18
|
+
| Tenancy | Multi-tenant boundaries and helpers | [Tenancy](tenancy.md) |
|
|
19
|
+
| Idempotency | Idempotent endpoints and middleware | [Idempotency](idempotency.md) |
|
|
20
|
+
| Rate Limiting | Middleware, dependency limiter, headers | [Rate limiting](rate-limiting.md) |
|
|
21
|
+
| Cache | cashews decorators, namespace management, TTL helpers | [Cache](cache.md) |
|
|
22
|
+
| Jobs | JobQueue, scheduler, CLI worker | [Jobs](jobs.md) |
|
|
23
|
+
| Observability | Prometheus, Grafana, OpenTelemetry | [Observability](observability.md) |
|
|
24
|
+
| Ops | Probes, breakers, SLOs & dashboards | [Ops](ops.md) |
|
|
25
|
+
| Webhooks | Subscription store, signing, retry worker | [Webhooks](webhooks.md) |
|
|
26
|
+
| CLI | Command groups for sql/mongo/obs/docs/dx/sdk/jobs | [CLI](cli.md) |
|
|
27
|
+
| Docs & SDKs | Publishing docs, generating SDKs | [Docs & SDKs](docs-and-sdks.md) |
|
|
28
|
+
| Acceptance | Acceptance harness and flows | [Acceptance](acceptance.md), [Matrix](acceptance-matrix.md) |
|
|
29
|
+
| Contributing | Dev setup and quality gates | [Contributing](contributing.md) |
|
|
30
|
+
| Repo Review | Checklist for releasing/PRs | [Repo review](repo-review.md) |
|
|
31
|
+
| Data Lifecycle | Fixtures, retention, erasure, backups | [Data lifecycle](data-lifecycle.md) |
|
|
32
|
+
|
|
33
|
+
## Minimal FastAPI bootstrap
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from fastapi import Depends
|
|
37
|
+
from svc_infra.api.fastapi.ease import easy_service_app
|
|
38
|
+
from svc_infra.api.fastapi.db.sql.add import add_sql_db
|
|
39
|
+
from svc_infra.cache import init_cache
|
|
40
|
+
from svc_infra.jobs.easy import easy_jobs
|
|
41
|
+
from svc_infra.webhooks.fastapi import require_signature
|
|
42
|
+
|
|
43
|
+
app = easy_service_app(name="Billing", release="1.2.3")
|
|
44
|
+
add_sql_db(app) # reads SQL_URL / DB_* envs
|
|
45
|
+
init_cache() # honors CACHE_PREFIX / CACHE_VERSION
|
|
46
|
+
queue, scheduler = easy_jobs() # switches via JOBS_DRIVER / REDIS_URL
|
|
47
|
+
|
|
48
|
+
@app.post("/webhooks/billing")
|
|
49
|
+
async def handle_webhook(payload = Depends(require_signature(lambda: ["current", "next"]))):
|
|
50
|
+
queue.enqueue("process-billing-webhook", payload)
|
|
51
|
+
return {"status": "queued"}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Environment switches
|
|
55
|
+
|
|
56
|
+
- **API** – toggle logging/observability and docs exposure with `ENABLE_LOGGING`, `LOG_LEVEL`, `LOG_FORMAT`, `ENABLE_OBS`, `METRICS_PATH`, `OBS_SKIP_PATHS`, and `CORS_ALLOW_ORIGINS`. 【F:src/svc_infra/api/fastapi/ease.py†L67-L111】【F:src/svc_infra/api/fastapi/setup.py†L47-L88】
|
|
57
|
+
- **Auth** – configure JWT secrets, SMTP, cookies, and policy using the `AUTH_…` settings family (e.g., `AUTH_JWT__SECRET`, `AUTH_SMTP_HOST`, `AUTH_SESSION_COOKIE_SECURE`). 【F:src/svc_infra/api/fastapi/auth/settings.py†L23-L91】
|
|
58
|
+
- **Database** – set connection URLs or components via `SQL_URL`/`SQL_URL_FILE`, `DB_DIALECT`, `DB_HOST`, `DB_USER`, `DB_PASSWORD`, plus Mongo knobs like `MONGO_URL`, `MONGO_DB`, and `MONGO_URL_FILE`. 【F:src/svc_infra/api/fastapi/db/sql/add.py†L55-L114】【F:src/svc_infra/db/sql/utils.py†L85-L206】【F:src/svc_infra/db/nosql/mongo/settings.py†L9-L13】【F:src/svc_infra/db/nosql/utils.py†L56-L113】
|
|
59
|
+
- **Jobs** – choose the queue backend with `JOBS_DRIVER` and provide Redis via `REDIS_URL`; interval schedules can be declared with `JOBS_SCHEDULE_JSON`. 【F:src/svc_infra/jobs/easy.py†L11-L27】【F:src/svc_infra/docs/jobs.md†L11-L48】
|
|
60
|
+
- **Cache** – namespace keys and lifetimes through `CACHE_PREFIX`, `CACHE_VERSION`, and TTL overrides `CACHE_TTL_DEFAULT`, `CACHE_TTL_SHORT`, `CACHE_TTL_LONG`. 【F:src/svc_infra/cache/README.md†L20-L173】【F:src/svc_infra/cache/ttl.py†L26-L55】
|
|
61
|
+
- **Observability** – turn metrics on/off or adjust scrape paths with `ENABLE_OBS`, `METRICS_PATH`, `OBS_SKIP_PATHS`, and Prometheus/Grafana flags like `SVC_INFRA_DISABLE_PROMETHEUS`, `SVC_INFRA_RATE_WINDOW`, `SVC_INFRA_DASHBOARD_REFRESH`, `SVC_INFRA_DASHBOARD_RANGE`. 【F:src/svc_infra/api/fastapi/ease.py†L67-L111】【F:src/svc_infra/obs/metrics/asgi.py†L49-L206】【F:src/svc_infra/obs/cloud_dash.py†L85-L108】
|
|
62
|
+
- **Webhooks** – reuse the jobs envs (`JOBS_DRIVER`, `REDIS_URL`) for the delivery worker and queue configuration. 【F:src/svc_infra/docs/webhooks.md†L32-L53】
|
|
63
|
+
- **Security** – enforce password policy, MFA, and rotation with auth prefixes such as `AUTH_PASSWORD_MIN_LENGTH`, `AUTH_PASSWORD_REQUIRE_SYMBOL`, `AUTH_JWT__SECRET`, and `AUTH_JWT__OLD_SECRETS`. 【F:src/svc_infra/docs/security.md†L24-L70】
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Idempotency & Concurrency Controls
|
|
2
|
+
|
|
3
|
+
This guide explains how idempotency works in svc-infra and how to enable it for safe retries.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
- Prevents duplicate processing of mutating requests (POST/PATCH/DELETE).
|
|
7
|
+
- Replays the previously successful response when the same `Idempotency-Key` is used.
|
|
8
|
+
- Detects conflicts when the same key is reused with a different request body, returning 409.
|
|
9
|
+
|
|
10
|
+
## Middleware usage
|
|
11
|
+
```python
|
|
12
|
+
from svc_infra.api.fastapi.middleware.idempotency import IdempotencyMiddleware
|
|
13
|
+
|
|
14
|
+
app.add_middleware(IdempotencyMiddleware) # default: in-memory store, 24h TTL
|
|
15
|
+
```
|
|
16
|
+
- Header name: `Idempotency-Key` (configurable via `header_name`)
|
|
17
|
+
- TTL: defaults to 24 hours (`ttl_seconds`)
|
|
18
|
+
|
|
19
|
+
### Redis store (recommended for multi-instance)
|
|
20
|
+
```python
|
|
21
|
+
import redis
|
|
22
|
+
from svc_infra.api.fastapi.middleware.idempotency import IdempotencyMiddleware
|
|
23
|
+
from svc_infra.api.fastapi.middleware.idempotency_store import RedisIdempotencyStore
|
|
24
|
+
|
|
25
|
+
r = redis.Redis.from_url("redis://localhost:6379/0")
|
|
26
|
+
store = RedisIdempotencyStore(r, prefix="idmp")
|
|
27
|
+
app.add_middleware(IdempotencyMiddleware, store=store, ttl_seconds=24*3600)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Per-route enforcement
|
|
31
|
+
If an endpoint must require idempotency always, add the dependency:
|
|
32
|
+
```python
|
|
33
|
+
from fastapi import Depends
|
|
34
|
+
from svc_infra.api.fastapi.middleware.idempotency import require_idempotency_key
|
|
35
|
+
|
|
36
|
+
@app.post("/payments/intents", dependencies=[Depends(require_idempotency_key)])
|
|
37
|
+
async def create_intent(...):
|
|
38
|
+
...
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Semantics
|
|
42
|
+
- First request with a key:
|
|
43
|
+
- The middleware claims the key and records a hash of the request body.
|
|
44
|
+
- On success (2xx), the response envelope is cached until TTL.
|
|
45
|
+
- Replay with same key and same body:
|
|
46
|
+
- Returns the cached response with the original status and headers.
|
|
47
|
+
- Replay with same key but different body:
|
|
48
|
+
- Returns 409 Conflict (don’t reuse keys for different logical operations).
|
|
49
|
+
|
|
50
|
+
## Testing
|
|
51
|
+
- Marker: `-m concurrency` selects concurrency tests in this repo.
|
|
52
|
+
- Scenarios covered:
|
|
53
|
+
- Successful first request and replay
|
|
54
|
+
- Conflict on mismatched payload reusing the same key
|
|
55
|
+
|
|
56
|
+
## Notes and pitfalls
|
|
57
|
+
- Use a unique key per logical operation (e.g., `order-{id}-capture-1`).
|
|
58
|
+
- TTL should exceed your max retry horizon.
|
|
59
|
+
- For stronger guarantees in Redis, consider a Lua script to make the claim + response update atomic (future improvement).
|
|
60
|
+
- If you also use optimistic locking, surface 409 when `If-Match` version mismatches during updates.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Optimistic Locking
|
|
65
|
+
|
|
66
|
+
Use the `If-Match` header and a version field on your models.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from svc_infra.api.fastapi.middleware.optimistic_lock import require_if_match, check_version_or_409
|
|
70
|
+
|
|
71
|
+
@app.patch("/resource/{rid}")
|
|
72
|
+
async def update_resource(rid: str, v: str = Depends(require_if_match)):
|
|
73
|
+
current = await repo.get(...)
|
|
74
|
+
check_version_or_409(lambda: current.version, v)
|
|
75
|
+
current.version += 1
|
|
76
|
+
await repo.save(current)
|
|
77
|
+
return {...}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Pitfalls:
|
|
81
|
+
- Always bump the version on successful updates.
|
|
82
|
+
- Return 428 when `If-Match` is missing on mutating routes that require optimistic locking.
|
|
83
|
+
- Consider ETag headers for GETs to complement conditional requests.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Outbox / Inbox
|
|
88
|
+
|
|
89
|
+
Outbox: record events/changes that must be delivered to external systems; a relay fetches and delivers reliably.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from svc_infra.db.outbox import InMemoryOutboxStore
|
|
93
|
+
|
|
94
|
+
ob = InMemoryOutboxStore()
|
|
95
|
+
msg = ob.enqueue("orders.created", {"order_id": 123})
|
|
96
|
+
nxt = ob.fetch_next(topics=["orders.created"]) # process
|
|
97
|
+
ob.mark_processed(nxt.id)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Inbox: deduplicate external deliveries (e.g., webhook replays) with TTL.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from svc_infra.db.inbox import InMemoryInboxStore
|
|
104
|
+
|
|
105
|
+
ib = InMemoryInboxStore()
|
|
106
|
+
if not ib.mark_if_new("provider-evt-abc", ttl_seconds=86400):
|
|
107
|
+
return 200 # duplicate
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Notes:
|
|
111
|
+
- In-memory stores are for tests/local dev; implement SQL/Redis for production with row locks and `SKIP LOCKED` (or Lua) as needed.
|
svc_infra/docs/jobs.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Background Jobs & Scheduling
|
|
2
|
+
|
|
3
|
+
This module provides a lightweight JobQueue abstraction with a Redis backend for production and in-memory implementations for local/tests. A simple interval scheduler and CLI runner are included.
|
|
4
|
+
|
|
5
|
+
> ℹ️ Job-related environment variables are documented in [Environment Reference](environment.md).
|
|
6
|
+
|
|
7
|
+
## Quickstart
|
|
8
|
+
|
|
9
|
+
- Initialize in app code:
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from svc_infra.jobs.easy import easy_jobs
|
|
13
|
+
queue, scheduler = easy_jobs() # uses JOBS_DRIVER=memory|redis
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- Enqueue a job:
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
job = queue.enqueue("send_email", {"to": "user@example.com"})
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- Process one job (async):
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from svc_infra.jobs.worker import process_one
|
|
26
|
+
await process_one(queue, handler)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- Run the CLI runner:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
svc-infra jobs run
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Redis Backend
|
|
36
|
+
|
|
37
|
+
Set environment variables:
|
|
38
|
+
- JOBS_DRIVER=redis
|
|
39
|
+
- REDIS_URL=redis://localhost:6379/0
|
|
40
|
+
|
|
41
|
+
Features:
|
|
42
|
+
- Visibility timeout during processing
|
|
43
|
+
- Exponential backoff on failures (base backoff_seconds * attempts)
|
|
44
|
+
- DLQ after max_attempts
|
|
45
|
+
|
|
46
|
+
## Scheduler
|
|
47
|
+
|
|
48
|
+
An interval-based scheduler runs async callables. You can define tasks via environment JSON:
|
|
49
|
+
|
|
50
|
+
- JOBS_SCHEDULE_JSON:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
[
|
|
54
|
+
{"name": "ping", "interval_seconds": 60, "target": "myapp.tasks:ping"}
|
|
55
|
+
]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The `target` must be an import path of the form `module:function`. Sync functions are wrapped.
|
|
59
|
+
|
|
60
|
+
## Testing
|
|
61
|
+
|
|
62
|
+
- In-memory queue and scheduler enable fast, deterministic tests.
|
|
63
|
+
- Redis tests use `fakeredis` and cover enqueue/reserve/ack/fail and DLQ path.
|
|
64
|
+
|
|
65
|
+
## Next
|
|
66
|
+
|
|
67
|
+
- Add metrics, Redis Lua for atomic multi-ops, SQL-backed queue, distributed scheduler.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Observability guide
|
|
2
|
+
|
|
3
|
+
`svc_infra.obs` adds Prometheus metrics, OpenTelemetry instrumentation, and a CLI to spin up Grafana dashboards.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from svc_infra.api.fastapi.ease import easy_service_app
|
|
7
|
+
|
|
8
|
+
app = easy_service_app(name="Billing", release="1.2.3") # ENABLE_OBS toggles middleware
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Environment
|
|
12
|
+
|
|
13
|
+
- `ENABLE_OBS`, `METRICS_PATH`, `OBS_SKIP_PATHS` – FastAPI bootstrap toggles for metrics exposure. 【F:src/svc_infra/api/fastapi/ease.py†L67-L111】
|
|
14
|
+
- `SVC_INFRA_DISABLE_PROMETHEUS` – disable the ASGI middleware entirely (used by tests/sidecars). 【F:src/svc_infra/obs/metrics/asgi.py†L49-L206】
|
|
15
|
+
- `SVC_INFRA_RATE_WINDOW`, `SVC_INFRA_DASHBOARD_REFRESH`, `SVC_INFRA_DASHBOARD_RANGE` – tune the Grafana dashboards generated by the CLI. 【F:src/svc_infra/obs/cloud_dash.py†L85-L108】
|
|
16
|
+
- Grafana Cloud support looks for `GRAFANA_CLOUD_URL`, `GRAFANA_CLOUD_TOKEN`, `GRAFANA_CLOUD_PROM_URL`, `GRAFANA_CLOUD_PROM_USERNAME`, `GRAFANA_CLOUD_RW_TOKEN`. 【F:src/svc_infra/obs/README.md†L37-L119】
|
svc_infra/docs/ops.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# SLOs & Ops
|
|
2
|
+
|
|
3
|
+
This guide explains how to use svc-infra’s probes, circuit breaker, and metrics for SLOs.
|
|
4
|
+
|
|
5
|
+
## Probes
|
|
6
|
+
|
|
7
|
+
- `add_probes(app, prefix="/_ops")` exposes:
|
|
8
|
+
- `GET /_ops/live` — liveness
|
|
9
|
+
- `GET /_ops/ready` — readiness
|
|
10
|
+
- `GET /_ops/startup` — startup
|
|
11
|
+
|
|
12
|
+
## Circuit breaker (placeholder)
|
|
13
|
+
|
|
14
|
+
- `circuit_breaker_dependency()` returns a dependency that returns 503 when `CIRCUIT_OPEN` is truthy.
|
|
15
|
+
- Use it per-route: `@app.get("/x", dependencies=[Depends(circuit_breaker_dependency())])`.
|
|
16
|
+
|
|
17
|
+
## Metrics and route classification
|
|
18
|
+
|
|
19
|
+
- `add_observability(app, ...)` enables Prometheus metrics and optional DB pool metrics.
|
|
20
|
+
- You can pass an optional `route_classifier(base_path, method) -> class` callable. When provided, the metrics middleware encodes the resolved route label as `"{base}|{class}"`. Dashboards can split this label to filter public/internal/admin routes.
|
|
21
|
+
|
|
22
|
+
## Dashboards
|
|
23
|
+
|
|
24
|
+
- A minimal Grafana dashboard JSON is provided at `src/svc_infra/obs/grafana/dashboards/http-overview.json` (import into Grafana).
|
|
25
|
+
- It shows:
|
|
26
|
+
- Success rate over 5 minutes
|
|
27
|
+
- p99 latency
|
|
28
|
+
- Top routes by 5xx rate
|
|
29
|
+
|
|
30
|
+
## Defaults & environment
|
|
31
|
+
|
|
32
|
+
- Prometheus middleware is enabled unless `SVC_INFRA_DISABLE_PROMETHEUS=1`.
|
|
33
|
+
- Observability settings: `METRICS_ENABLED`, `METRICS_PATH`, and optional histogram buckets.
|
|
34
|
+
|
|
35
|
+
## See also
|
|
36
|
+
|
|
37
|
+
- Timeouts & Resource Limits: `./timeouts-and-resource-limits.md` — request/body/handler timeouts, outbound client timeouts, DB statement timeouts, jobs/webhooks, and graceful shutdown.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Rate Limiting & Abuse Protection
|
|
2
|
+
|
|
3
|
+
This guide shows how to enable and tune the built-in rate limiter and request-size guard, and how to hook simple metrics for abuse detection.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Global middleware-based rate limiting with standard headers
|
|
8
|
+
- Per-route dependency for fine-grained limits
|
|
9
|
+
- 429 responses include `Retry-After`
|
|
10
|
+
- Pluggable store interface (in-memory provided; Redis store available)
|
|
11
|
+
- Request size limit middleware returning 413
|
|
12
|
+
- Metrics hooks for rate-limiting events and suspect payloads
|
|
13
|
+
|
|
14
|
+
## Global middleware
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from svc_infra.api.fastapi.middleware.ratelimit import SimpleRateLimitMiddleware
|
|
18
|
+
|
|
19
|
+
app.add_middleware(
|
|
20
|
+
SimpleRateLimitMiddleware,
|
|
21
|
+
limit=120, # requests
|
|
22
|
+
window=60, # seconds
|
|
23
|
+
key_fn=lambda r: r.headers.get("X-API-Key") or r.client.host,
|
|
24
|
+
)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Responses include headers:
|
|
28
|
+
- `X-RateLimit-Limit`
|
|
29
|
+
- `X-RateLimit-Remaining`
|
|
30
|
+
- `X-RateLimit-Reset` (epoch seconds)
|
|
31
|
+
|
|
32
|
+
When exceeded, responses are 429 with `Retry-After` and the same headers.
|
|
33
|
+
|
|
34
|
+
Advanced options:
|
|
35
|
+
- `scope_by_tenant=True` to automatically include tenant id in the key if tenancy is configured.
|
|
36
|
+
- `limit_resolver=request -> int | None` to override the limit dynamically per request (e.g., per-plan quotas).
|
|
37
|
+
|
|
38
|
+
## Per-route dependency
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from fastapi import Depends
|
|
42
|
+
from svc_infra.api.fastapi.dependencies.ratelimit import rate_limiter
|
|
43
|
+
|
|
44
|
+
limiter = rate_limiter(limit=10, window=60, key_fn=lambda r: r.client.host)
|
|
45
|
+
|
|
46
|
+
@app.get("/resource", dependencies=[Depends(limiter)])
|
|
47
|
+
def get_resource():
|
|
48
|
+
return {"ok": True}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The dependency supports the same options as the middleware: `scope_by_tenant`, `limit_resolver`, and a custom `store`.
|
|
52
|
+
|
|
53
|
+
## Store interface
|
|
54
|
+
|
|
55
|
+
The limiter uses a store abstraction so you can inject Redis or other backends.
|
|
56
|
+
|
|
57
|
+
- Default: `InMemoryRateLimitStore` (best-effort, single-process)
|
|
58
|
+
- Interface: `RateLimitStore` with `incr(key, window) -> (count, limit, resetEpoch)`
|
|
59
|
+
|
|
60
|
+
### Redis store
|
|
61
|
+
|
|
62
|
+
Use `RedisRateLimitStore` for multi-instance deployments. It implements a fixed-window counter
|
|
63
|
+
with atomic increments and sets expiry to the end of the window.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import redis
|
|
67
|
+
from svc_infra.api.fastapi.middleware.ratelimit_store import RedisRateLimitStore
|
|
68
|
+
from svc_infra.api.fastapi.middleware.ratelimit import SimpleRateLimitMiddleware
|
|
69
|
+
|
|
70
|
+
r = redis.Redis.from_url("redis://localhost:6379/0")
|
|
71
|
+
store = RedisRateLimitStore(r, limit=120, prefix="rl")
|
|
72
|
+
|
|
73
|
+
app.add_middleware(SimpleRateLimitMiddleware, limit=120, window=60, store=store)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Notes:
|
|
77
|
+
- Fixed-window counters are simple and usually sufficient. For smoother limits, consider
|
|
78
|
+
sliding window or token bucket in a future iteration.
|
|
79
|
+
- Use a namespace/prefix per environment/tenant if needed.
|
|
80
|
+
|
|
81
|
+
## Request size guard
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from svc_infra.api.fastapi.middleware.request_size_limit import RequestSizeLimitMiddleware
|
|
85
|
+
|
|
86
|
+
app.add_middleware(RequestSizeLimitMiddleware, max_bytes=1_000_000)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- Returns 413 with a Problem+JSON-like structure when `Content-Length` exceeds `max_bytes`.
|
|
90
|
+
|
|
91
|
+
## Metrics hooks
|
|
92
|
+
|
|
93
|
+
Hooks live in `svc_infra.obs.metrics` and are no-ops by default. Assign them to log or emit metrics.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import logging
|
|
97
|
+
import svc_infra.obs.metrics as metrics
|
|
98
|
+
|
|
99
|
+
logger = logging.getLogger(__name__)
|
|
100
|
+
|
|
101
|
+
metrics.on_rate_limit_exceeded = lambda key, limit, retry: logger.warning(
|
|
102
|
+
"rate_limited", extra={"key": key, "limit": limit, "retry_after": retry}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
metrics.on_suspect_payload = lambda path, size: logger.warning(
|
|
106
|
+
"suspect_payload", extra={"path": path, "size": size}
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Tuning tips
|
|
111
|
+
|
|
112
|
+
- Prefer API key or user ID for `key_fn`; fall back to IP if unauthenticated.
|
|
113
|
+
- Keep windows small (e.g., 60s); layer multiple limits when needed.
|
|
114
|
+
- For distributed deployments, use `RedisRateLimitStore` for atomic increments.
|
|
115
|
+
- Consider separate limits for read vs write routes.
|
|
116
|
+
- Combine with request size limits and auth lockout for layered defense.
|
|
117
|
+
|
|
118
|
+
## Related
|
|
119
|
+
|
|
120
|
+
- Timeouts & Resource Limits: `./timeouts-and-resource-limits.md` — complements rate limits by bounding slow uploads, long handlers, and downstream timeouts.
|
|
121
|
+
|
|
122
|
+
## Testing
|
|
123
|
+
|
|
124
|
+
- Use `-m ratelimit` to select rate-limiting tests.
|
|
125
|
+
- `-m security` also includes these in this repo by default.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# svc-infra Integration Review
|
|
2
|
+
|
|
3
|
+
## Executive Summary
|
|
4
|
+
- `setup_service_api` remains the high-level entry point for API construction. It auto-wires common middleware (request IDs, error handling, idempotency, rate limiting) and OpenAPI customisation so downstream services only supply router packages and metadata. 【F:src/svc_infra/api/fastapi/setup.py†L1-L187】
|
|
5
|
+
- Auth, SQL, jobs, cache, and observability still expose single-call helpers (`add_auth_users`, `add_sql_db`/`setup_sql`, `easy_jobs`, `init_cache`, `add_observability`) that hide wiring details while keeping flags and dependency overrides for advanced scenarios. 【F:src/svc_infra/api/fastapi/auth/add.py†L27-L329】【F:src/svc_infra/api/fastapi/db/sql/add.py†L1-L118】【F:src/svc_infra/jobs/easy.py†L1-L28】【F:src/svc_infra/cache/__init__.py†L1-L29】【F:src/svc_infra/obs/add.py†L1-L68】
|
|
6
|
+
- Security primitives (lockout, session rotation, RBAC/ABAC, audit chain) are packaged as small composable functions and documented patterns, making them easy to reuse outside FastAPI while still integrating with the default auth routers. 【F:src/svc_infra/security/lockout.py†L1-L96】【F:src/svc_infra/security/session.py†L1-L98】【F:src/svc_infra/security/permissions.py†L1-L148】【F:src/svc_infra/security/audit_service.py†L1-L73】【F:docs/security.md†L1-L96】
|
|
7
|
+
- Webhook utilities provide signature verification, a subscription service, and a default router, but they do not yet follow the same add-helper pattern (there is no `add_webhooks(app, ...)` or env-driven persistence wiring). This is the primary deviation from the established DX story. 【F:src/svc_infra/webhooks/fastapi.py†L1-L37】【F:src/svc_infra/webhooks/service.py†L1-L45】【F:src/svc_infra/webhooks/router.py†L1-L55】【F:docs/webhooks.md†L1-L59】
|
|
8
|
+
|
|
9
|
+
## Detailed Findings
|
|
10
|
+
|
|
11
|
+
### API bootstrap and middleware
|
|
12
|
+
- `setup_service_api` builds a parent app and any versioned child apps, runs the OpenAPI mutation pipeline, registers default routers, and attaches middleware without extra code in downstream projects. 【F:src/svc_infra/api/fastapi/setup.py†L1-L187】
|
|
13
|
+
- Scoped docs helpers automatically group endpoints when `add_auth_users` or versioned routers are mounted, so projects get organised API docs with minimal configuration. 【F:src/svc_infra/api/fastapi/auth/add.py†L269-L329】
|
|
14
|
+
- Idempotency and optimistic locking remain opt-in via middleware or dependencies with clear docs; defaults are safe for local use and can be swapped for Redis-backed stores. 【F:docs/idempotency.md†L1-L110】
|
|
15
|
+
|
|
16
|
+
### Authentication and security
|
|
17
|
+
- `add_auth_users` continues to be the main integration surface. It mounts routers based on flags, configures session middleware from env, and exposes API-key plus OAuth support as toggles. Projects can override policy objects or provider models if needed, preserving flexibility. 【F:src/svc_infra/api/fastapi/auth/add.py†L27-L329】
|
|
18
|
+
- Runtime security helpers (principal resolution, `RequireRoles`, `RequireScopes`, `RequirePermission`, ABAC predicates) make it straightforward to guard routes consistently with the dual routers that enforce doc security defaults. 【F:src/svc_infra/api/fastapi/auth/security.py†L1-L146】【F:src/svc_infra/api/fastapi/dual/protected.py†L1-L74】【F:src/svc_infra/security/permissions.py†L1-L148】
|
|
19
|
+
- Low-level primitives (lockout, refresh token rotation, audit chain) remain framework agnostic so other stacks can reuse them. Documentation in `docs/security.md` is aligned with the code and offers wiring recipes. 【F:src/svc_infra/security/lockout.py†L1-L96】【F:src/svc_infra/security/session.py†L1-L98】【F:src/svc_infra/security/audit_service.py†L1-L73】【F:docs/security.md†L1-L96】
|
|
20
|
+
- Recommendation: add a security bundle similar to `setup_sql` that can register `SecurityHeadersMiddleware` and strict CORS when FastAPI is not created via `setup_service_api`. 【F:src/svc_infra/security/headers.py†L1-L35】
|
|
21
|
+
|
|
22
|
+
### Data and jobs
|
|
23
|
+
- SQL helpers offer both granular (`add_sql_db`, `add_sql_resources`) and bundled (`setup_sql`) flows. They guard against duplicate setup and generate CRUD schemas when not provided, keeping the quick-start feel while allowing custom services. 【F:src/svc_infra/api/fastapi/db/sql/add.py†L1-L118】
|
|
24
|
+
- Jobs expose `easy_jobs()` that switches between memory and Redis based on env, matching the pick-style ergonomics used in logging. Documentation explains scheduler tasks and the CLI runner. 【F:src/svc_infra/jobs/easy.py†L1-L28】【F:docs/jobs.md†L1-L65】
|
|
25
|
+
- Cache decorators export both `cache_read` and the friendlier aliases (`cached`, `mutates`) so consumers can adopt them incrementally. 【F:src/svc_infra/cache/__init__.py†L1-L29】
|
|
26
|
+
|
|
27
|
+
### Observability
|
|
28
|
+
- `add_observability` lazily imports instrumentation, respects env flags, and returns a shutdown no-op to keep signature compatibility. It tolerates missing dependencies to avoid burdening projects that do not enable metrics. 【F:src/svc_infra/obs/add.py†L1-L68】
|
|
29
|
+
- Grafana and Prometheus templates are packaged separately, but the code path aligns with the simple toggle approach used elsewhere.
|
|
30
|
+
|
|
31
|
+
### Webhooks and outbox
|
|
32
|
+
- Signature utilities (`require_signature`, `verify_any`) and service abstractions (`WebhookService`, `InMemoryWebhookSubscriptions`) follow the composable style, and docs describe usage alongside the shared job and outbox infrastructure. 【F:src/svc_infra/webhooks/fastapi.py†L1-L37】【F:src/svc_infra/webhooks/service.py†L1-L45】【F:docs/webhooks.md†L1-L59】
|
|
33
|
+
- Gaps relative to the rest of the repo:
|
|
34
|
+
- Router dependencies always create new in-memory stores, so deployments must override dependencies manually. Providing an `add_webhooks(app, store=..., subs=...)` helper that honours env (for example `REDIS_URL`) would mirror `add_sql_db` and `add_observability`. 【F:src/svc_infra/webhooks/router.py†L1-L55】
|
|
35
|
+
- No convenience function registers the router or scheduler tick automatically; consumers must read docs to wire `make_outbox_tick` and queue processors. A top-level helper such as `setup_webhooks(app, outbox=None, inbox=None)` could bundle router inclusion and scheduler guidance.
|
|
36
|
+
|
|
37
|
+
### Documentation and DX consistency
|
|
38
|
+
- Feature guides (security, jobs, webhooks, idempotency) contain quickstart snippets that mirror the API surface, reinforcing the ease-of-setup story. 【F:docs/security.md†L1-L96】【F:docs/jobs.md†L1-L65】【F:docs/webhooks.md†L1-L59】【F:docs/idempotency.md†L1-L110】
|
|
39
|
+
- README remains empty; adding a high-level index pointing to these guides would help teams discover the single-call helpers faster. 【F:README.md†L1-L4】
|
|
40
|
+
- Consider adding a matrix listing which environment variables drive each helper (`SQL_URL`, `REDIS_URL`, `METRICS_PATH`, `AUTH_*`) to highlight parity across modules.
|
|
41
|
+
|
|
42
|
+
## Actionable Suggestions
|
|
43
|
+
1. **Webhook setup helper** — provide `add_webhooks` or `setup_webhooks` to mount the router, accept dependency overrides, and optionally wire scheduler tasks. Default to env-driven outbox or inbox selection to match other modules. 【F:src/svc_infra/webhooks/router.py†L1-L55】
|
|
44
|
+
2. **Security bundle** — offer a helper that installs `SecurityHeadersMiddleware`, strict CORS, and optional signed-cookie settings for apps that bypass `setup_service_api`. This keeps manual FastAPI apps aligned with the default hardening posture. 【F:src/svc_infra/security/headers.py†L1-L35】
|
|
45
|
+
3. **Documentation index** — populate `README.md` with links to the feature guides and highlight key add-style helpers so new projects immediately see the plug-and-play building blocks. 【F:README.md†L1-L4】
|
|
46
|
+
4. **Environment reference** — add a doc or table enumerating env vars consumed by helpers (SQL_URL, REDIS_URL, METRICS_PATH, AUTH_*), emphasising how swapping values maintains flexibility across deployments.
|
|
47
|
+
|
|
48
|
+
Overall, aside from the webhook ergonomics gap and missing README guidance, the codebase maintains the quick-start plus override pattern highlighted in your sample usage while exposing low-level hooks for teams that need deeper control.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Security: Configuration & Examples
|
|
2
|
+
|
|
3
|
+
This guide covers the security primitives built into svc-infra and how to wire them:
|
|
4
|
+
|
|
5
|
+
> ℹ️ Environment variables for the auth/security helpers are catalogued in [Environment Reference](environment.md).
|
|
6
|
+
|
|
7
|
+
- Password policy and breach checking
|
|
8
|
+
- Account lockout (exponential backoff)
|
|
9
|
+
- Sessions and refresh tokens (rotation + revocation)
|
|
10
|
+
- JWT key rotation
|
|
11
|
+
- Signed cookies
|
|
12
|
+
- CORS and security headers
|
|
13
|
+
- RBAC and ABAC
|
|
14
|
+
- MFA policy hooks
|
|
15
|
+
|
|
16
|
+
Module map (examples reference these):
|
|
17
|
+
- `svc_infra.security.lockout` (LockoutConfig, compute_lockout, record_attempt, get_lockout_status)
|
|
18
|
+
- `svc_infra.security.signed_cookies` (sign_cookie, verify_cookie)
|
|
19
|
+
- `svc_infra.security.audit` and `security.audit_service` (hash-chain audit logs)
|
|
20
|
+
- `svc_infra.api.fastapi.auth.gaurd` (password login with lockout checks)
|
|
21
|
+
- `svc_infra.api.fastapi.auth.routers.*` (sessions, oauth routes, etc.)
|
|
22
|
+
- `svc_infra.api.fastapi.auth.settings.get_auth_settings` (cookie + token settings)
|
|
23
|
+
- `svc_infra.api.fastapi.middleware.security_headers` and CORS setup (strict defaults)
|
|
24
|
+
|
|
25
|
+
## Password policy and breach checking
|
|
26
|
+
- Enforced by validators with a configurable policy.
|
|
27
|
+
- Breach checking uses the HIBP k-Anonymity range API; can be toggled via settings.
|
|
28
|
+
|
|
29
|
+
Example toggles (pseudo-config):
|
|
30
|
+
- `AUTH_PASSWORD_MIN_LENGTH=12`
|
|
31
|
+
- `AUTH_PASSWORD_REQUIRE_SYMBOL=True`
|
|
32
|
+
- `AUTH_PASSWORD_BREACH_CHECK=True`
|
|
33
|
+
|
|
34
|
+
## Account lockout
|
|
35
|
+
- Exponential backoff with a max cooldown cap to deter credential stuffing.
|
|
36
|
+
- Attempts tracked by user_id and/or IP hash.
|
|
37
|
+
- Login endpoint blocks with 429 + `Retry-After` during cooldown.
|
|
38
|
+
|
|
39
|
+
Key API (from `svc_infra.security.lockout`):
|
|
40
|
+
- `LockoutConfig(threshold=5, window_minutes=15, base_cooldown_seconds=30, max_cooldown_seconds=3600)`
|
|
41
|
+
- `compute_lockout(fail_count, cfg)` → `LockoutStatus(locked, next_allowed_at, failure_count)`
|
|
42
|
+
- `record_attempt(session, user_id, ip_hash, success)`
|
|
43
|
+
- `get_lockout_status(session, user_id, ip_hash, cfg)`
|
|
44
|
+
|
|
45
|
+
Login integration (simplified):
|
|
46
|
+
```python
|
|
47
|
+
from svc_infra.security.lockout import get_lockout_status, record_attempt
|
|
48
|
+
|
|
49
|
+
# Compute ip_hash from request.client.host
|
|
50
|
+
status = await get_lockout_status(session, user_id=None, ip_hash=ip_hash)
|
|
51
|
+
if status.locked:
|
|
52
|
+
raise HTTPException(429, headers={"Retry-After": ..})
|
|
53
|
+
|
|
54
|
+
user = await user_manager.user_db.get_by_email(email)
|
|
55
|
+
if not user:
|
|
56
|
+
await record_attempt(session, user_id=None, ip_hash=ip_hash, success=False)
|
|
57
|
+
raise HTTPException(400, "LOGIN_BAD_CREDENTIALS")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Sessions and refresh tokens
|
|
61
|
+
- Sessions are enumerable and revocable via the sessions router.
|
|
62
|
+
- Refresh tokens are rotated; old tokens are invalidated via a revocation list.
|
|
63
|
+
|
|
64
|
+
Operational notes:
|
|
65
|
+
- Persist sessions/tokens in a durable DB.
|
|
66
|
+
- Favor short access token TTLs if refresh flow is robust.
|
|
67
|
+
|
|
68
|
+
## JWT key rotation
|
|
69
|
+
- Primary secret plus `old_secrets` allow seamless rotation.
|
|
70
|
+
- Set environment variables:
|
|
71
|
+
- `AUTH_JWT__SECRET="..."`
|
|
72
|
+
- `AUTH_JWT__OLD_SECRETS="old1,old2"`
|
|
73
|
+
|
|
74
|
+
## Signed cookies
|
|
75
|
+
Module: `svc_infra.security.signed_cookies`
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from svc_infra.security.signed_cookies import sign_cookie, verify_cookie
|
|
79
|
+
|
|
80
|
+
sig = sign_cookie({"sub": "user-123"}, secret="k1", exp_seconds=3600)
|
|
81
|
+
payload = verify_cookie(sig, secret="k1", old_secrets=["k0"]) # returns dict
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## CORS and security headers
|
|
85
|
+
- Strict CORS defaults (deny by default). Provide allowlist entries.
|
|
86
|
+
- Security headers middleware sets common protections (X-Frame-Options, X-Content-Type-Options, etc.).
|
|
87
|
+
|
|
88
|
+
Use `svc_infra.security.add.add_security` to install the default middlewares on any
|
|
89
|
+
FastAPI app. By default it adds:
|
|
90
|
+
|
|
91
|
+
- `SecurityHeadersMiddleware` with practical defaults:
|
|
92
|
+
- **Content-Security-Policy**: Allows same-origin resources, inline styles/scripts, data URI images, and HTTPS images. Blocks external scripts and framing.
|
|
93
|
+
- **Strict-Transport-Security**: Forces HTTPS with long max-age and subdomain support
|
|
94
|
+
- **X-Frame-Options**: Blocks framing (DENY)
|
|
95
|
+
- **X-Content-Type-Options**: Prevents MIME sniffing (nosniff)
|
|
96
|
+
- **Referrer-Policy**: Limits referrer leakage
|
|
97
|
+
- **X-XSS-Protection**: Disabled (CSP is the modern protection)
|
|
98
|
+
- A strict `CORSMiddleware` that only enables CORS when origins are provided (via
|
|
99
|
+
parameters or environment variables such as `CORS_ALLOW_ORIGINS`).
|
|
100
|
+
|
|
101
|
+
The default CSP policy is:
|
|
102
|
+
```
|
|
103
|
+
default-src 'self';
|
|
104
|
+
script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;
|
|
105
|
+
style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;
|
|
106
|
+
img-src 'self' data: https:;
|
|
107
|
+
connect-src 'self';
|
|
108
|
+
font-src 'self' https://cdn.jsdelivr.net;
|
|
109
|
+
frame-ancestors 'none';
|
|
110
|
+
base-uri 'self';
|
|
111
|
+
form-action 'self'
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This works out-of-the-box for most web applications, including FastAPI's built-in documentation (Swagger UI, ReDoc), while maintaining strong security.
|
|
115
|
+
|
|
116
|
+
The helper also supports optional toggles so you can match the same cookie and
|
|
117
|
+
header configuration that `setup_service_api` uses.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from fastapi import FastAPI
|
|
121
|
+
|
|
122
|
+
from svc_infra.security.add import add_security
|
|
123
|
+
|
|
124
|
+
app = FastAPI()
|
|
125
|
+
|
|
126
|
+
add_security(
|
|
127
|
+
app,
|
|
128
|
+
cors_origins=["https://app.example.com"],
|
|
129
|
+
headers_overrides={"Content-Security-Policy": "default-src 'self'; script-src 'self'"}, # Stricter CSP
|
|
130
|
+
install_session_middleware=True, # adds Starlette's SessionMiddleware
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Environment variables (applied when parameters are omitted):
|
|
135
|
+
|
|
136
|
+
| Variable | Purpose |
|
|
137
|
+
| --- | --- |
|
|
138
|
+
| `CORS_ALLOW_ORIGINS` | Comma-separated CORS origins (e.g. `https://app.example.com, https://admin.example.com`) |
|
|
139
|
+
| `CORS_ALLOW_METHODS` | Allowed HTTP methods (defaults to `*`) |
|
|
140
|
+
| `CORS_ALLOW_HEADERS` | Allowed headers (defaults to `*`) |
|
|
141
|
+
| `CORS_ALLOW_ORIGIN_REGEX` | Regex used when matching origins (ignored if not set) |
|
|
142
|
+
| `CORS_ALLOW_CREDENTIALS` | Toggle credentials support (`true` / `false`) |
|
|
143
|
+
| `SESSION_COOKIE_NAME` | Session cookie name (defaults to `svc_session`) |
|
|
144
|
+
| `SESSION_COOKIE_MAX_AGE_SECONDS` | Max age for the session cookie (defaults to `14400`) |
|
|
145
|
+
| `SESSION_COOKIE_SAMESITE` | SameSite policy (`lax` by default) |
|
|
146
|
+
| `SESSION_COOKIE_SECURE` | Force the session cookie to be HTTPS-only |
|
|
147
|
+
| `SESSION_SECRET` | Secret key for Starlette's SessionMiddleware |
|
|
148
|
+
|
|
149
|
+
When your service already uses `setup_service_api`, call `add_security` after
|
|
150
|
+
building the parent app if you need additional overrides while keeping the
|
|
151
|
+
defaults intact:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from svc_infra.api.fastapi.setup import setup_service_api
|
|
155
|
+
from svc_infra.security.add import add_security
|
|
156
|
+
|
|
157
|
+
app = setup_service_api(...)
|
|
158
|
+
|
|
159
|
+
add_security(
|
|
160
|
+
app,
|
|
161
|
+
headers_overrides={"Strict-Transport-Security": "max-age=63072000; includeSubDomains"},
|
|
162
|
+
enable_hsts_preload=False,
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## RBAC and ABAC
|
|
167
|
+
- RBAC decorators guard endpoints by role/permission.
|
|
168
|
+
- ABAC evaluates resource ownership and attributes (e.g., `owns_resource`).
|
|
169
|
+
|
|
170
|
+
## MFA policy hooks
|
|
171
|
+
- Policy decides when MFA is required; login returns 401 with `MFA_REQUIRED` and a pre-token when applicable.
|
|
172
|
+
|
|
173
|
+
## Troubleshooting
|
|
174
|
+
- 429 on login: lockout active. Check `Retry-After` and `FailedAuthAttempt` rows.
|
|
175
|
+
- Token invalid post-refresh: confirm rotation + revocation writes.
|
|
176
|
+
- Cookie verification errors: check signing keys/exp.
|