Context. surface operator · domain operator (tenant — SSO/SAML/OIDC config) · route /operator/sso · auth signed-in · source apps/oshun/web/src/app/operator/sso/page.tsx
Last walked. 2026-06-25 operator SSO roster row-cell addendum — direct signed-in /operator/sso render, generatedAt anchor, live roster/masthead count attributes, seeded SAML/OIDC card metadata attributes, visible tenant/protocol/IdP/sync field cells, footer targets, anonymous redirect-before-render, editor create/import/edit/delete/test legs, and real BFF read/write/import/probe contracts verified in tenant-sso-config.spec.ts. Evidence: WALKTHROUGH/results/v1-real-infra-run-2026-06-22.md §91.
Purpose#
The tenant federation roster: one card per tenant SSO connection from
GET /v1/sso, showing protocol (SAML/OIDC), status, the IdP metadata pointer,
and last sync time. The roster is followed by the shipped OperatorSsoEditor,
which manages SSO connections through the live admin SSO BFF: create, import
metadata/discovery, edit claim mappings, delete, and run the connection test.
Entry points#
- From
/operator(hub) — operator-surface card; foot "← Operator hub" returns there - Cross-links from siblings —
/operator/auditfoot row links here;/operator/tenantslinks here; this page links onward to/operator/tenants/operator/lms, and/operator/audit - From tenant onboarding — after onboarding a tenant, configuring SSO surfaces here (the empty state says "Onboard a tenant to start")
- Direct URL — session-gated only (
proxy.tsredirects sessionless →/welcome?redirect=); no operator-role gate in the proxy - Bookmark —
canonical = '/operator/sso'
Layout regions#
page.tsx is an async server component: await bffGet<SsoResponse>('/v1/sso')
(falling back to { connections: [] }), rendered inside
<LWebShell top={<LCustomerNav active="library" />}> (reuses the customer
nav, Library active). Centered column maxWidth: 1080.
- Header:
LCustomerNav(Library highlighted — verify intended chrome) - Page anchor: outer content column exposes
data-operator-sso-pageanddata-operator-sso-generated-at - Masthead:
LMastheadwrapped bydata-operator-sso-mastheadwithdata-operator-sso-connection-count; visible copy is left "Operator · SSO", right${data.connections.length} connections, kicker "Tenant federation", title "The SSO roster." - Roster:
<div data-operator-sso-roster data-operator-sso-roster-count>with a grid of connection cards OR the empty card - Connection card (per
row,<div data-operator-sso-card>): accent border whenstatus === 'active'; stable attributes exposedata-operator-sso-id,data-operator-sso-tenant-id,data-operator-sso-tenant-label,data-operator-sso-protocol,data-operator-sso-status,data-operator-sso-idp-metadata-url, anddata-operator-sso-last-sync-iso- Top line: "Tenant · {tenantId}" eyebrow +
tenantLabel(italic when active), right-aligned "{PROTOCOL} · {status}" (accent when active) - "IdP metadata · {idpMetadataUrl}" (mono,
word-break: break-all) - "Last sync · {localized lastSyncIso | 'never'}" (serif italic)
- Visible field cells expose
data-operator-sso-field="tenant-eyebrow" | "tenant-label" | "protocol-status" | "idp-metadata" | "last-sync"
- Top line: "Tenant · {tenantId}" eyebrow +
- Editor:
<OperatorSsoEditor data-sso-editor>— lists live admin SSO connections, exposes Test / Edit / Delete on each row, renders an Import metadata form for SAML XML or OIDC discovery, and renders a New/Edit connection form for tenant id, protocol, IdP entity/issuer, thumbprint, endpoints, claim mappings, flow toggles, JIT, and session/refresh lifetimes - Footer action row:
LBtnghost "← Operator hub" (/operator),LBtnghost "Tenants" (/operator/tenants),LBtnghost "LMS" (/operator/lms),LBtnprimary "Audit log" (/operator/audit)
States#
- Loading — source-verified 2026-06-25: no route-level
loading.tsx; server-sideawait bffGetblocks the roster render, so there is no skeleton state. The client editor separately renders[data-sso-loading]while its admin list request is pending - Anonymous user — Playwright 2026-06-25: sessionless
/operator/ssoredirects to/welcome?...before[data-operator-sso-page]or[data-operator-sso-roster]render - Signed-in user — Playwright 2026-06-25: primed signed-in session plus operator admin bearer renders the page, generatedAt anchor, masthead/ roster count attributes, live roster cards, footer, and editor. Still session-gated only; no operator-role gate in the proxy or in-page guard
- Empty — unit-covered:
src/app/operator/sso/__tests__/page.test.tsxrendersdata.connections.length === 0and asserts "No SSO connections configured. Onboard a tenant to start." plus "0 connections" - Populated — Playwright: real admin BFF seeds live SAML/OIDC
connections and
/operator/ssorenders one card per connection; active cards expose accent status, stable protocol/status/id/tenant/metadata/sync attributes, and visible field cells - Protocol diversity — Playwright asserts both "SAML · active" and "OIDC · active" from live seeded connections
- Status diversity — active is browser-covered; pending / disabled / error remain BFF-envelope/source-visible only and need dedicated seeded browser cases if the store can create those states
-
lastSyncIsonull — Playwright asserts "Last sync · never" - BFF unreachable / non-2xx — unit-covered:
src/app/operator/sso/__tests__/page.test.tsxmocksbffGetreturningnull; the page substitutes{ connections: [] }and shows the empty state (no fabricated connections). Dedicated outage-route E2E is not forceable throughpage.routebecause this is a server component - Offline — RSC fetch (
revalidate 60); operator SW policy TBD - Standalone PWA — renders; verify robots/no-index (no
robotsexport here)
Interactions#
Masthead#
- Masthead labels — non-interactive; Playwright asserts "Operator ·
SSO", "Tenant federation", the exact live "
Nconnections" text, and matching masthead/roster count attributes
Connection card (per row in data.connections)#
- Tenant eyebrow + label (text) — "Tenant · {tenantId}" +
tenantLabel; non-interactive; Playwright asserts seeded SAML/OIDC attributes and visible field cells (no click into a tenant or connection-edit view) - Protocol · status (text) — "{PROTOCOL} · {status}"; non-interactive
- IdP metadata URL (text) — displayed verbatim; not a link (plain
text with
word-break: break-all); verify whether it should be clickable to the IdP metadata document. Playwright pins SAML metadata URL verbatim and OIDC issuer-derived discovery URL - Last sync (text) — localized or "never"; non-interactive
- Card — static roster card is non-interactive; edit/test/delete live in the editor row below, keyed by admin connection id
SSO editor#
- List — loads the live admin SSO list through
ssoClient.list; browser E2E bridges the request to the real BFF with an operator-admin bearer - Create connection — Playwright 2026-06-25 fills the New connection form for a valid OIDC connection, submits, verifies the form reset, reads the persisted connection back from the BFF, and sees the new editor row
- Import metadata — Playwright 2026-06-26 drives the Import metadata
form for SAML XML and OIDC discovery through the live
POST /v1/admin/sso/importroute, reloads the editor roster, and verifies parsed entity/issuer, endpoint, thumbprint/JWKS, and required email claim mapping persistence by BFF read-back - Edit connection — Playwright opens a prefilled SAML connection, changes the required email mapping external claim, saves, and verifies the persisted BFF store
- Claim-mapping matrix — Playwright adds/persists all seven internal attribute slots and all four transform options through the browser editor and BFF read-back
- Delete connection — Playwright deletes a connection and verifies the editor row disappears and the BFF store no longer lists it
- Test connection — Playwright runs the browser Test action against real
local OIDC discovery/JWKS endpoints and asserts both the reachable verdict
and the fail-closed
oidc-jwks: failverdict - Client validation — unit-covered:
OperatorSsoEditor.test.tsxasserts the required email mapping and BFF validation-error surfaces
Footer actions#
- "← Operator hub" (
LBtnghost, link) →/operator - "Tenants" (
LBtnghost, link) →/operator/tenants - "LMS" (
LBtnghost, link) →/operator/lms - "Audit log" (
LBtnprimary, link) →/operator/audit
Data & contracts#
- Reads:
bffGet<SsoResponse>('/v1/sso')—{ generatedAt: string; connections: Connection[] }whereConnection = { id, tenantId, tenantLabel, protocol: 'saml'|'oidc', status: 'active'|'pending'|'disabled', idpMetadataUrl, lastSyncIso: string | null }. Onnullsubstitutes an empty list - Writes:
OperatorSsoEditoruses/v1/admin/ssofor create/list/update/ delete,/v1/admin/sso/importfor metadata/discovery import, and/v1/admin/sso/:id/testfor probe results - Realtime: None observed.
- Caching: RSC fetch,
next.revalidate = 60, 5 s abort - Auth/role check: session-gated only (
proxy.tsredirects sessionless →/welcome?redirect=); no operator-role gate in the proxy and no in-page guard
Cross-references#
- Parent:
operator-overview.md - Sibling operator routes:
operator-tenants.mdoperator-audit.mdoperator-incidents.mdoperator-tenant.md— per-tenant member consoleoperator-lms.md
- Feature spec:
V1/features.md - Architecture:
V1/ARCHITECTURE.md - Journeys traversing this view:
E2E coverage#
apps/oshun/web/e2e/tenant-sso-config.spec.ts— direct/operator/ssosigned-in render, stable page/masthead/roster/card/ field/footer anchors, generatedAt ISO attribute, live card count tied to the masthead and roster attributes, live BFF-seeded SAML and OIDC roster cards, seeded card id/tenant/label/protocol/status/metadata/sync attributes, visible tenant/protocol/IdP/sync field cells, footer targets (/operator,/operator/tenants,/operator/lms,/operator/audit), anonymous redirect-before-render,/v1/ssoreal-HTTP envelope tripwire, and browser editor create/import/edit/delete/test flows over the live admin SSO BFF via the operator-admin bridge.apps/oshun/web/e2e/tenant-sso-crud-bff.spec.ts— pure-BFF create/list/update/test/delete, validation, metadata import, and successful/fail-closed probe contracts.apps/oshun/web/e2e/tenant-sso-oidc-login-bff.spec.ts— real OIDC login/callback runtime with signed local id token, claim transforms, JIT, and failure verdicts.- Unit coverage:
apps/oshun/web/src/app/operator/sso/__tests__/page.test.tsxcovers server-component empty and BFF-null fallback branches;apps/oshun/web/src/components/operator/OperatorSsoEditor.test.tsxcovers client validation and API-error rendering.
Open questions / known gaps#
- The operator page now has a live SSO editor for create/edit/delete/test plus metadata import and claim mappings; this older read-only roster gap is closed on the operator surface. The separate tenant-admin metadata upload/discovery and enable-state-machine gaps remain journey-level product gaps
-
idpMetadataUrlis plain text, not a link — confirm whether operators should be able to open the IdP metadata document - No way to trigger a sync from this view;
lastSyncIsois display-only — document the sync trigger path - Customer
LCustomerNav(Library active) chrome on an operator route and norobots: { index: false }— confirm intended - An operator-role gate appears absent —
proxy.tsonly redirects sessionless users to/welcome; no role check in the proxy or page component (possible gap vs intended scope)