Skip to content

feat(mupot-rbac-scoped-keys): scoped-API-key mint UI with role presets and guide#99

Merged
servathadi merged 1 commit into
mainfrom
feat/mupot-rbac-scoped-keys
Jun 11, 2026
Merged

feat(mupot-rbac-scoped-keys): scoped-API-key mint UI with role presets and guide#99
servathadi merged 1 commit into
mainfrom
feat/mupot-rbac-scoped-keys

Conversation

@servathadi

Copy link
Copy Markdown
Contributor

Summary

  • Deliverable 1 — Role presets (src/auth/role-presets.ts): three presets (sales-rep, admin, observer) each declaring role, scopeType, scopeHint, allows[], denies[]. Module header has an honest enforcement-status note.
  • Deliverable 2+3 — Mint flow + guide (src/dashboard/keys.ts, src/dashboard/index.ts): GET /admin/keys renders a preset picker with a per-preset guide panel (allow/deny list + enforcement note); POST /admin/keys/mint is isAdmin-gated, validates scope_id against this pot's D1 (tenant-scoped), writes a capability grant (INSERT OR IGNORE — never downgrades), mints a member_token via the shared service, shows the raw key exactly once (no redirect, no re-fetch, no log).
  • Deliverable 4 — Enforcement check (documented in role-presets.ts + PR): the current system is rank-only. Scope + rank are real: a squad-scoped member cannot touch org-admin routes. But per-surface granular caps (outreach:send-gated, mcpwp:write, budget:write) are policy documentation only today — no route calls requireCapability for those surfaces yet. Follow-up required: add requireCapability gates on those surfaces to make the deny lists runtime enforcement.

Security checklist

  • isAdmin checked before any DB write (both GET and POST routes)
  • CSRF covered by existing dashboard-wide hono/csrf middleware
  • Raw token returned once, never persisted or logged
  • scope_id validated against this pot's D1 (no foreign-UUID injection)
  • INSERT OR IGNORE on capability: never silently downgrades a higher-rank existing grant
  • Cache-Control: no-store + Referrer-Policy: no-referrer from existing dashboard middleware cover the show-once page

Test plan

  • 47 test files / 805 tests pass (npm test)
  • Typecheck clean (npx tsc --noEmit)
  • New tests/dashboard-keys.test.ts — 44 test cases:
    • Preset shape (sales-rep / admin / observer) — allows/denies content, rank, scopeHint
    • findPreset / isValidPresetId
    • loadKeysView — 4 queries issued, correct SQL patterns
    • mintScopedKey — happy path (sales-rep, admin, observer), all guard paths (unknown preset, member not found, missing scope_id, foreign squad UUID), show-once raw format, INSERT OR IGNORE non-downgrade, label audit encoding
    • Enforcement-status documentation tests (assert the deny-list contains the documented pending-follow-up surfaces)

Honest enforcement note

The blast-radius reduction Hadi asked for is partially real today:

  • Scope is real: a squad-scoped member token cannot satisfy requireOrgCapability('admin') checks.
  • Rank is real: an observer cannot satisfy a lead check.
  • NOT real yet: there is no requireCapability gate on outreach:send, mcpwp:write, budget:write, or provision route paths. Adding those gates is the follow-up that makes the deny lists runtime enforcement.

🤖 Generated with Claude Code

…s and guide

Deliverable 1 — src/auth/role-presets.ts
  Three role presets (sales-rep/admin/observer) each with allow/deny lists,
  scopeType+scopeHint, and an honest enforcement note in the module header.
  sales-rep: role=member, scope=squad, allows leads/outreach/pipeline read,
  denies admin/cross-tenant/mcpwp:write/budget:write. Admin: role=admin,
  scope=org, denies owner. Observer: role=observer, scope=squad, read-only.

Deliverable 2+3 — src/dashboard/keys.ts + src/dashboard/index.ts
  GET /admin/keys — preset picker + per-preset guide panel (allows/denies +
  enforcement note) + active scoped-keys table with revoke. POST /admin/keys/mint
  — isAdmin-gated, validates preset+member+scope against this pot's D1 (tenant-
  scoped, no foreign-UUID injection), writes capability grant (INSERT OR IGNORE
  to avoid downgrading higher-rank grants), mints member_token via shared
  mintMemberToken service, renders raw key ONCE (no redirect, no log, no re-fetch).
  Nav link added. CSRF + no-store + no-referrer from existing dashboard middleware.

Deliverable 4 — enforcement status documented (PR note)
  Current gates are rank-only (5-level ladder). The allow/deny lists are
  enforced at the scope+rank level (e.g. squad-scoped member cannot touch
  org-admin routes). Per-surface grants (outreach:send-gated, mcpwp:write,
  budget:write) are POLICY DOCUMENTATION only today. Follow-up required:
  add requireCapability gates on those surfaces to make the deny lists real
  runtime enforcement.

Tests — tests/dashboard-keys.test.ts
  47 files / 805 tests passing. New file: 44 test cases covering preset
  correctness, loadKeysView query shape, mintScopedKey happy + all guard
  paths (unknown_preset, member_not_found, scope_id_required, squad_not_found,
  INSERT OR IGNORE non-downgrade), show-once raw token shape, label audit
  encoding, and enforcement-status documentation tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant