Scrubbed extract of
ADP-Orchestration-UI/Epic-1-Stories/US-OUI-1.6-Permission-Scoping-Enforcement.md— cited by the Chronicle (03-the-platform-rises). Redactions: phone / service detail / secrets / clearance / personal / internal hosts.
type: implementation-plan category: Work project: ADP-Orchestration-UI priority: High status: done created: 2026-05-14 epic: Epic-1-Foundation user-story: US-OUI-1.6 effort: 8h precedence:
- “[[Projects/ADP-Orchestration-UI/Epic-1-Stories/US-OUI-1.2-Design-System]]”
- “[[US-OUI-1.4-API-Client]]”
- “[[Projects/ADP-Orchestration-UI/Epic-1-Stories/US-OUI-1.5b-Login-Authentication-Page]]” parallel_group: review-profile: standard source: wireframe-gap-analysis-2026-05 depends-on:
- ADR-013
- ADR-018 tags:
- implementation-plan
- epic-1
- permissions
- rbac
- cross-cutting
US-OUI-1.6: Permission Scoping Enforcement (role-aware rendering primitives)
Epic: [[README|Epic 1: Project Foundation & Design System]] Owner Agent: \xe2\x80\x94 Effort: 8h Priority: 🔼 High Status: 🔵 Backlog
Mission
Build the cross-cutting role-aware rendering primitives that every downstream OUI epic will consume to gate UI surfaces by the authenticated user’s resolved roles. Provide a PermissionContext, a <RoleGate> component, a useHasRole() / useHasSurface() hook pair, and a typed Surface → Role[] map derived from [[../../AI-Dev-Platform/ADR/ADR-013-Permission-And-Role-Model|ADR-013]] §5. Do not invent new role names or extend the platform role set; consume the five platform roles plus the adp-sensitive-viewer modifier as defined in ADR-013. Claims are sourced from the Keycloak access token issued per [[../../AI-Dev-Platform/ADR/ADR-018-Identity-Federation-Strategy|ADR-018]] — do not call a separate authorization endpoint.
Context
The Wireframe Gap Analysis §6 names role/scope-aware view rendering as a missing capability: regular users see their own sessions, operators see fleet view, admins see provider config and the policy editor. ADR-013 defines the two-axis platform×project role model, five platform roles, and a surface→role table; ADR-018 defines the Keycloak claim shape and composite-role expansion. This story produces the React primitives so that subsequent stories in Epic 2 (Spawn Flow, Platform Services panel), Epic 3 (Approval Policy editor, Modify flow), Epic 5 (Provider config), and Epic 6 (admin surfaces, audit feeds) all gate consistently with one mechanism. The non-elevating read-only impersonation flow from ADR-013 §7 is wired here so impersonation context decorates the same primitives — no parallel auth path.
Context Bootstrap
Read the following before writing any code, in order:
- [[../../AI-Dev-Platform/ADR/ADR-017-Human-Review-Branching-And-TDD-Workflow|ADR-017]] — review profile (
standard), four-checkpoint workflow, TDD enforcement, PR description schema - [[../../AI-Dev-Platform/ADR/ADR-013-Permission-And-Role-Model|ADR-013]] — §2 (five platform roles +
adp-sensitive-viewer), §5 (surface→role table), §7 (read-only impersonation rules) - [[../../AI-Dev-Platform/ADR/ADR-018-Identity-Federation-Strategy|ADR-018]] — Keycloak claim shape, composite-role expansion, token format
- [[../Wireframe-Gap-Analysis|Wireframe Gap Analysis]] §6 — capability rows for “Permission scoping for unified dashboard” and “Account-switcher / impersonation”
- [[../Architecture|OUI Architecture]] — Keycloak auth-broker mermaid sequence
- [[Projects/ADP-Orchestration-UI/Epic-1-Stories/US-OUI-1.5b-Login-Authentication-Page]] — token storage, refresh flow, channel selector
- [[../_pkf|OUI _pkf]] — current project state, branch protection
Gate Policy
| Trigger | Gate Type | Condition / Reasoning |
|---|---|---|
| New role name introduced outside the ADR-013 §2 enumeration | Trust Gate | Role model is ADR-governed; surface for ADR amendment |
Surface added to Surface → Role[] map without a row in ADR-013 §5 |
Trust Gate | Surface mapping is part of the role model contract |
| Impersonation flow grants any write capability | Trust Gate | ADR-013 §7 mandates read-only, non-elevating |
| Token claim parse fails or contains an unrecognized claim | Confidence Gate | Surface to user with a clear error rather than silently treating user as anonymous |
| Test failure during Verify & Refactor | Trust Gate | Do not mark complete with failing tests |
Change touches Projects/AI-Dev-Platform/ADR/ |
Trust Gate | Forces rigorous profile; ADR work is rigorous per ADR-017 |
Deliverables
src/auth/PermissionContext.tsx (new)
-
PermissionContextReact context exposing{ roles: PlatformRole[]; projectRoles: ProjectRoleEntry[]; sensitiveViewer: boolean; impersonating: ImpersonationState | null } -
PermissionProviderderives the value from the JWT in the auth store; re-derives on token refresh -
PlatformRoleandProjectRoleEntrytypes exported and match ADR-013 §2 exactly (no extra members)
src/auth/roles.ts (new)
-
PLATFORM_ROLESliteral union:adp-developer | adp-operator | adp-admin | adp-auditor(+adp-sensitive-vieweras additive modifier) — corrected 2026-07-03 (W9): the earlier bullet listedadp-viewer/adp-platform-owner, roles that do not exist in ADR-013 §2; the original implementer followed the stale bullet and shipped an inventedadp-platform-ownerrole (fixed in OUI PR #38). ADR-013 §2 is authoritative, per this story’s own Mission text. -
SENSITIVE_VIEWER_MODIFIERconstantadp-sensitive-viewer -
SURFACE_ROLE_MAPtypedRecord<SurfaceId, PlatformRole[]>mirroring ADR-013 §5 row-for-row -
expandCompositeRoles(claims)resolves Keycloak composite expansion per ADR-018
src/auth/RoleGate.tsx (new)
-
<RoleGate require="adp-operator">…children…</RoleGate>renders only if user holds the required role -
requireaccepts a single role, an array (OR), or{ allOf: [...] }(AND) -
fallbackprop renders an explicit denied state; defaults to rendering nothing - Honors impersonation: if
impersonating != null, only read capabilities resolve true
src/auth/useHasRole.ts and src/auth/useHasSurface.ts (new)
-
useHasRole(role)returns boolean fromPermissionContext -
useHasSurface(surfaceId)looks upSURFACE_ROLE_MAPand resolves against current roles - Both hooks are pure derivations of context; no network calls
src/auth/Impersonation.tsx (new)
-
useImpersonation()exposesstart(targetUserId)andstop(); populatesimpersonatingin context - Imposed read-only constraint: any write-capable surface returns
falsefromuseHasSurfacewhenimpersonating != null - Impersonation start/stop emits an audit log event via the API client (
POST /api/audit/impersonation) per ADR-013 §7
src/auth/__tests__/permission-context.test.tsx (new)
- Renders
<RoleGate>with various role permutations including composite expansion - Asserts surface→role map matches ADR-013 §5 every row (snapshot pinned to ADR section)
- Asserts impersonation forces read-only resolution
- Asserts unknown claim raises a Confidence Gate marker through the error boundary, not silent allow
Execution Phases
TDD-ordered per [[../../AI-Dev-Platform/ADR/ADR-017-Human-Review-Branching-And-TDD-Workflow|ADR-017]] §4. Plan / Test / PR review checkpoints fire between phases per review-profile: standard.
- Branch Creation — Create
agent/US-OUI-1.6-permission-scopingfrommain. No commits to protected branches; AIS fires anIRREVERSIBLETrust Gate on violation (ADR-017 §3). - Load Context — Read all Context Bootstrap files. Confirm US-OUI-1.5a token storage shape is finalized. (Plan Review checkpoint fires.)
- Write Failing Tests — Author the four test groups in
permission-context.test.tsx. Runpnpm test --run; every test must fail before implementation. Failing transitions logged via ADR-017 §4TestTransition. (Test Review checkpoint fires.) - Implement — Add
roles.ts,PermissionContext.tsx,RoleGate.tsx, the two hooks, andImpersonation.tsx. Re-run tests; all pass. - Verify & Refactor —
pnpm build,pnpm test --run,pnpm lint,pnpm typecheckall green. Update status toreview. - PR Submission — Push branch; open one PR to
mainwith the structured PR description below. (PR Review checkpoint fires; merge is human-only.)
PR Submission Requirements
PR description schema enforced at PR-open time per ADR-017 §5 (pr-description-schema-version: 1.0). Required: Reasoning Chain, Gate History (with gateOrigin: review/<phase>), Citations, Review Profile (standard, source: project default), Test Plan with TestTransition rows.
Implementation Notes
Surface ID conventions
SurfaceId is a typed string literal union covering every gated surface: 'sessions.fleet' | 'sessions.spawn' | 'gates.policy.edit' | 'gates.policy.view' | 'routing.providers.edit' | 'platform-services.mcp' | 'audit.sensitivity' | 'audit.impersonation' | 'admin.config'. Add IDs only when a downstream story needs one and there is an ADR-013 §5 row to back it.
Claim source
Roles come from the JWT’s realm_access.roles and resource_access.<client>.roles claims per ADR-018. Composite expansion is performed client-side using the ADR-018 §Claim mapping section’s documented composite list — do not call out to Keycloak at render time.
Files to Modify
src/auth/*— new files abovesrc/App.tsx— wrap children in<PermissionProvider>src/api/client.ts— add the audit-impersonation endpoint call
Definition of Done
- All Deliverables checked
- All Execution Phases verified
- No new dependencies introduced
- Tests pass:
pnpm test --run src/auth - ADR-013 §5 surface map verified row-for-row in test snapshot
- Status updated to
review
Related
- [[../../AI-Dev-Platform/ADR/ADR-013-Permission-And-Role-Model|ADR-013]] — role model consumed
- [[../../AI-Dev-Platform/ADR/ADR-018-Identity-Federation-Strategy|ADR-018]] — claim source
- [[../../AI-Dev-Platform/ADR/ADR-017-Human-Review-Branching-And-TDD-Workflow|ADR-017]] — review workflow
- [[US-OUI-1.7-Sensitivity-Classification-UI]] — sibling design-system primitive
- [[../Wireframe-Gap-Analysis|Wireframe Gap Analysis]] §6
- [[../_pkf|OUI _pkf]]