operator-lms.mdoperator-metis.mdoperator-tenants.mdtenant-bulk-member-invite-scim.mdJourney flow#
Generated from the authored steps below — click a node to jump to that section.
The shipped OneRoster path can preview normalized roster JSON, commit a clean plan through admin APIs, and project committed counts into Metis. It does not fetch from an SIS, schedule pulls, expose an operator Apply control, or remove people and enrollments that disappear upstream. This is an import and reconciliation seam, not a complete bidirectional roster sync.
Personas#
- Tenant LMS/SIS operator — normalizes institutional user and enrollment
records and inspects a proposed diff in
/operator/lms. - API operator — calls the apply or pull endpoint after external review.
- Metis teacher or administrator — reads the committed roster summary in
/operator/metis. - Security reviewer — needs tenant-qualified authorization, durable commit identity, conflict evidence, and auditable changes.
Pre-conditions#
- Incoming users contain
sourcedId,username,enabledUser, and a supported OneRoster role; enrollments identify an existing or incoming user and class. - Admin routes accept
admin:*oradmin:workspace:tenant-console. The workspace scope is unqualified, and the route does not bind the requested tenant to a tenant claim. - Deployable BFF mutation paths require the admin database. Startup hydrates the
OneRoster snapshot before traffic; if the sink is unavailable, apply/pull
return
503 oneroster_persistence_failedinstead of falling back to memory. - An external caller, not Oshun, obtains and normalizes the SIS payload.
Steps#
1. Choose the comparison boundary#
There are two different dry-run contracts.
POST /v1/tenant/lms/oneroster/dry-run compares caller-supplied incoming*
arrays to caller-supplied existing* arrays; it does not read the committed
store. POST /v1/admin/lms/oneroster/preview compares the incoming arrays with
the current durable tenant projection. The operator page uses the second
contract.
2. Preview normalized JSON in the operator page#
/operator/lms accepts pasted normalized JSON and calls the preview endpoint.
The diff table separates user creates, user updates, enrollment creates, and
blocking conflicts. Preview is read-only: it leaves the committed roster and
commit list unchanged.
The browser surface has no file parser, provider connection, Apply button,
approval queue, or scheduling control. A clean preview therefore ends at
canApply: true; it does not authorize or execute the import.
3. Apply a clean plan through the API#
POST /v1/admin/lms/oneroster/apply reruns the real dryRunOneRosterImport
engine against committed state. A conflict-free request persists the users,
enrollments, and replay-prevention commit as one ordered snapshot, publishes it
only after the save succeeds, then returns applied counts and rosterAfter. A
duplicate commit id is rejected, a conflicting plan mutates nothing, and a
failed save leaves both the visible roster and commit history unchanged.
Commit identity is not fully tenant-qualified in storage: the duplicate map is
keyed globally by commitId. Callers should not assume the same commit id can
be reused safely for another tenant.
4. Understand what “sync” changes#
The plan creates users, updates changed users, and creates enrollments. It does not delete or deactivate a committed user that is absent from the new payload, and it does not remove an enrollment omitted upstream. Clean application proves additive/update reconciliation only, not exact mirror convergence.
5. Model an externally triggered pull#
POST /v1/admin/lms/oneroster/pull accepts an already normalized payload plus
pull metadata. It derives a pull:<tenantId>:<pullId> commit, reconciles it
against stored state, and applies a clean plan. Despite the route name, this
code performs no network request to an SIS and no periodic scheduler invokes it;
an external worker must supply the payload and timing.
6. Read the committed projection#
GET /v1/admin/lms/oneroster lists commits and per-tenant roster summaries. The
Metis operator page uses that read model for aggregate users/enrollments and
tenant cards. It is a read-only projection; Metis does not initiate or repair a
roster import.
7. Follow audit evidence#
A successful apply emits tenant.oneroster.commit.applied. A clean pull emits
tenant.oneroster.pull.reconciled, and a pull conflict emits
tenant.oneroster.pull.conflicts_detected. The conflict audit persists only the
count; the authorized HTTP response still returns actionable conflict text.
Apply conflicts and duplicate apply attempts return errors without audit events,
so the audit stream is not a complete attempt ledger.
Post-conditions#
- A clean API apply stores one commit plus the created/updated users and new enrollments and exposes updated summary counts.
- Preview has not mutated committed state.
- Conflict apply/pull and duplicate commit/pull attempts do not alter roster counts.
- Successful apply, successful pull, and pull conflict have the documented audit evidence.
- A successful apply/pull acknowledgement means roster state and its commit id are recoverable after restart; a failed save emits no success audit.
- No claim is established for SIS connectivity, periodic execution, deletion/ deactivation, operator approval, or browser-side commit.
Failure modes#
- Cross-tenant workspace authority — an unqualified tenant-console scope can operate on a request-selected tenant; tenant membership is not enforced.
- False source of truth — the tenant dry-run endpoint can compare two caller-provided snapshots and therefore cannot prove parity with committed state.
- Preview mistaken for commit — the operator UI can show
canApply: truebut has no Apply control or mutation receipt. - No actual pull — the pull endpoint receives data; it neither connects to an SIS nor schedules itself.
- Stale records remain — missing users and enrollments are not disabled or removed, so upstream deletions do not converge.
- Global commit-id collision — the same commit id can conflict across tenants because duplicate identity is not tenant-keyed.
- Username or reference conflict — collisions and enrollments for unknown
users return a blocked plan; apply returns
422 has_conflicts. - Incomplete attempt audit — duplicate and conflicting applies leave no audit event, while pull conflicts do.
- Persistence unavailable — deployable mutations fail closed with
503; they do not publish a process-local roster or success audit. - Replica race — mutation ordering is per BFF process over one whole-store snapshot; there is no distributed compare-and-swap across replicas.
- No account-subject link — roster
sourcedId/usernamevalues are institution identities, not an exact customer-auth ownership key. Account DSAR/erasure must not guess from usernames; tenant offboarding is separate.
E2E coverage#
apps/oshun/web/e2e/tenant-oneroster-provisioning-bff.spec.tscovers authorization, validation, clean/conflict dry-runs, apply/list, duplicates, pull reconciliation/conflicts, mutation safety, and audit read-back against the real BFF.apps/oshun/web/e2e/tenant-bulk-operations-export-bff.spec.tsproves a roster bulk-operation row awaits the same durable authority and is visible through the OneRoster projection read-back.apps/oshun/web/e2e/operator-lms.spec.tsdrives the browser preview form and proves clean/conflict previews do not mutate the committed roster.apps/oshun/web/e2e/tenant-oneroster-metis-card.spec.tsapplies through the API and verifies the committed summary in/operator/metis.- Coverage depth: deep for the local preview/apply/read spine; absent for SIS network ingestion, scheduling, browser commit, and removal convergence.
Per-view files touched#
operator/operator-lms.md— paste-and-preview diff table, without Apply.operator/operator-metis.md— read-only OneRoster summary card.operator/operator-tenants.md— tenant context adjacent to the roster projection.tenant-bulk-member-invite-scim.md— separate identity and membership projections.
Cross-references#
tenant-onboard.md— invite acceptance and seat allocation outside OneRoster.tenant-audit-log-investigation.md— audit-store investigation boundaries.- Sources:
oneroster-route.ts,admin-oneroster-apply.ts,oneroster-apply-store.ts,OneRosterDiffPreviewForm.tsx, and the@metis/integrationsdryRunOneRosterImportengine.
Open questions#
- How should every route bind tenant id to a tenant-qualified admin claim?
- Which production worker owns SIS credentials, fetching, cadence, backoff, replay, and source receipts before calling the pull endpoint?
- Should commit identity be
(tenantId, commitId)rather than globally keyed? - What policy deactivates missing users and removes stale enrollments without erasing intentional local exceptions?
- Where should review and Apply live, and what audit event should capture every rejected, conflicting, or duplicate attempt?