Tenant Admin Console · Surface walkthrough

Roles and permissions

A per-surface walkthrough of the Tenant Admin Console tenant-admin surface: layout, states, interactions, data, and cross-references.

drafted
8sections3 minread

On this page

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 item data-workspace-id="roles"
  • Direct URL /roles (bookmarkable; middleware-gated)

Layout regions#

Inside TenantShell:

  • Header: TenantHeader shows tenant · <tenantId>"Roles and permissions"
  • Main: <section data-testid="tenant-roles-panel" data-tenant-id={tenantId}>
    • <h1>Roles and permissions</h1> + subtitle paragraph
    • .summaryGrid — four .summaryItem tiles: Risk score (diff.riskScore), Reviewer signoff ("Required" / "No"), Dry-run matches (matched/total), Assignment (approval.assignment.state or "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.entries is always non-empty
  • Populated (short) — capability rows for audit.read, members.write, policy.attest render; summary tiles reflect diffTemplateAgainstCanonical and runDryRun
  • 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)
  • GatedgetTenantServerSession() null → redirect to /unauthorized; sidebar Locked when canEnterTenantWorkspace is 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; current aria-current="page"; Locked when gated
  • Capability diff rows — non-interactive; capability id as code, canonical/tenant as allow/deny text, delta value

Data & contracts#

  • Reads: no BFF/network read. The page composes pure functions from @oshun/tenant-console on in-page fixtures:
    • diffTemplateAgainstCanonical({ canonicalRole: CanonicalRole, template: TenantRoleTemplate })diff.riskScore, diff.requiresReviewerSignoff, diff.entries
    • runDryRun({ effective, actions }) → array; Dry-run matches = filter(r.matched).length / length
    • transitionAssignment({ assignment: RoleAssignment, to: 'approved', actorId, nowUnixSeconds })approval.ok / approval.assignment.state
    • actorId is the signed-in session.payload.userId; NOW = Date.UTC(2026, 4, 11, 15) (seconds)
  • Writes: None. (transitionAssignment is 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#

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 primaryJob describing signoff and assignment management, no approve/deny/assign controls exist (read-only). Verify where the interactive flow is intended to live.
  • transitionAssignment runs at render with a fixed 'approved' target; the surfaced Assignment tile is illustrative, not a persisted decision.