Context. surface tenant-admin · domain identity-access · route /roles · auth signed-in + tenant:admin (getTenantServerSession) · source apps/oshun/tenant-admin/src/app/roles/page.tsx
Last walked. —
Purpose#
Read-only governance view of tenant role customization: shows a tenant role
template diffed against its canonical base (risk score, reviewer-signoff
requirement), dry-run authorization results, and the state of a sample role
assignment after an approval transition. Backs the roles workspace
(governance group) in libs/oshun/navigation/src/tenant-ia.ts.
Entry points#
- Shell sidebar (
TenantSidebar) — "Roles" (shortLabel) under the Governance group, link to/roles(data-workspace-id="roles") - Workspace grid on
/(home) — list itemdata-workspace-id="roles" - Direct URL
/roles(bookmarkable; middleware-gated)
Layout regions#
Inside TenantShell:
- Header:
TenantHeadershowstenant · <tenantId>› "Roles and permissions" - Main:
<section data-testid="tenant-roles-panel" data-tenant-id={tenantId}><h1>Roles and permissions</h1>+ subtitle paragraph.summaryGrid— four.summaryItemtiles: Risk score (diff.riskScore), Reviewer signoff ("Required" / "No"), Dry-run matches (matched/total), Assignment (approval.assignment.stateor "blocked").table— capability diff table: columns Capability (code), Canonical ("allow"/"deny"), Tenant ("allow"/"deny"), Delta
States#
- Loading — N/A; server component computes the diff/dry-run in one pass
- Empty — N/A; canonical role defines 3 capabilities so
diff.entriesis always non-empty - Populated (short) — capability rows for
audit.read,members.write,policy.attestrender; summary tiles reflectdiffTemplateAgainstCanonicalandrunDryRun - Populated (long) — N/A; fixture capability set is fixed
- Error (recoverable) — N/A; no runtime query
- Error (unrecoverable) — no session →
redirect('/unauthorized?reason=missing-session&returnTo=/roles') - Offline — N/A for data (SSR-rendered)
- Gated —
getTenantServerSession()null → redirect to/unauthorized; sidebar Locked whencanEnterTenantWorkspaceis false - Standalone PWA — verify; no manifest/SW confirmed in source
Interactions#
The <main> content is a static report — no buttons, inputs, approve/deny
controls, or template editor. All interactivity is shell chrome:
- Skip to main content (link,
TenantShell) → anchors#tenant-main - Sidebar workspace links (links,
TenantSidebar) → navigate; currentaria-current="page"; Locked when gated - Capability diff rows — non-interactive; capability id as
code, canonical/tenant as allow/deny text,deltavalue
Data & contracts#
- Reads: no BFF/network read. The page composes pure functions from
@oshun/tenant-consoleon in-page fixtures:diffTemplateAgainstCanonical({ canonicalRole: CanonicalRole, template: TenantRoleTemplate })→diff.riskScore,diff.requiresReviewerSignoff,diff.entriesrunDryRun({ effective, actions })→ array; Dry-run matches =filter(r.matched).length / lengthtransitionAssignment({ assignment: RoleAssignment, to: 'approved', actorId, nowUnixSeconds })→approval.ok/approval.assignment.stateactorIdis the signed-insession.payload.userId;NOW = Date.UTC(2026, 4, 11, 15)(seconds)
- Writes: None. (
transitionAssignmentis a pure computation over a fixture, not a persisted write) - Realtime: None.
- Caching: SSR (server component); deterministic per
tenantId/userId - Auth/role check:
getTenantServerSession()in page; middleware gate
Contracts: CanonicalRole, RoleAssignment, TenantRoleTemplate from
@oshun/tenant-console (libs/oshun/tenant-console).
Cross-references#
- Workspace definition:
libs/oshun/navigation/src/tenant-ia.ts(roles— groupgovernance,requiredScopes: ['tenant:admin:*'],iconToken: 'badge-check') - Shell:
TenantShell.tsx,TenantSidebar.tsx,TenantHeader.tsx - Sibling routes: members.md (members hold role assignments), policy.md, integrations.md, notifications.md, status.md
- Conventions: ../WALKTHROUGH/00-conventions.md
Open questions / known gaps#
- No live roles BFF route is wired — page renders a static canonical/template diff fixture, not the tenant's real role catalog.
- Despite the workspace
primaryJobdescribing signoff and assignment management, no approve/deny/assign controls exist (read-only). Verify where the interactive flow is intended to live. -
transitionAssignmentruns at render with a fixed'approved'target; the surfaced Assignment tile is illustrative, not a persisted decision.