Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 30 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`.
Expand Down
16 changes: 9 additions & 7 deletions campus_cli/audit/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
13 changes: 7 additions & 6 deletions campus_cli/audit/client.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 9 additions & 5 deletions campus_cli/audit/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 10 additions & 8 deletions campus_cli/audit/login.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 3 additions & 3 deletions campus_cli/utils/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"",
Expand Down
Loading