feat(mupot-rbac-scoped-keys): scoped-API-key mint UI with role presets and guide#99
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
src/auth/role-presets.ts): three presets (sales-rep,admin,observer) each declaringrole,scopeType,scopeHint,allows[],denies[]. Module header has an honest enforcement-status note.src/dashboard/keys.ts,src/dashboard/index.ts):GET /admin/keysrenders a preset picker with a per-preset guide panel (allow/deny list + enforcement note);POST /admin/keys/mintisisAdmin-gated, validates scope_id against this pot's D1 (tenant-scoped), writes a capability grant (INSERT OR IGNORE— never downgrades), mints amember_tokenvia the shared service, shows the raw key exactly once (no redirect, no re-fetch, no log).role-presets.ts+ PR): the current system is rank-only. Scope + rank are real: a squad-scopedmembercannot touch org-admin routes. But per-surface granular caps (outreach:send-gated,mcpwp:write,budget:write) are policy documentation only today — no route callsrequireCapabilityfor those surfaces yet. Follow-up required: addrequireCapabilitygates on those surfaces to make the deny lists runtime enforcement.Security checklist
isAdminchecked before any DB write (both GET and POST routes)hono/csrfmiddlewarescope_idvalidated against this pot's D1 (no foreign-UUID injection)INSERT OR IGNOREon capability: never silently downgrades a higher-rank existing grantCache-Control: no-store+Referrer-Policy: no-referrerfrom existing dashboard middleware cover the show-once pageTest plan
npm test)npx tsc --noEmit)tests/dashboard-keys.test.ts— 44 test cases:findPreset/isValidPresetIdloadKeysView— 4 queries issued, correct SQL patternsmintScopedKey— 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 IGNOREnon-downgrade, label audit encodingHonest enforcement note
The blast-radius reduction Hadi asked for is partially real today:
membertoken cannot satisfyrequireOrgCapability('admin')checks.observercannot satisfy aleadcheck.requireCapabilitygate onoutreach:send,mcpwp:write,budget:write, orprovisionroute paths. Adding those gates is the follow-up that makes the deny lists runtime enforcement.🤖 Generated with Claude Code