profile.mdoperator-audit.mdoperator-metis.mdoperator-tenant.mdJourney flow#
Generated from the authored steps below — click a node to jump to that section.
The platform-admin APIs ship two durable, operator-owned workflows. Bulk
operations validate, dry-run, stage, and commit four mutation kinds through real
executors; unsupported kinds fail loudly. Bulk exports enforce the
resource/format/residency matrix, create SHA-256 manifests, require a release
decision, and gate raw-file reads. /operator/tenant exposes the export
workflow only; there is no browser UI for drafting or committing bulk
operations.
Personas#
- Platform operator — holds
admin:*oradmin:studioand owns operation and export records under their authenticated user id. - Tenant operations specialist — prepares user, entitlement, roster, or persona rows for a target tenant.
- Compliance reviewer — verifies an export manifest and approves or rejects release.
- Workspace tenant admin — intentionally receives
403; the routes are not available toadmin:workspace:tenant-console.
Pre-conditions#
- The BFF is running with an admin bearer. Both route families also expose
legacy
/admin/*aliases, while the UI uses/v1/admin/*. - Admin snapshot persistence is configured if operations/archives must survive restart. Without the admin database, their singleton stores are process-local.
- Commit targets are available: customer auth/profile, tenant seat allocation, OneRoster, and active persona stores.
- Export records include their own
resourceKind; residency validation also reads each record's residency field.
Steps#
1. Draft an operator-owned bulk operation#
POST /v1/admin/bulk-operations accepts kind, title, rationale, target tenant,
and rows. The response starts at drafted; another operator's list remains
empty. Validation derives required-field and type issues, while dry-run
classifies each row as create, update, or skip and records a field-level plan.
No CSV/file upload parser feeds this request. Operators or automation must construct the typed JSON rows directly.
2. Validate, dry-run, and stage#
Stable transitions are drafted → validated → dry_run_complete → staged.
Validation errors remain inspectable at validated and block the dry-run
transition. Invalid or repeated transitions return 409. Creation and every
successful transition append admin.bulk_operation.* audit events.
3. Commit through the exact executor matrix#
Commit requires rationale. Non-skip rows run sequentially through these real effects:
| Kind | Commit effect |
|---|---|
users |
Provision customer account, then allocate a target-tenant seat |
entitlements |
Update an existing account plan to free/pro/premium |
rosters |
Apply one durable OneRoster commit per row |
persona_assignments |
Set the subject's active persona and surface |
content, taxonomy |
Fail with “no commit executor … nothing was changed” |
A row failure stops subsequent rows and marks the operation failed, but
earlier successful rows remain applied and are retained in commitOutcomes.
This is an honest partial-commit ledger, not a transaction or automatic
rollback.
4. Verify mutation read-backs#
The real-BFF journey reads the created user and plan through /v1/profile, the
roster through /v1/admin/lms/oneroster, and the persona selection through
/v1/personas/active. It also reads both universal admin search and structured
audit-log events. Unsupported content commit stays failed with null
committedAt and an explicit reason.
5. Create an integrity-manifest export#
POST /v1/admin/bulk-exports validates the resource/format matrix, record-kind
equality, and per-record residency against the target region. A successful
archive contains version oshun-bulk-export-v1, file summaries and content,
file SHA-256 values, a manifest SHA-256, a pending release, and a 30-day
retention timestamp. List/create projections omit raw file content; detail read
includes it for operator inspection.
The browser panel is intentionally narrower than the API: it always creates a
two-record content/json/us archive from two typed titles. It does not
expose other resource kinds, formats, regions, or arbitrary records.
6. Verify and decide release#
POST /:opId/verify recomputes the file/manifest integrity verdict. The API
accepts approved or rejected release decisions with rationale. The browser
renders only Approve. The archive owner is also recorded as the approver; there
is no distinct reviewer identity or four-eyes gate.
Before approval, the file endpoint returns
409 bulk_export_release_not_approved. After approval it returns the raw
content with file/manifest hash, residency, retention, and content-disposition
headers. The browser panel calls getText and renders the content inside the
page despite the button label “Download released file”; it does not create a
browser download artifact.
7. Inspect durability and audit evidence#
When configured, operation states and full export archives—including file
content—write through to durable admin snapshots. Structured audit events cover
operation lifecycle/failure and export execute/release/download. Retention is
metadata only: the store does not purge expired archives or block a read after
retentionExpiresAtUnixSeconds.
Post-conditions#
- Supported mutation rows have observable downstream effects and per-row outcomes; unsupported kinds cannot masquerade as committed.
- Operation and export lists remain isolated by operator user id.
- An approved export has verifiable file/manifest hashes and release/audit metadata.
- The UI can create, inspect, verify, approve, and display one fixed-shaped content export.
- Bulk mutation UI, upload parsing, atomic rollback, reviewer separation, retention enforcement, and a real browser file download are absent.
Failure modes#
- Workspace scope — tenant-console workspace admins receive
403even for their own tenant. - Validation block — malformed user rows reach
validatedwith issues but cannot enter dry-run complete. - Partial commit — earlier executor effects survive when a later row fails;
retry/compensation must inspect
commitOutcomes. - Unsupported kind — content/taxonomy operations end
failedwith no false commit timestamp. - Duplicate transition/op id — invalid state changes and duplicate export ids are rejected per operator.
- Residency/kind mismatch — schema-valid export requests fail
422before an archive is created. - Same-actor approval — the archive owner can approve their own release; compliance separation is descriptive, not enforced.
- Retention metadata only — an expired timestamp does not delete or deny an archive/file.
- UI format narrowing — operators cannot use the page to export rights, users, OneRoster, CSV, or a non-US region.
- Misnamed download — the web control displays response text in the panel rather than handing a saved file to the browser.
E2E coverage#
apps/oshun/web/e2e/tenant-bulk-operations-export-bff.spec.tscovers auth/isolation, users/entitlements/rosters/persona executors, validation, unsupported failure, downstream read-backs, export matrix, integrity, release gate, response headers, and audit/search evidence over the real BFF.apps/oshun/web/e2e/tenant-bulk-operations-export-ui.spec.tsdrives the fixed content export panel through create, inspect, verify, approve, get-text “download,” and audit read-back.- Coverage depth: deep for the implemented APIs and export UI, partial for the named operator workflow. Mutation draft/diff/commit has no browser path; upload parsing, compensation, dual control, expiry enforcement, and varied export UI are unimplemented.
Per-view files touched#
operator/operator-tenant.md— fixed-shape export release panel; no bulk-operation panel.operator/operator-audit.md— structured lifecycle evidence.operator/operator-metis.md— OneRoster executor read-back.customer/09-account/profile.md— account/plan effect read-back at the API boundary.
Cross-references#
tenant-bulk-member-invite-scim.md— separate CSV/SCIM/member seams.tenant-oneroster-roster-sync.md— durable roster executor target.persona-voice-avatar-approval-workflow.md— adjacent persona governance, not invoked by assignment commit.- Sources: the two
admin-bulk-*.tsroute/store pairs,apps/oshun/bff/src/admin/bulk-commit-executors.ts,apps/oshun/web/src/components/lilith/TenantBulkExportPanel.tsx, and the admin bulk contracts underlibs/contracts/src/common/.
Open questions#
- Which operator UI owns JSON/CSV upload, row diff review, commit rationale, partial-outcome inspection, and compensation?
- Should row effects be transactional per operation, or what resumable idempotency/rollback contract makes partial commits safe?
- How will two-person release approval ensure the requester cannot approve their own export?
- What job enforces retention expiry and records purge evidence?
- Should the browser create a real downloaded file and verify response hashes client-side instead of only rendering response text?