diff --git a/README.md b/README.md index bd590e3..1f9be08 100644 --- a/README.md +++ b/README.md @@ -141,13 +141,18 @@ scopes never self-confer). ### campus.audit: API-key auth (#54) -The audit service (`/audit/v1`) is a separate, user-blind deployment -that does **not** accept OAuth tokens: it authenticates with its own -API keys (`audit_v1_…`) sent as Bearer credentials. `campus audit` is -the CLI's only command group using this second credential, stored in -the same credential store as your OAuth tokens and bound to the audit -URL it was stored for (per `ENV`/`CAMPUS_ENV` or `CAMPUS_AUDIT_URL`, -like every other endpoint): +The audit service is a separate, user-blind deployment with two auth +lanes. Its versioned API (`/audit/v1`) — everything `campus audit` +talks to — accepts only the service's own API keys (`audit_v1_…`) sent +as Bearer credentials, never OAuth tokens or campus accounts. The +service's web UI has its own, separate gate: campus accounts on the +server's `AUDIT_ADMINS` list sign in as **superadmins** (they may view +traces *and* manage keys in the browser), and in development +environments an audit API key can sign in too. None of that issues CLI +credentials: `campus audit` is the CLI's only command group using a +second credential, stored in the same credential store as your OAuth +tokens and bound to the audit URL it was stored for (per +`ENV`/`CAMPUS_ENV` or `CAMPUS_AUDIT_URL`, like every other endpoint): ```bash # Store a key: the CAMPUS_AUDIT_API_KEY env var, an interactive prompt @@ -176,10 +181,20 @@ command has one shape: list envelopes keep their `count` bodyless action such as `keys revoke` reports `{"id": …, "revoked": true}`. +The service gates everything through two capabilities, and a key earns +them scope by scope: `traces:read` for trace surfaces (the CLI's +`traces` commands, and the browser trace viewer via key sign-in), +`apikeys:write` for key management. A key with `apikeys:write` gains +no trace access, and a key with `traces:read` gains no key +management; `apikeys:read` alone is an API/CLI privilege that does not +admit a browser session. Web superadmins hold both capabilities by +being on the list. + Two roles use the group: -- **The operator** holds the key with `apikeys:*` scopes (the one - seeded from the server's `AUDIT_OPERATOR_API_KEY`) and administers +- **The operator** holds a key carrying `apikeys:read`/`apikeys:write` + — the deployment seeds one from the server's `AUDIT_OPERATOR_API_KEY` + env var (with every `traces:*` scope besides) — and administers everyone else's keys — mint, inspect, update, rotate, revoke: ```bash @@ -216,6 +231,12 @@ Two roles use the group: `campus audit keys revoke` (operator) or by letting `--expires-at` pass. + In a development environment the same `traces:read` key also signs + into the audit web UI's trace viewer, where the session holds + exactly the key's scopes; revoking or re-scoping the key ends the + browser session on its next request. That sign-in path does not + exist outside development. + ### Dry-run mode All `client`, `user`, `vault` and `audit` commands accept `--dry-run`. diff --git a/campus_cli/audit/__init__.py b/campus_cli/audit/__init__.py index d0194a3..00b089c 100644 --- a/campus_cli/audit/__init__.py +++ b/campus_cli/audit/__init__.py @@ -1,12 +1,14 @@ """campus.audit commands (#54). -The audit service (/audit/v1) is a separate, user-blind deployment -authenticating with its own API keys (`audit_v1_...`, Bearer) rather -than OAuth user tokens. The command group mirrors its two -capabilities: the operator's key lifecycle (campus audit keys) and -read-only trace queries (campus audit traces), plus login/logout and -the offline status view over the stored key — the CLI's only -non-OAuth credential. +The audit service is a separate, user-blind deployment. Its versioned +API (/audit/v1) authenticates with its own API keys (`audit_v1_...`, +Bearer) rather than OAuth user tokens; the service's web UI has a +separate browser gate (campus#929 superadmins, campus#916 +development-only key sign-in) that issues no CLI credentials. The +command group maps the API's scope model onto two capabilities: key +management (apikeys:write; campus audit keys) and trace reading +(traces:read; campus audit traces), plus login/logout and the offline +status view over the stored key — the CLI's only non-OAuth credential. """ import typer diff --git a/campus_cli/audit/client.py b/campus_cli/audit/client.py index 29803e7..575b981 100644 --- a/campus_cli/audit/client.py +++ b/campus_cli/audit/client.py @@ -1,11 +1,12 @@ """Shared plumbing for campus.audit commands (#54). -The audit service (/audit/v1) authenticates with one of its own API -keys (`audit_v1_...`) sent as a Bearer token — OAuth user tokens are -not accepted — so it gets its own credential slot and client here, -parallel to the OAuth machinery in campus_cli.auth. Like the OAuth -tokens, the stored key is bound to the service URL it was stored -against and rejected on a target mismatch. +The audit service's versioned API (/audit/v1) authenticates with one +of its own API keys (`audit_v1_...`) sent as a Bearer token — OAuth +user tokens are not accepted (the service's web UI has a separate +browser gate, campus#929/#916) — so it gets its own credential slot +and client here, parallel to the OAuth machinery in campus_cli.auth. +Like the OAuth tokens, the stored key is bound to the service URL it +was stored against and rejected on a target mismatch. """ import typer diff --git a/campus_cli/audit/keys.py b/campus_cli/audit/keys.py index 8e90324..2a07a77 100644 --- a/campus_cli/audit/keys.py +++ b/campus_cli/audit/keys.py @@ -3,11 +3,15 @@ The operator workflow for minting and managing audit API keys, e.g. one per student for dev-environment access: `owner_id` is a free-form label, conventionally the student's email (organizational only — -campus.audit is user-blind and self-contained by design), `scopes` -gate what the key may call (e.g. ["traces:read"]), and key management -itself requires apikeys:* scopes, held only by the operator key. -Plaintext key material is shown exactly once, at creation and at -regeneration (mirroring the server). +campus.audit is user-blind and self-contained by design), and +`scopes` gate what the key may call — student keys carry +["traces:read"]. Managing keys through this API requires apikeys:* +scopes: the deployment seeds one such operator key from its +AUDIT_OPERATOR_API_KEY env var, and minting does not validate scopes +— but apikeys:write is also the web UI's key-management capability +(campus#929), so hand one out deliberately. Plaintext key material is +shown exactly once, at creation and at regeneration (mirroring the +server). """ from datetime import datetime, timezone diff --git a/campus_cli/audit/login.py b/campus_cli/audit/login.py index 4969d26..4e41f6f 100644 --- a/campus_cli/audit/login.py +++ b/campus_cli/audit/login.py @@ -1,13 +1,15 @@ """campus.audit login/logout/status (#54, #62). -The audit service only accepts its own API keys, so 'campus audit -login' stores a key from --api-key, CAMPUS_AUDIT_API_KEY, or an -interactive prompt instead of running the OAuth device flow. The key -is verified against the service (best-effort, never blocking on -network errors) and bound to the audit URL it was stored for, like -OAuth tokens are bound to their auth endpoint. 'status' reports the -stored key's local state; the audit service has no "who am I" -endpoint, so it is a purely offline view. +The audit API (/audit/v1) accepts only the service's own API keys, so +'campus audit login' stores a key from --api-key, CAMPUS_AUDIT_API_KEY, +or an interactive prompt instead of running the OAuth device flow. +(The service's web UI has a separate browser gate — campus#929 +superadmins, campus#916 development-only key sign-in — which issues no +CLI credentials.) The key is verified against the service (best-effort, +never blocking on network errors) and bound to the audit URL it was +stored for, like OAuth tokens are bound to their auth endpoint. +'status' reports the stored key's local state; the audit service has +no "who am I" endpoint, so it is a purely offline view. """ import json diff --git a/campus_cli/utils/output.py b/campus_cli/utils/output.py index 991fb94..5107a10 100644 --- a/campus_cli/utils/output.py +++ b/campus_cli/utils/output.py @@ -24,9 +24,9 @@ "", ] -# Preamble for campus.audit --dry-run snippets (#54): the audit -# service authenticates with one of its own API keys, not an OAuth -# token, so its resource root is built directly. +# Preamble for campus.audit --dry-run snippets (#54): the audit API +# (/audit/v1) authenticates with one of its own API keys, not an +# OAuth token, so its resource root is built directly. AUDIT_PYTHON_API_PREAMBLE = [ "from campus_python import AuditRoot, CampusRequest", "",