Repository navigation
feat(api_tokens): personal access tokens and service API keys (#455) - #484
Merged
Merged
Conversation
…carve-out (#455) UserContext gains auth_kind ("session" by default) and scopes (None = uncapped), wired through from_user / to_session_dict / from_session_dict. auth.contracts.tokens holds the credential kinds and the smpat_/smsk_ prefixes. - resolve_principal_permissions and the bare-router fallback cap the resolved set at the principal's scopes (apply_scope_cap); an admin's wildcard expands to the scope list only. - require_permission skips its admin shortcut for a scoped principal; the /admin index, the Doctor page and the maintenance bypass use the new is_admin_principal, which is False for any token. - users and keycloak providers return None at once for an api_tokens bearer value instead of looking it up. - the tenants resolver binds a service principal to its pinned tenant and ignores the tenant header and subdomain for it. Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
New module simple_module_api_tokens. Tokens are smpat_/smsk_ + 32 random bytes in base62; only sha256(token) and an 8-char display prefix are stored, and the secret is returned once. A principal resolver does one indexed lookup by hash per request (no cache), refuses revoked, expired, unknown and inactive/disabled-owner tokens, and throttles last_used_at to one conditional UPDATE per 5 minutes in its own transaction. Scopes must be held by the creator at creation and cap the principal on every request on top of the owner's live grants. Service keys act as svc:<id> with roles chosen at creation (never admin or tenant:*) and are pinned to their tenant. Tokens can only be created, listed and revoked from a session (403 for any token), with RequiresCsrf on every mutation. Self-service page at /api-tokens/ (user menu), admin screen at /admin/api-tokens/ (Access group). Create/revoke are audited; the hash is excluded. Migration 51b2058d8afd (branch label api_tokens), verified with the Postgres round trip. Added to the release matrix; not to `smpy new`. Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
The scaffolded tsconfig set baseUrl, which TypeScript 6 rejects (TS5102) and fails `make lint`; views.py imported InertiaResponse from a package that does not exist; package.json peered @inertiajs/react ^2. Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
Hide role-gated menu entries from token principals, vary PAT tenant resolution on Authorization, fix scaffold views import order, and add tests for scaffold templates, UserContext token fields and is_admin_principal. Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
- M1: simple_module_hosting.token_guard, an app-wide dependency. API tokens reach only permission-gated, non-page routes (or TokenAllowed). Other routes get a 403; public routes serve the token as anonymous. - M2: PATs are pinned to their creation tenant like service keys and need an active membership there, else 401. Token answers vary on Authorization. - M3: provider bearer credentials are auth_kind "bearer" (unscoped, full rights). Token management requires a cookie session with no Authorization header. - L2: /users/me fails closed on a non-UUID principal. - L5: drop tenant:* and admin from stored service-key roles. - Refuse a PAT for an account the resolver cannot load (Keycloak), with a 400. - L1: "Revoke all" endpoint and button. L1/L3/L4 are documented decisions. Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
Guard accepted websockets (HTTPConnection), page marker now enforced at request time so nested include_router cannot hide it, page beats TokenAllowed. Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
…s 4) - N2: layout shared props render a token principal as anonymous (no auth user, permissions, menus or tenant memberships on 403/public pages). - N3: a PAT requires its owner to currently hold api_tokens.use (UserContext.required_permission, checked tenant-aware after tenant resolution); otherwise the request is demoted to anonymous -> 401. - N4: demoting a token to anonymous also re-resolves the tenant as anonymous (TenantMiddleware re-resolution, Vary kept). - N1: token_guard reads FastAPI's effective route context so include_router(dependencies=[...]) gates and page markers are seen; falls back to the original route (fail closed). Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
Deploying simple-module-python with
|
| Latest commit: |
76d87df
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://1922a848.simple-module-python.pages.dev |
| Branch Preview URL: | https://feat-api-tokens-455.simple-module-python.pages.dev |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
# Conflicts: # CHANGELOG.md # framework/hosting/simple_module_hosting/_module_routes.py
This was referenced Oct 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What it adds
New
api_tokensmodule (simple_module_api_tokens) giving users personal access tokens (PATs) and admins service API keys.smpat_<43 base62>(acts as its owner), service keysmsk_<43 base62>(acts assvc:<token id>, no person). Plaintext is shown once; only an 8-char display prefix is kept.sha256(token)is stored.*never survives in the cap. A user can only pick scopes they hold.admin,tenant:*and unknown roles with 422)./api-tokens(create, copy-once secret, list, revoke, revoke all) and admin page at/admin/api-tokens(all tokens, service keys).api_tokens.use/ admin permissions, audit events for create/revoke, and a "revoke all" action.docs/modules/api_tokens.md.Framework changes
UserContextgainsauth_kind,scopesandrequired_permission.TokenAllowed, never on pages (pages answer 403 with anonymous layout props, so the token owner'sauth/menusdo not leak).is_admin_principalhelper.auth_kindfor users access tokens and keycloak JWTs.smpat_/smsk_to the token resolver.make new-module(tsconfig, Inertia import, peer version).Behaviour changes
auth_kind == "bearer"instead of"session"; they can no longer mint PATs.TokenAllowed.Security review
Adversarial review found 3 medium (M1 tokens reaching sign-in-only routes, M2 PAT not tenant-bound, M3 bearer tokens minting 365-day PATs), lows (L1-L5) and follow-ups N1-N4 (page marker lost through nested
include_router; refused-token pages carrying ownerauth/menus; revoke-all/list outlivingapi_tokens.use; public-route tenant context). M1-M3, L2, L5 and N1-N4 are fixed and re-verified; L1, L3, L4 are documented decisions (below).Known limitations and decisions
owner_user_id(module isolation).QA
8/8 scenarios passed (PAT lifecycle, token curl matrix, revoke, service keys, admin scoping, expiry, es fallback, cookie sessions). P3 notes: API-level 422 text for empty name/scopes is not translated (unreachable from UI); only an
encatalog is shipped (es shows English, no raw keys); pre-existing: fresh installs have no "user" role, so self-registered users lackapi_tokens.useuntil an admin assigns one.Release note for maintainer
Register a PyPI pending trusted publisher for
simple_module_api_tokensbefore the next release, or that leg 403s and blocks the tag.Verification (this run, rebased on origin/main 138c30b, already up to date)
make lint: passmake test-py: 4184 passed, 11 skipped, 65 deselected, 1 xfailedmake test-js: 71 files, 548 tests passedmake migrations-roundtrip-pg: pass (upgrade, downgrade, upgrade,alembic checkclean)make doctor: 0 errors, 0 warnings, 1 info (SM026)Part of the #447-#459 batch (spec lands with #481).
Closes #455
https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4