2026-08-11 10:45:37 -03:00
|
|
|
# ADR 0001: Work-order domain uses server-derived account scope
|
2026-08-04 16:07:26 -03:00
|
|
|
|
|
|
|
|
## Status
|
|
|
|
|
|
2026-08-11 10:45:37 -03:00
|
|
|
**Superseded** (historical Proposed single-org ADR). Current contract (PR #47 / SH-221):
|
2026-08-05 09:57:14 -03:00
|
|
|
|
2026-08-11 10:45:37 -03:00
|
|
|
- `WorkOrder.AccountId` / `ApplicationUser.AccountId` schema keys (nullable; legacy null fail-closed)
|
2026-08-06 10:26:04 -03:00
|
|
|
- JWT `account_id` when `ApplicationUser.AccountId` is set
|
|
|
|
|
- JWT `org_scope=all` when Admin has no AccountId (explicit signed elevation)
|
2026-08-11 14:52:02 -03:00
|
|
|
- **Reads** (board, list, advanced search, detail, media, board comments,
|
2026-08-11 15:18:29 -03:00
|
|
|
legacy comments-by-work-order-id, legacy `GET GetComments`): `ApplyBaseScope` +
|
2026-08-11 10:45:37 -03:00
|
|
|
`ApplyAccountScope(int)` when account-scoped; org-wide path skips account filter
|
2026-08-11 14:52:02 -03:00
|
|
|
- **Writes** (board mutations, media, board/legacy comments, `POST …/completion-doc`):
|
|
|
|
|
same account filter at service/data entry; authorize before storing blobs
|
2026-08-26 09:12:48 -03:00
|
|
|
- **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
|
2026-08-06 10:26:04 -03:00
|
|
|
- Missing/malformed scope → **Forbidden** (absence of claim does not elevate)
|
2026-08-05 09:57:14 -03:00
|
|
|
|
2026-08-06 09:47:34 -03:00
|
|
|
## Context (historical)
|
2026-08-04 16:07:26 -03:00
|
|
|
|
|
|
|
|
SH-116 requires that cross-tenant, unauthorized, and out-of-scope media access
|
2026-08-06 10:26:04 -03:00
|
|
|
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.
|
2026-08-06 09:47:34 -03:00
|
|
|
|
2026-08-11 10:45:37 -03:00
|
|
|
## Current domain contract (superseding)
|
2026-08-06 09:47:34 -03:00
|
|
|
|
|
|
|
|
1. **Organization boundary** = `ApplyBaseScope` (non-deleted, non-template).
|
2026-08-06 10:26:04 -03:00
|
|
|
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.
|
2026-08-11 10:45:37 -03:00
|
|
|
5. **Create stamp**:
|
2026-08-26 09:12:48 -03:00
|
|
|
- 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
|
2026-08-11 10:45:37 -03:00
|
|
|
`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.
|
2026-08-11 14:52:02 -03:00
|
|
|
Board comments, legacy comments-by-WO-id, and completion-doc uploads apply the
|
|
|
|
|
same account filter before read/write (and before blob storage).
|
2026-08-11 10:45:37 -03:00
|
|
|
8. **User lifecycle** persists `AccountId` on Admin create/edit so non-Admin
|
|
|
|
|
principals can receive `account_id`.
|
2026-08-04 16:07:26 -03:00
|
|
|
|
|
|
|
|
## Consequences
|
|
|
|
|
|
2026-08-11 10:45:37 -03:00
|
|
|
- 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`).
|
2026-08-26 09:12:48 -03:00
|
|
|
- Board create resolves from `Location.AccountId`. Customer name match remains
|
|
|
|
|
the ingest/webhook/legacy resolution path.
|
2026-08-05 09:57:14 -03:00
|
|
|
|
|
|
|
|
## Excepted rule
|
2026-08-04 16:07:26 -03:00
|
|
|
|
2026-08-11 10:45:37 -03:00
|
|
|
None. Hard rule **server-derived tenant scope**
|
|
|
|
|
(`ARCHITECTURE_AND_CODE_QUALITY.md` §2) is enforced via claims + AccountId.
|
2026-08-04 16:07:26 -03:00
|
|
|
|
|
|
|
|
## Review / expiry
|
|
|
|
|
|
2026-08-11 10:45:37 -03:00
|
|
|
Re-review by **2027-02-04**, or when AccountId becomes non-nullable with a
|
|
|
|
|
full backfill migration.
|
2026-08-04 16:07:26 -03:00
|
|
|
|
|
|
|
|
## References
|
|
|
|
|
|
|
|
|
|
- SH-116 — Completion document: fields + media categorization
|
2026-08-06 09:47:34 -03:00
|
|
|
- SH-221 — Server-derived tenant/customer scope for Work Order domain
|
|
|
|
|
- PR: Sea-Haven-Industries/shoc-backend#47
|
|
|
|
|
- `WorkOrderBoardQueryFilters.ApplyBaseScope` / `ApplyAccountScope`
|
2026-08-11 10:45:37 -03:00
|
|
|
- `IWorkOrderAccountResolver` / `WorkOrderMediaAuthorization` / `SeaHavenClaimTypes`
|
2026-08-05 09:57:14 -03:00
|
|
|
- `ARCHITECTURE_AND_CODE_QUALITY.md` §2, §10
|