docs: add Claude AI assistant rules and configuration
Add comprehensive rule documentation for AI-assisted development covering authentication surfaces, outbound-call safety invariants, and other project conventions to guide Claude's understanding of critical system behaviors.
This commit is contained in:
69
.claude/rules/auth-surfaces.md
Normal file
69
.claude/rules/auth-surfaces.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
description: Casdoor SSO for the browser + owner-minted PATs for MCP/CLI; owner-only on every surface; one resolver; ?token= fallback; dev-owner on loopback
|
||||
paths:
|
||||
- "auth.py"
|
||||
- "api/auth.py"
|
||||
- "api/tokens.py"
|
||||
- "api/deps.py"
|
||||
- "api/websocket.py"
|
||||
- "mcp_server/server.py"
|
||||
- "main.py"
|
||||
---
|
||||
|
||||
# Authentication across the four surfaces
|
||||
|
||||
Auth is **Casdoor SSO for the browser + owner-minted Personal Access Tokens for
|
||||
MCP/CLI**, and the gateway is **owner-only**: exactly one operator (the Casdoor
|
||||
user whose name matches `OWNER_NAME`) may use any surface; every other identity
|
||||
gets 403. There is one resolver behind all of it — don't add a second auth
|
||||
mechanism, a per-surface token, or a bypass.
|
||||
|
||||
- **`resolve_bearer(session, raw_token)` in [auth.py](../../auth.py) is the single
|
||||
resolver.** It turns a bearer string into a `User` (or `None`), classifying it
|
||||
as a PAT (`hs_pat_` prefix → hash lookup) or a Casdoor JWT (RS256, validated
|
||||
against the endpoint's JWKS). `resolve_from_header_or_query` wraps it to accept
|
||||
the token from the `Authorization` header *or* a `?token=` query param. Every
|
||||
surface funnels through these — REST, WebSocket, MCP.
|
||||
|
||||
- **Owner gating is `is_owner(user)` + `get_current_owner`.** `is_owner` matches
|
||||
`user.name == OWNER_NAME` (SSO) or the dev-owner sub (dev mode). REST routers
|
||||
carry `dependencies=[Depends(get_current_owner)]` (aliased `_auth` in
|
||||
[main.py](../../main.py)) → 401 if unauthenticated, 403 if not owner. New
|
||||
protected routers get the same dependency. `/auth/me` is the one exception: it
|
||||
resolves the user *without* the owner gate so a signed-in non-owner sees
|
||||
`is_owner:false` (the dashboard's "not authorized" screen) instead of a bare
|
||||
401.
|
||||
|
||||
- **The `?token=` query-param fallback is intentional and narrow.** Browsers
|
||||
can't set headers on a WebSocket connect or on an `<audio src>`/`<a href>`
|
||||
recording download, so the current token (Casdoor JWT, or a PAT) rides as
|
||||
`?token=`. It's validated by the same resolver as the header. Don't widen it or
|
||||
remove it without accounting for those two consumers.
|
||||
|
||||
- **WebSocket checks ownership itself** ([api/websocket.py](../../api/websocket.py)
|
||||
`_authorize`) rather than via a router dependency, because WS handshakes don't
|
||||
run FastAPI dependencies the same way. It opens a `session_scope`, resolves the
|
||||
bearer (header or `?token=`), and closes with **4401** unless the caller is the
|
||||
owner. Keep the check **before** `websocket.accept()`.
|
||||
|
||||
- **MCP is gated by the ASGI `_owner_only_mcp` wrapper in main.py**, not by
|
||||
FastMCP auth. `create_mcp_server` builds `FastMCP(auth=None)`; the wrapper reads
|
||||
the ASGI scope's `Authorization` header, resolves it via the same
|
||||
`resolve_from_header_or_query` + `is_owner`, and short-circuits non-owner
|
||||
requests with 401/403 (plus an RFC 9728 `WWW-Authenticate` header pointing at
|
||||
`/.well-known/oauth-protected-resource/mcp`). This is why PATs *and* JWTs both
|
||||
work on `/mcp` with one code path. The nested `mcp_http_app.lifespan` still runs
|
||||
— the wrapper is pure middleware around the inner app.
|
||||
|
||||
- **Dev mode is the loopback bypass, not a token.** `CASDOOR_ENABLED=false` makes
|
||||
every request resolve to the dev owner — permitted **only** on a loopback bind.
|
||||
`_check_startup_config` in [main.py](../../main.py) exits if SSO is disabled and
|
||||
`HOST` is off-loopback (the network would see a dev-owner-open gateway), and
|
||||
exits if SSO is enabled but `CASDOOR_ENDPOINT`/`CLIENT_ID`/`CLIENT_SECRET`/
|
||||
`OWNER_NAME` is missing. **Never weaken these to "warn and continue" — a
|
||||
startup misconfiguration must stop the service.**
|
||||
|
||||
- **Never log a token.** `client_secret` is a `SecretStr`; read it via
|
||||
`.get_secret_value()` only where you hand it to the Casdoor SDK. PAT plaintext
|
||||
is shown once at creation and only its SHA-256 hash is stored — never log the
|
||||
plaintext, a JWT, or a hash.
|
||||
Reference in New Issue
Block a user