API keys Settings UI and docs

Task 9372 · Running · Coder · created · updated

Part of epic 9326 (Jaah API keys). Child C4 of the Architect's decomposition: console Settings → API keys (list, detail/create, permission grid, secret reveal, rotate/revoke), Audit actor rendering, and reader docs. Depends on the backend child (the §5 management API below). May start against the contract with mocked API in unit tests. ## Original requirement (verbatim) > Implement Jaah API keys workflow. > - Permissions: same as we have internally: subject:action. Access to object is limited to the owner of the key. Should be very easy to configure on the UI. By default all is max, but user can remove some > - API Keys can only be created by at least Org Admin > - Keys have expiration > Do a web research and suggest the most modern approach to this for this type of systems ## Requirements this child delivers (PRD, verbatim) - **R2 Creation gate.** Only a signed-in Org Owner or Org Admin can create a key. Nobody else sees the create control or can call the create endpoint (403). - **R5 Configuration UI = one grid**: rows = subjects (feature labels as the console already renders them), columns = View/Create/Edit/Delete, plus the "Org administration" row. **Every box ticked by default**; user unticks to remove. One-click presets: "All", "Read-only" (view column only), "None". Must reuse the existing grid/matrix look used elsewhere in the console (ui-style-guide). - **R6 Expiration.** Creation offers: 7, 30, 90 days, 1 year, custom date, **No expiration**. **Default 1 year.** An expired key is refused (401) and shown as "Expired". - **R8** Secret shown once with a copy button and a "you won't see this again" notice; list shows a non-secret identifier (prefix + last 4). - **R9 Lifecycle actions.** Edit in place: rename, change description, tick/untick permissions, change IP allowlist. Rotate: owner picks old-secret overlap **None / 1h / 24h / 7d, default 24h**. Revoke: immediate, irreversible; key shown as "Revoked". Expiry cannot be extended. - **R10 Visibility & control.** Every Org Owner and Org Admin sees all org keys (name, owner, permissions, created, expires, last used, status — never the secret) and can revoke any. Edit/rotate is owner-only. - **R11 UI location.** Top-level **Settings → "API keys"** area (`/settings/api-keys`), visible only to Org Owner/Admin. List → detail per the console UI style guide; states: empty, active, expiring soon (≤7 days), expired, revoked, owner-suspended. - **R12** Each key shows last-used time and source IP; requests made with a key are attributed in the audit log as "<owner> via API key <name>". - **R15 Availability.** Feature ON by default — no flag — on the dev control plane after merge, reachable by any Org Owner/Admin, and documented in the reader docs portal (`apps/docs`, API reference: how to authenticate with a key). ## Approach (Architecture W1–W3, D1, §9, §10 C4) - **W1 list**: `apps/web/src/pages/Settings.tsx` `PANELS` entry `apiKeys` (`fixed`, `own`), `apps/web/src/pages/settings/apiKeys/ApiKeysSection.tsx`. `ui/feed/FeedList` row per key: name, owner, display id, permissions summary ("All", "Read-only", "N of M"), expires ("Never" / "Expires in N days"), last used (+IP), status chip. Row opens detail. Empty state names what is missing + the Create action. - **W2 detail/create**: `apps/web/src/pages/settings/apiKeys/ApiKeyDetailPage.tsx`, routes `/settings/api-keys/new` and `/settings/api-keys/:keyId` in `App.tsx`, inside the `RequireFeature permission="system:view"` wrapper (`:549-552`) and before `/settings/:area`. `ui/detail/DetailPage` + `components/Tabs` + `useUrlTab`. Tabs: Overview (name, description, expiry — selectable on create with 7/30/90 days/1 year (default)/custom date+time/No expiration, read-only after), IP allowlist; Permissions (grid); Activity (`/api/audit?api_key=<id>`). One Save bar. Create = detail page in create mode (style guide over the PRD's "create dialog"). **Secret reveal is a one-shot dialog** (full secret, working copy button, "you won't see this again" notice). Rotate (overlap select, default 24h) and Revoke are confirm dialogs. Owner-only: Edit, Rotate. Non-owner admin: Revoke only. Operator acting-as a tenant: list + Revoke, no Create/Edit/Rotate; operator plane not acting: no area. - **W3 grid** `apps/web/src/pages/settings/apiKeys/KeyPermissionGrid.tsx`: built from `GET /api/permissions` (`feature_label`, `honoured` — omit unhonoured), never from `types.ts` mirrors. Rows = features, columns View/Create/Edit/Delete, plus "Org administration" row. Presets All/Read-only/None on top. All ticked by default; all ticked (or All) saves `grants_all=true`, any untick saves an explicit set. At 390 px it stacks into one card per feature with four labelled checkboxes — never a horizontally scrolled table; labels wrap, never truncated. Reuse the styling of `apps/web/src/pages/settings/jarvisTools/PermissionGrid.tsx` if its shape fits; otherwise lift its cell styles into `ui/` rather than copying them (two-copies rule). - **Audit actor**: the Audit list renders `<owner label> via API key <name>` whenever `data.api_key_name` is present — composed at render time, never stored. - **D1 docs**: `apps/docs/content/administration/security-and-audit/api-keys.md` + `.shots.json` (create, permissions, expiry, rotate, revoke) and `apps/docs/content/jaah-api/authentication.md` (`Authorization: Bearer`, error codes `invalid_api_key`/`ip_not_allowed`/`api_key_not_allowed`, refused surfaces: account security, key management, identity minting, streams/WebSockets), linked from `apps/docs/content/jaah-api/index.md`; `tools/docs_coverage.py` green. - Copy follows `docs/standards/ui-copy-standards.md` (short labels; the reveal warning and revoke confirmation keep what they must name). Management API contract (from the backend child): `GET/POST /api/api-keys`, `GET/PATCH /api/api-keys/{id}`, `POST /api/api-keys/{id}/rotate {overlap: "none"|"1h"|"24h"|"7d"}` → `{key, secret}`, `POST /api/api-keys/{id}/revoke`. `ApiKeyOut = {id, name, description, owner:{id,label}, display, grants_all, permissions[], org_admin, ip_allowlist[], expires_at|null, created_at, created_by, updated_at, last_used_at|null, last_used_ip|null, status: active|expiring|expired|revoked|suspended, revoked_at|null, revoked_by|null, revoke_reason|null, previous_secret_valid_until|null, can_edit, can_revoke}`. Create body `{name, description, grants_all, permissions, org_admin, ip_allowlist, expires_at|null}` → `201 {key, secret}`. Tests: web unit tests (grid presets/defaults/grants_all serialisation, stacked layout at 390 px, status chips, owner vs non-owner actions, reveal dialog), an e2e per `apps/web/e2e/README.md` budget for create → reveal → list. Credentials & reach: - AC1, AC3, AC9, AC11, AC13, AC17 on dev | `me/jaah/debug/main/operator-login` on `https://app.dev.jaah.ai` (control plane `dev.jaah.ai`), signed in to a tenant as Owner or acting as that tenant's seat | **verified**: `credential_get` succeeded; login probe answered 200 (role owner). - AC4 (grid at 1280 px and 390 px) | operator-login on `app.dev.jaah.ai` + the Playwright browser on the node (viewport resize) | **verified**. - AC2, AC12 (non-admin member, second admin who revokes) | Throwaway members created by the verifier with operator-login (D-A4): create the member on the tenant's Users page, set the password through the member edit (`PATCH /api/users/{id}`, driven from the console as the operator), sign in as that member in a second Playwright context and enrol TOTP when prompted; remove the members afterwards | **verified** for the creating identity. - AC16 docs coverage | `tools/docs_coverage.py` in the worktree | **verified**. Out of scope: Do NOT change backend auth/routes (backend child) beyond what the contract above exposes; do NOT build the reminder sweep or MentionsBell row (sweep child). No custom-role editor and no change to built-in roles or the `feature:action` catalog. No per-project narrowing UI. No org-level max-lifetime policy, approval workflow, per-key rate limits. No OAuth client-credentials. No re-enabling of public OpenAPI. No change to cookie-session auth behaviour for existing console users. Done means: - AC1 Org Admin sees Settings → API keys and can create a key — Sign in as Org Admin → open `/settings/api-keys` → "Create key" → name it → keep defaults → Create. Observe: full secret shown once with copy button (clicking it puts the exact secret on the clipboard) and warning; list row appears with name, owner = you, expires = today+1y, status Active. - AC2 Non-admins cannot create or see the page — Sign in as a project_developer member → Settings shows no "API keys" area; `/settings/api-keys` shows no create control; `POST` to the create endpoint with that session → 403. - AC3 Default permissions = all — In the create page observe every View/Create/Edit/Delete box and "Org administration" ticked before any click. - AC4 Grid is easy and readable — Create page at 1280 px and at 390 px phone width: every subject label fully readable (no truncation to a few chars), no horizontal page scroll, presets All/Read-only/None each set the grid in one click. Matches existing console grid styling. At 390 px the grid is the stacked per-feature card layout, not a squeezed table. - AC9 Expiry (UI) — Create a key with custom expiry of tomorrow → list shows "Expires in 1 day" / expiring-soon state; after expiry (dev: a key expiring in minutes) → call → 401, row shows Expired. "No expiration" option exists and its row shows "Never". - AC10 (UI) — After closing the reveal dialog no UI shows the secret (list/detail show prefix+last 4 only). - AC11 Rotate with overlap (UI) — Rotate, pick 1h → new secret shown once; old secret works (200) until overlap ends, then 401; pick None → old secret 401 immediately. Default selection shown = 24h. - AC12 Revoke (UI) — Owner revokes → next call 401, row Revoked. A second Org Admin (not owner) opens the page → sees the key with its owner → can Revoke, has no Edit/Rotate. - AC13 Last used + audit (UI) — After a key call the row shows last-used time and IP. `/audit` shows events: created, edited, rotated, revoked, refused (with reason), and API requests attributed "<owner> via API key <name>". No secret in any audit payload. The detail Activity tab lists that key's events. - AC16 Docs — Reader docs portal has an "API keys" page (create, permissions, expiry, rotation, `Authorization: Bearer` usage); `apps/docs` route-coverage check (`tools/docs_coverage.py`) green. - AC17 On by default — All above observed on dev after merge with no setting or flag toggled. (The reminder half of the epic is verified at epic finish once the sweep child has also merged.) - Web unit tests + e2e green; `./dc check quick` green.

Outcome

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started