shoc-backend/docs/adr/0001-work-order-single-org-scope.md
Arthur Bassi 61923b2a7d feat(work-orders): stamp board create account from location
Org-wide create no longer depends on customer name. POST /workorders/board requires locationId and stamps WorkOrder.AccountId from Location.AccountId.
2026-08-26 09:12:48 -03:00

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.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