mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 16:33:12 +00:00
Org-wide create no longer depends on customer name. POST /workorders/board requires locationId and stamps WorkOrder.AccountId from Location.AccountId.
4.1 KiB
4.1 KiB
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.AccountIdschema keys (nullable; legacy null fail-closed)- JWT
account_idwhenApplicationUser.AccountIdis set - JWT
org_scope=allwhen 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
AccountIdfrom JWTaccount_idorLocation.AccountIdvia requiredlocationId; bodycustomer/accountIdare not trusted. AddWorkorder, ingest, webhook/recon, sync still stamp from claim or uniqueAccounts.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)
- Organization boundary =
ApplyBaseScope(non-deleted, non-template). - Account boundary = claim
account_id→WorkOrder.AccountId == claim. - Org-wide = claim
org_scope=allonly (issued to Admin without AccountId). Not inferred from missingaccount_id. - Fail-closed = no valid account or org-scope claim → Forbidden.
- Create stamp:
- Board
POST /workorders/board: requiredlocationId. Scoped → stamp JWTaccount_idonly whenLocation.AccountIdmatches (else Forbidden). Org-wide → stampLocation.AccountId. Missing location → NotFound; nullLocation.AccountId→ AccountUnresolved. Bodycustomer/accountIdare ignored for the stamp. - Legacy AddWorkorder: authenticated +
account_id→ stamp claim; authenticated +org_scope=all→ unique Customer→Accounts.Name; elseAccountUnresolved. - Ingest / webhook / sync → same Customer resolution; unresolved create is rejected or skipped (no null AccountId on new rows).
- Board
- Legacy rows with
AccountId == nullare invisible to account-scoped callers; onlyorg_scope=allmay read them. - Authorization at service entry: staff roles may read/mutate any resulting
work order; role
Useronly whenAssignTo == 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). - User lifecycle persists
AccountIdon Admin create/edit so non-Admin principals can receiveaccount_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/ApplyAccountScopeIWorkOrderAccountResolver/WorkOrderMediaAuthorization/SeaHavenClaimTypesARCHITECTURE_AND_CODE_QUALITY.md§2, §10