# ADR 0001: Work-order domain uses server-derived account scope ## Status **Superseded** (historical Proposed single-org ADR). Current contract (PR #47 / SH-221): - `WorkOrder.AccountId` / `ApplicationUser.AccountId` schema keys (nullable; legacy null fail-closed) - JWT `account_id` when `ApplicationUser.AccountId` is set - JWT `org_scope=all` when Admin has no AccountId (explicit signed elevation) - **Reads** (board, list, advanced search, detail, media, board comments, legacy comments-by-work-order-id, legacy `GET GetComments`): `ApplyBaseScope` + `ApplyAccountScope(int)` when account-scoped; org-wide path skips account filter - **Writes** (board mutations, media, board/legacy comments, `POST …/completion-doc`): same account filter at service/data entry; authorize before storing blobs - **Creates** (board): stamp `AccountId` from JWT `account_id` or `Location.AccountId` via required `locationId`; body `customer`/`accountId` are not trusted. AddWorkorder, ingest, webhook/recon, sync still stamp from claim or unique `Accounts.Name` ↔ `Customer`; unresolvable → reject/skip - Missing/malformed scope → **Forbidden** (absence of claim does not elevate) ## Context (historical) SH-116 requires that cross-tenant, unauthorized, and out-of-scope media access be rejected without metadata disclosure. An interim Proposed ADR allowed org-wide staff access via absence of an account claim; that path was rejected in review (fail-open) and replaced by the contract below. ## Current domain contract (superseding) 1. **Organization boundary** = `ApplyBaseScope` (non-deleted, non-template). 2. **Account boundary** = claim `account_id` → `WorkOrder.AccountId == claim`. 3. **Org-wide** = claim `org_scope=all` only (issued to Admin without AccountId). Not inferred from missing `account_id`. 4. **Fail-closed** = no valid account or org-scope claim → Forbidden. 5. **Create stamp**: - Board `POST /workorders/board`: required `locationId`. Scoped → stamp JWT `account_id` only when `Location.AccountId` matches (else Forbidden). Org-wide → stamp `Location.AccountId`. Missing location → NotFound; null `Location.AccountId` → AccountUnresolved. Body `customer`/`accountId` are ignored for the stamp. - Legacy AddWorkorder: authenticated + `account_id` → stamp claim; authenticated + `org_scope=all` → unique Customer→Accounts.Name; else `AccountUnresolved`. - Ingest / webhook / sync → same Customer resolution; unresolved create is rejected or skipped (no null AccountId on new rows). 6. **Legacy rows** with `AccountId == null` are invisible to account-scoped callers; only `org_scope=all` may read them. 7. **Authorization at service entry**: staff roles may read/mutate any resulting work order; role `User` only when `AssignTo == actorId` (media); delete staff-only. Board comments, legacy comments-by-WO-id, and completion-doc uploads apply the same account filter before read/write (and before blob storage). 8. **User lifecycle** persists `AccountId` on Admin create/edit so non-Admin principals can receive `account_id`. ## Consequences - Cross-account and missing-scope tests are required for create and read paths. - Dispatcher/Manager/Supervisor/User without AccountId cannot access board, detail, search, list, or media until AccountId is assigned (or they are Admin with `org_scope=all`). - Board create resolves from `Location.AccountId`. Customer name match remains the ingest/webhook/legacy resolution path. ## Excepted rule None. Hard rule **server-derived tenant scope** (`ARCHITECTURE_AND_CODE_QUALITY.md` §2) is enforced via claims + AccountId. ## Review / expiry Re-review by **2027-02-04**, or when AccountId becomes non-nullable with a full backfill migration. ## References - SH-116 — Completion document: fields + media categorization - SH-221 — Server-derived tenant/customer scope for Work Order domain - PR: Sea-Haven-Industries/shoc-backend#47 - `WorkOrderBoardQueryFilters.ApplyBaseScope` / `ApplyAccountScope` - `IWorkOrderAccountResolver` / `WorkOrderMediaAuthorization` / `SeaHavenClaimTypes` - `ARCHITECTURE_AND_CODE_QUALITY.md` §2, §10