fastapi-m8 3.2.0__tar.gz → 4.0.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.
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.codacy.yml +2 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.dockerignore +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.github/workflows/CI.yaml +26 -9
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.github/workflows/PiPy.yml +5 -7
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.gitignore +1 -4
- fastapi_m8-4.0.0/.gitleaks.toml +15 -0
- fastapi_m8-4.0.0/AGENTS.md +27 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/CHANGELOG.md +189 -0
- fastapi_m8-4.0.0/CLAUDE.md +27 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/PKG-INFO +181 -22
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/README.md +177 -20
- fastapi_m8-4.0.0/constraints-all.txt +237 -0
- fastapi_m8-4.0.0/constraints.txt +114 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/__init__.py +22 -1
- fastapi_m8-4.0.0/fastapi_m8/_api_key.py +415 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/_compat.py +17 -0
- fastapi_m8-4.0.0/fastapi_m8/_deps.py +502 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/_internal_auth.py +5 -2
- fastapi_m8-4.0.0/fastapi_m8/_revocation.py +567 -0
- fastapi_m8-4.0.0/fastapi_m8/_route_audit.py +77 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/_version.py +1 -1
- fastapi_m8-4.0.0/fastapi_m8/config.py +356 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/scripts/docker_start.sh +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/pyproject.toml +8 -3
- fastapi_m8-4.0.0/tests/test_api_key.py +618 -0
- fastapi_m8-4.0.0/tests/test_api_key_deps.py +438 -0
- fastapi_m8-4.0.0/tests/test_ci_policy.py +136 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_compat.py +30 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_config.py +80 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_deps.py +341 -35
- fastapi_m8-4.0.0/tests/test_packaging.py +164 -0
- fastapi_m8-4.0.0/tests/test_public_typing.py +118 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_revocation.py +389 -38
- fastapi_m8-4.0.0/tests/test_route_audit.py +151 -0
- fastapi_m8-3.2.0/.github/workflows/ci.yml +0 -54
- fastapi_m8-3.2.0/fastapi_m8/_deps.py +0 -231
- fastapi_m8-3.2.0/fastapi_m8/_revocation.py +0 -321
- fastapi_m8-3.2.0/fastapi_m8/config.py +0 -186
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.env.example +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.gitattributes +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.github/FUNDING.yml +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.github/dependabot.yml +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/.pydocstyle +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/LICENSE +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/SECURITY.md +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/_app.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/_async_stub.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/_engine.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/_events.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/_health.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/scripts/__init__.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/fastapi_m8/scripts/pre_start.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/__init__.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/conftest.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_app.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_app_extra.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_async_stub.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_config_file_secrets.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_engine.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_event_signing_gate.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_events.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_health.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_host_header_routing.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_internal_auth.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_meta.py +0 -0
- {fastapi_m8-3.2.0 → fastapi_m8-4.0.0}/tests/test_pre_start.py +0 -0
|
File without changes
|
|
@@ -6,6 +6,9 @@ on:
|
|
|
6
6
|
pull_request:
|
|
7
7
|
branches: ["main"]
|
|
8
8
|
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
9
12
|
env:
|
|
10
13
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
11
14
|
|
|
@@ -13,10 +16,10 @@ jobs:
|
|
|
13
16
|
lint:
|
|
14
17
|
runs-on: ubuntu-latest
|
|
15
18
|
steps:
|
|
16
|
-
- uses: actions/checkout@
|
|
19
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
17
20
|
|
|
18
21
|
- name: Set up Python
|
|
19
|
-
uses: actions/setup-python@
|
|
22
|
+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
20
23
|
with:
|
|
21
24
|
python-version: "3.14"
|
|
22
25
|
cache: "pip"
|
|
@@ -33,10 +36,10 @@ jobs:
|
|
|
33
36
|
typecheck:
|
|
34
37
|
runs-on: ubuntu-latest
|
|
35
38
|
steps:
|
|
36
|
-
- uses: actions/checkout@
|
|
39
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
37
40
|
|
|
38
41
|
- name: Set up Python
|
|
39
|
-
uses: actions/setup-python@
|
|
42
|
+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
40
43
|
with:
|
|
41
44
|
python-version: "3.14"
|
|
42
45
|
cache: "pip"
|
|
@@ -50,10 +53,10 @@ jobs:
|
|
|
50
53
|
security:
|
|
51
54
|
runs-on: ubuntu-latest
|
|
52
55
|
steps:
|
|
53
|
-
- uses: actions/checkout@
|
|
56
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
54
57
|
|
|
55
58
|
- name: Set up Python
|
|
56
|
-
uses: actions/setup-python@
|
|
59
|
+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
57
60
|
with:
|
|
58
61
|
python-version: "3.14"
|
|
59
62
|
|
|
@@ -84,10 +87,10 @@ jobs:
|
|
|
84
87
|
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
85
88
|
|
|
86
89
|
steps:
|
|
87
|
-
- uses: actions/checkout@
|
|
90
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
88
91
|
|
|
89
92
|
- name: Set up Python ${{ matrix.python-version }}
|
|
90
|
-
uses: actions/setup-python@
|
|
93
|
+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
91
94
|
with:
|
|
92
95
|
python-version: ${{ matrix.python-version }}
|
|
93
96
|
cache: "pip"
|
|
@@ -100,7 +103,7 @@ jobs:
|
|
|
100
103
|
|
|
101
104
|
- name: Upload coverage to Codecov
|
|
102
105
|
if: matrix.python-version == '3.11' && github.actor != 'dependabot[bot]'
|
|
103
|
-
uses: codecov/codecov-action@
|
|
106
|
+
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
|
104
107
|
with:
|
|
105
108
|
token: ${{ secrets.CODECOV_TOKEN }}
|
|
106
109
|
files: coverage.xml
|
|
@@ -112,3 +115,17 @@ jobs:
|
|
|
112
115
|
with:
|
|
113
116
|
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
|
114
117
|
coverage-reports: coverage.xml
|
|
118
|
+
|
|
119
|
+
secret-scan:
|
|
120
|
+
name: Secret scan (gitleaks)
|
|
121
|
+
runs-on: ubuntu-latest
|
|
122
|
+
|
|
123
|
+
steps:
|
|
124
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
125
|
+
with:
|
|
126
|
+
fetch-depth: 0
|
|
127
|
+
|
|
128
|
+
- uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0
|
|
129
|
+
env:
|
|
130
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
131
|
+
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
|
|
@@ -12,9 +12,9 @@ jobs:
|
|
|
12
12
|
runs-on: ubuntu-latest
|
|
13
13
|
|
|
14
14
|
steps:
|
|
15
|
-
- uses: actions/checkout@
|
|
15
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
16
16
|
|
|
17
|
-
- uses: actions/setup-python@
|
|
17
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
18
18
|
with:
|
|
19
19
|
python-version: "3.x"
|
|
20
20
|
|
|
@@ -52,8 +52,6 @@ jobs:
|
|
|
52
52
|
|
|
53
53
|
- name: Publish release distributions to PyPI
|
|
54
54
|
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
|
|
55
|
-
with:
|
|
56
|
-
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
57
55
|
|
|
58
56
|
sbom:
|
|
59
57
|
name: Generate & attach SBOM
|
|
@@ -65,16 +63,16 @@ jobs:
|
|
|
65
63
|
id-token: write
|
|
66
64
|
|
|
67
65
|
steps:
|
|
68
|
-
- uses: actions/checkout@
|
|
66
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
69
67
|
|
|
70
|
-
- uses: actions/setup-python@
|
|
68
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
71
69
|
with:
|
|
72
70
|
python-version: "3.x"
|
|
73
71
|
|
|
74
72
|
- name: Install package
|
|
75
73
|
run: pip install -e ".[all]"
|
|
76
74
|
|
|
77
|
-
- uses: anchore/sbom-action@
|
|
75
|
+
- uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
|
78
76
|
with:
|
|
79
77
|
artifact-name: fastapi-m8-${{ github.event.release.tag_name }}.sbom.spdx.json
|
|
80
78
|
format: spdx-json
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
title = "fastapi-m8 gitleaks config"
|
|
2
|
+
|
|
3
|
+
[extend]
|
|
4
|
+
useDefault = true
|
|
5
|
+
|
|
6
|
+
[allowlist]
|
|
7
|
+
description = "Project-wide allowlist"
|
|
8
|
+
paths = [
|
|
9
|
+
# Test fixtures may contain deterministic low-entropy test values
|
|
10
|
+
'''tests/''',
|
|
11
|
+
]
|
|
12
|
+
# Add SHAs of any prior accidental secret commits here after rotation:
|
|
13
|
+
# commits = [
|
|
14
|
+
# "abc123def456", # description of what was rotated and when
|
|
15
|
+
# ]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# fastapi-m8
|
|
2
|
+
|
|
3
|
+
## Layer
|
|
4
|
+
Platform (FastAPI application framework)
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
Application factory wiring `auth-sdk-m8` into FastAPI services using `fa-auth-m8`.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Rules
|
|
14
|
+
- No business logic
|
|
15
|
+
- Only reusable service scaffolding
|
|
16
|
+
- Must remain minimal and reusable across services
|
|
17
|
+
- No coupling to fa-auth-m8 or any domain service
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Workspace integration
|
|
22
|
+
|
|
23
|
+
When nested in a workspace, locate the nearest ancestor containing
|
|
24
|
+
`.workspace/policy.index.json` and apply its `python` policy.
|
|
25
|
+
|
|
26
|
+
If no workspace policy exists, use this file, `pyproject.toml`, repository
|
|
27
|
+
documentation and existing CI as the authoritative local context.`
|
|
@@ -5,6 +5,195 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · Versioning:
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [4.0.0] — 2026-07-17 · auth-sdk-m8 3.0.0 alignment — canonical role/flag invariant + JTI-status v2 + remote API-key principal
|
|
9
|
+
|
|
10
|
+
> **MAJOR — coordinated release with `auth-sdk-m8 3.0.0`.** `fastapi-m8` raises the SDK
|
|
11
|
+
> floor to `>=3.0.0,<4.0.0` to consume the canonical role/flag authorization invariant
|
|
12
|
+
> (`has_superuser_privileges()` and `has_minimum_role()`), the subject-bound JTI-status v2
|
|
13
|
+
> introspection contract, and the API-key principal/introspection schemas. All role guards
|
|
14
|
+
> now delegate to the shared SDK policy helpers, and the new remote API-key principal
|
|
15
|
+
> dependencies (`get_current_api_key_reader`/`_writer` and `require_api_key_role(...)`)
|
|
16
|
+
> resolve a key to its owner's **current** authority through the issuer's introspection
|
|
17
|
+
> endpoint, with no administrative or superuser capability surface. The API-key path is
|
|
18
|
+
> inherently fail-closed, never caches a positive principal, and never retries after
|
|
19
|
+
> transmission — ensuring a role downgrade lands on the key's very next request.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **JTI-status v2 (subject-bound, generation-tagged)** — `RemoteRevocationClient`
|
|
24
|
+
now speaks the v2 introspection contract: the request asserts the subject read
|
|
25
|
+
from the token (`{jti, expected_user_id, schema_version}`), and an active reply
|
|
26
|
+
carries the owner's `auth_generation`, which tags the cache entry it produces.
|
|
27
|
+
As defense in depth an active reply naming a different subject is refused and
|
|
28
|
+
never cached. **The v2 decision never falls open:** a reply this release cannot
|
|
29
|
+
interpret — malformed, unknown `schema_version`, or a pre-2.0 issuer's bare
|
|
30
|
+
`{"active": true}` with no generation — raises the new `RevocationDecisionError`
|
|
31
|
+
(a `RevocationCheckError`, so dependencies still answer `503`) regardless of
|
|
32
|
+
`ACCESS_REVOCATION_FAILURE_MODE`, which remains a transport-outage escape only.
|
|
33
|
+
- **`AuthDeps.handle_auth_event(event)`** — the supported `on_event` handler for
|
|
34
|
+
`build_event_stream_client`, so no service re-derives the eviction rules
|
|
35
|
+
locally. Accepts **both v1 and v2** `session-revoked` events for the rollout
|
|
36
|
+
interval in which the issuer emits v2 before every consumer has upgraded, and
|
|
37
|
+
routes `user-deleted` as before. v2 events apply the exact `<`/`==`/`>`
|
|
38
|
+
watermark rule per user, deduplicated on the durable `event_id` (never the SSE
|
|
39
|
+
transport id, which resets on issuer restart), so a replay is idempotent while
|
|
40
|
+
sibling JTI events at one generation stay distinguishable. A user-wide event
|
|
41
|
+
evicts the user's entries older than the revoking generation and spares
|
|
42
|
+
sessions minted after it. A v1 event carries no generation to compare, so it
|
|
43
|
+
evicts the whole user; an unusable payload flushes the cache.
|
|
44
|
+
- **Remote API-key principal dependencies** — `AuthDeps` gains
|
|
45
|
+
`get_current_api_key_principal`, the `require_api_key_role(...)` factory, and its
|
|
46
|
+
`get_current_api_key_reader`/`_writer` specializations, built by the same single
|
|
47
|
+
`build_auth_deps()` call as the JWT guards. A key presented on `X-API-Key` is
|
|
48
|
+
resolved to its **owner's current authority** through the issuer's
|
|
49
|
+
`POST /private/v1/api-keys/introspect`, then authorized with the same shared SDK
|
|
50
|
+
predicate the JWT guards use. The key itself carries no role, so a
|
|
51
|
+
`writer → reader` downgrade denies the key's very next request in every token
|
|
52
|
+
mode. Effective authority is an intersection that can only narrow: the owner's
|
|
53
|
+
live role ∩ the key's immutable `access_mode` ∩ the matching audience ∩ the route
|
|
54
|
+
policy. The members are `None` unless `API_KEY_INTROSPECTION_ENABLED=true`.
|
|
55
|
+
- **`ApiKeyIntrospectionClient`** — the issuer introspection client, alongside
|
|
56
|
+
`RemoteRevocationClient` and following the same split (the SDK owns the schemas,
|
|
57
|
+
the transport lives here). Bounded connection pool and concurrency semaphore,
|
|
58
|
+
redirects disabled, a hard response-size cap enforced mid-read, a consecutive-
|
|
59
|
+
failure circuit breaker, and pool-timeout-bounded load shedding.
|
|
60
|
+
- **Dedicated API-key configuration group** — `API_KEY_INTROSPECTION_ENABLED`,
|
|
61
|
+
`API_KEY_INTROSPECTION_URL` (or derived from `INTROSPECTION_URL`),
|
|
62
|
+
`_SCHEMA_VERSION`, `_CONNECT_TIMEOUT`, `_READ_TIMEOUT`, `_POOL_TIMEOUT`,
|
|
63
|
+
`_MAX_CONCURRENCY`, `_MAX_RESPONSE_BYTES`, `_CIRCUIT_FAILURE_THRESHOLD`, and
|
|
64
|
+
`_CIRCUIT_RESET_SECONDS`. Enabling the feature without an endpoint,
|
|
65
|
+
`INTERNAL_CLIENT_ID`, `PRIVATE_API_SECRET`, or with a schema version this SDK
|
|
66
|
+
release cannot speak **fails at startup**, so a half-configured guard never
|
|
67
|
+
serves traffic.
|
|
68
|
+
- **Public exports** — `API_KEY_HEADER`, `ApiKeyIntrospectionError`,
|
|
69
|
+
`ApiKeyQuotaExceededError`, and `derive_api_key_introspection_url`.
|
|
70
|
+
- **`AuthDeps.get_current_active_writer`** — the JWT writer dependency, built by the
|
|
71
|
+
single `build_auth_deps()` call alongside the existing authenticated/admin/superuser
|
|
72
|
+
members. Requires a minimum role of `WRITER`, so `WRITER`/`ADMIN`/`SUPERADMIN` pass
|
|
73
|
+
and `USER`/`READER` receive the unchanged `403 "The user doesn't have enough
|
|
74
|
+
privileges"` response.
|
|
75
|
+
- **`audit_api_key_routes(app, bare_dependency=..., exempt_paths=...)`** — walks a
|
|
76
|
+
built `FastAPI` app's routes and reports any that depend directly on the bare
|
|
77
|
+
`get_current_api_key_principal` instead of a role-capped dependency
|
|
78
|
+
(`require_api_key_role(...)`'s output). Depending on the bare dependency alone
|
|
79
|
+
grants no capability, but a route wired to it directly is a wiring mistake this
|
|
80
|
+
catches at test time (§3.3.1). Returns `BareApiKeyDependency(path, methods)`
|
|
81
|
+
findings; an explicit `exempt_paths` allowlist covers intentional read-only
|
|
82
|
+
routes (e.g. a `/verify`-style endpoint).
|
|
83
|
+
|
|
84
|
+
### Changed
|
|
85
|
+
|
|
86
|
+
- **All JWT role guards now authorize through the `auth-sdk-m8` policy helpers.**
|
|
87
|
+
`get_current_active_writer`/`_admin` delegate the hierarchy to
|
|
88
|
+
`has_minimum_role()` (replacing the local `RoleType.is_valid_role_auth()` call), so
|
|
89
|
+
the role ordering exists only in the SDK and the two guards cannot drift.
|
|
90
|
+
|
|
91
|
+
### Security
|
|
92
|
+
|
|
93
|
+
- **`get_current_active_writer`/`_admin`/`_superuser` never answer from the positive
|
|
94
|
+
revocation cache (`REV-CACHE-01`, 3.5.4).** They now resolve the token through a
|
|
95
|
+
dedicated fresh-only path (`RemoteRevocationClient.is_revoked(..., bypass_cache=True)`)
|
|
96
|
+
instead of the shared `get_current_user` dependency, so the first role-sensitive
|
|
97
|
+
request after a revocation commits always observes the new state. The short-TTL
|
|
98
|
+
positive cache (`REVOCATION_CACHE_TTL_SECONDS`) now serves only the general
|
|
99
|
+
authenticated tier (`CurrentUser`/`get_current_user`); a bypassed lookup still
|
|
100
|
+
refreshes the shared cache entry so the general tier benefits from it too.
|
|
101
|
+
- **`get_current_active_superuser` now requires canonical dual evidence.** It delegates
|
|
102
|
+
to the SDK's `has_superuser_privileges()`, which demands `role == SUPERADMIN` **and**
|
|
103
|
+
`is_superuser=True` together. Previously the guard checked the `is_superuser` flag and
|
|
104
|
+
the role in two separate steps; the flag is no longer sufficient evidence on its own at
|
|
105
|
+
any step. A token whose claims disagree (e.g. `is_superuser=true` with a lower role) is
|
|
106
|
+
already rejected at validation by the SDK 3.0.0 model invariant — this guard-level check
|
|
107
|
+
is defense in depth for a principal that reaches the dependency inconsistent.
|
|
108
|
+
- **No role guard consults `is_superuser` for a role threshold.** The writer and admin
|
|
109
|
+
dependencies are pure role checks, so a stray `is_superuser=true` can never bypass them.
|
|
110
|
+
- **An API key never grants administrative or superuser authority.** No
|
|
111
|
+
`get_current_api_key_admin`/`_superuser` dependency exists and no setting creates
|
|
112
|
+
one — `require_api_key_role()` raises `ApiKeyCapabilityCeilingError` at wiring time
|
|
113
|
+
for any role above `WRITER`. An owner's `ADMIN`/`SUPERADMIN` role grants an API-key
|
|
114
|
+
request nothing beyond writer-level capability, and `is_superuser` on the principal
|
|
115
|
+
is evidence about the owner's record, never authority. (Earlier drafts of this work
|
|
116
|
+
described a remote `_superuser` specialization disabled behind a per-deployment
|
|
117
|
+
opt-in; it is **removed**, not disabled-by-default.)
|
|
118
|
+
- **The API-key path is fail-closed with no configurable escape.** It never reads
|
|
119
|
+
`ACCESS_REVOCATION_FAILURE_MODE`, whose `fail_open` value must not exist here: an
|
|
120
|
+
issuer outage, timeout, open circuit, shed request, oversized/malformed response,
|
|
121
|
+
unknown schema version, or an audience mismatch always denies with `503`. There is
|
|
122
|
+
no fallback to a cached principal, a locally stored role, or bare key validity.
|
|
123
|
+
- **No positive principal caching.** Every capability-bearing request introspects the
|
|
124
|
+
issuer; only HTTP connection pooling is reused. `auth_generation` on the response is
|
|
125
|
+
evidence for the current decision, never authorization to reuse the principal later.
|
|
126
|
+
- **No post-transmission retries.** Introspection consumes the key's quota, so only a
|
|
127
|
+
failure known to precede transmission (connection establishment) is retried once. A
|
|
128
|
+
read timeout or `5xx` is never replayed, since that could double-charge the owner.
|
|
129
|
+
- **The raw key is confined to the outbound payload.** It is a `SecretStr` in every
|
|
130
|
+
model, and the request body is built explicitly at the transmission point rather
|
|
131
|
+
than serialized generically (which would send the mask). It never appears in the
|
|
132
|
+
URL, query, logs, traces, metrics, exception text, or model `repr`. Redirects are
|
|
133
|
+
disabled so the key cannot be replayed to an unauthenticated host.
|
|
134
|
+
- **Denials carry no owner-state oracle.** An unknown, revoked, or expired key and
|
|
135
|
+
every missing/inactive/inconsistent-owner cause share the single generic
|
|
136
|
+
`401 Invalid or expired API key`, identical to the response for no key at all.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## [3.3.0] — 2026-07-02 · legacy consumer private-auth is development-only (item 11.2b) + supply-chain hardening (11.6/11.7/11.8)
|
|
141
|
+
|
|
142
|
+
> **MINOR — security remediation, no public API removed.** The only runtime behaviour change
|
|
143
|
+
> is a new fail-closed boot check: a production/strict consumer (`ENVIRONMENT=production` or
|
|
144
|
+
> `STRICT_PRODUCTION_MODE=true`) that still runs legacy single-shared-secret private auth
|
|
145
|
+
> (`INTROSPECTION_URL` set, `INTERNAL_CLIENT_ID` unset) now raises at settings construction.
|
|
146
|
+
> Such a consumer already could not authenticate against a hardened `fa-auth-m8` issuer
|
|
147
|
+
> (item 11.2a retired the shared-secret fallback), so this converts a latent deployment fault
|
|
148
|
+
> into an explicit error rather than changing any working configuration. Local/development is
|
|
149
|
+
> unaffected. `auth-sdk-m8` floor raised to `>=2.1.1,<3.0.0` (carries the SDK's latest
|
|
150
|
+
> dependency/CI maintenance release; no new SDK API is required).
|
|
151
|
+
|
|
152
|
+
### Security
|
|
153
|
+
|
|
154
|
+
- **Legacy single-shared-secret private-auth mode is now development-only (OWASP remediation
|
|
155
|
+
item 11.2b).** `ConsumerServiceSettings` gained a `model_validator` that fails settings
|
|
156
|
+
construction when a production/strict consumer (`ENVIRONMENT=production` or
|
|
157
|
+
`STRICT_PRODUCTION_MODE=true`) has `INTROSPECTION_URL` configured but no
|
|
158
|
+
`INTERNAL_CLIENT_ID`. `fa-auth-m8` has retired the issuer's legacy single-shared-secret
|
|
159
|
+
private-API fallback (item 11.2a), so a production/strict consumer left in legacy
|
|
160
|
+
token-only mode is guaranteed to fail against a hardened issuer — this turns that latent
|
|
161
|
+
deployment fault into an explicit, fail-closed boot error. Legacy mode remains valid for
|
|
162
|
+
local/development.
|
|
163
|
+
- **Coherent per-consumer private-auth group validation.** The same validator now rejects
|
|
164
|
+
(in every environment) `SERVICE_TOKEN_EXCHANGE_ENABLED=true` without `INTERNAL_CLIENT_ID`
|
|
165
|
+
(exchange has no identity to present and would silently downgrade to legacy) and
|
|
166
|
+
`INTERNAL_CLIENT_ID` set without `PRIVATE_API_SECRET` (per-consumer mode needs its
|
|
167
|
+
bootstrap secret). No validation message echoes a secret value.
|
|
168
|
+
- **PyPI Trusted Publishing is now the sole publish mechanism (item 11.6).** The long-lived
|
|
169
|
+
`PYPI_API_TOKEN` secret was removed from `PiPy.yml`; the release job publishes via OIDC
|
|
170
|
+
(`id-token: write`) into a protected `pypi` environment. No API token is stored in the repo
|
|
171
|
+
or referenced by any workflow.
|
|
172
|
+
- **CI policy tests (item 11.7).** New `tests/test_ci_policy.py` asserts the supply-chain
|
|
173
|
+
posture in CI: no `PYPI_API_TOKEN` reference, `id-token: write` present, the protected
|
|
174
|
+
`pypi` environment is used, the duplicate `ci.yml` is gone, the secret-scan job is present,
|
|
175
|
+
every action is SHA-pinned in both workflows, and key runtime packages are pinned in
|
|
176
|
+
`constraints-all.txt`.
|
|
177
|
+
- **Locked dependency constraints (item 11.8).** `constraints.txt` and `constraints-all.txt`
|
|
178
|
+
are generated via `pip-compile` from public PyPI only (no custom index), giving CI a
|
|
179
|
+
reproducible, fully-pinned install closure.
|
|
180
|
+
|
|
181
|
+
### Changed
|
|
182
|
+
|
|
183
|
+
- CI workflows consolidated — the secret-scan job was absorbed into `CI.yaml` and the
|
|
184
|
+
duplicate `ci.yml` removed; dependabot SHA bumps for `checkout`, `setup-python`,
|
|
185
|
+
`sbom-action`, `codecov-action`, and `gitleaks-action` applied across both workflows.
|
|
186
|
+
- `COMPAT_MATRIX` gains a `3.3` entry (`auth-sdk-m8 >=2.1.0,<3.0.0`, unchanged floor).
|
|
187
|
+
|
|
188
|
+
### Docs
|
|
189
|
+
|
|
190
|
+
- README *Per-consumer internal auth* section and the defaults-by-layer table now document
|
|
191
|
+
that legacy mode is development-only and fatal under production/strict; removed wording
|
|
192
|
+
that implied the issuer still offers a shared-secret fallback. Compatibility table updated
|
|
193
|
+
with the `3.1.0`–`3.3.0` rows.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
8
197
|
## [3.2.0] — 2026-06-28 · ungated `/health` constant liveness body (item 9.4 Design B)
|
|
9
198
|
|
|
10
199
|
> **MINOR — additive, backward-compatible.** The only behaviour change is in the ungated
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# fastapi-m8
|
|
2
|
+
|
|
3
|
+
## Layer
|
|
4
|
+
Platform (FastAPI application framework)
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
Application factory wiring `auth-sdk-m8` into FastAPI services using `fa-auth-m8`.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Rules
|
|
14
|
+
- No business logic
|
|
15
|
+
- Only reusable service scaffolding
|
|
16
|
+
- Must remain minimal and reusable across services
|
|
17
|
+
- No coupling to fa-auth-m8 or any domain service
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Workspace integration
|
|
22
|
+
|
|
23
|
+
When nested in a workspace, locate the nearest ancestor containing
|
|
24
|
+
`.workspace/policy.index.json` and apply its `python` policy.
|
|
25
|
+
|
|
26
|
+
If no workspace policy exists, use this file, `pyproject.toml`, repository
|
|
27
|
+
documentation and existing CI as the authoritative local context.
|