shoc-backend/docs/adr/0001-work-order-single-org-scope.md

5.5 KiB
Raw Blame History

ADR 0001: Work-order media uses single-org scope (board-aligned)

Status

Proposed — awaiting approval (2026-08-05).

This ADR is not self-accepted by the PR author. Approval is required from:

  • CODEOWNERS team @Sea-Haven-Industries/internal-dev (see .github/CODEOWNERS)
  • SH-116 product owner

Until approved (or until real tenant enforcement lands), this document records a pending exception request against the hard rule below — it does not waive the rule on its own.

Context

SH-116 requires that cross-tenant, unauthorized, and out-of-scope media access be rejected without metadata disclosure. The repository hard rule (ARCHITECTURE_AND_CODE_QUALITY.md §2) requires server-derived tenant scope: filtering by tenant/customer/owner comes from the authenticated principal and the data layer, never from client-supplied body/query as the source of truth.

Verified against the current codebase:

  • There is no TenantId / CustomerId column on WorkOrder, WorkOrderAttachments, or ApplicationUser.
  • WorkOrder.Customer is free-text (nvarchar), not a FK.
  • JWT issuance (AuthenticationService.GetToken) emits only Name, NameIdentifier, Jti, and Role — no tenant/customer claim.
  • Board, search, and detail already treat the deployment as a single organization via ApplyBaseScope (non-deleted, non-template).

Introducing a multi-tenant key requires product modeling plus a schema migration, which is out of scope for the SH-116 media contract and blocked by AGENTS.md (no migrations / product behavior without explicit instruction).

Decision (proposed)

Until real tenant enforcement exists, work-order media authorization matches the board:

  1. Organization boundary = ApplyBaseScope in the data layer (istemplate != true and not deleted). Lookups outside that set resolve as missing → NotFound / null (no disclosure).
  2. Authorization at service entry from claims: staff roles (Admin, Manager, Dispatcher, Supervisor) may read/mutate any in-org work order; role User (technician) only when WorkOrder.AssignTo == actorId; delete remains staff-only.
  3. Scope is never taken from request body or query as the trust source; actorId and roles come from the authenticated principal.

This is not a substitute for SH-116 cross-tenant isolation. It is the interim behavior while the exception is under review or real tenant keys land.

Accepted risk (while Proposed / if Accepted)

Any authenticated staff principal who knows a numeric work-order id can read or mutate media for that work order, provided it passes ApplyBaseScope. There is no server-derived tenant/customer boundary separating staff access across customers. Deleted / template / missing ids are not a cross-tenant test; they only prove the ApplyBaseScope filter.

Consequences

  • Tests for deleted, template, and missing work-order ids cover ApplyBaseScope rejection only — they must not be labeled as SH-116 cross-tenant coverage.
  • Staff org-wide access by numeric id remains intentional and aligned with the board for this interim state.
  • Merge of PR #47 that relies on this ADR requires either:
    1. formal approval of this ADR by the approvers listed in Status, or
    2. landing of real tenant enforcement (see below).

Path to real enforcement

Candidate design for the deferred multi-tenant work (tracked in the linked Jira ticket):

  1. Tenant key — reuse the existing Accounts entity (CRM customer) as the customer boundary; add WorkOrder.AccountId (FK) and ApplicationUser.AccountId (or an equivalent membership table).
  2. Claim — emit a server-derived account_id (or equivalent) claim in AuthenticationService.GetToken from the authenticated user’s account membership; never accept account id from body/query as trust source.
  3. Data filter — extend ApplyBaseScope (or a sibling filter) so board, detail, search, and media loads restrict by the claim-derived account scope; staff may still be broader if product defines org-wide roles, but that must be an explicit claims rule, not “any numeric id”.
  4. Backfill — map free-text WorkOrder.Customer strings to Accounts rows where possible; unresolved rows need a product decision (block, orphan bucket, or manual remapping).
  5. Tests — add true cross-tenant rejection tests (staff/tech of account A cannot read or mutate media of a work order owned by account B) with stable NotFound / Forbidden and no metadata disclosure.

Excepted rule

Hard rule: server-derived tenant scope (ARCHITECTURE_AND_CODE_QUALITY.md §2).

Requested clarification while this ADR is Proposed/Accepted: in the work-order domain, the interim server-derived scope key is the organization boundary enforced by ApplyBaseScope plus claims-derived role/assignee — not a TenantId/CustomerId column. Absence of a multi-tenant key is a temporary product/architecture gap until superseded by the path above.

Review / expiry

Re-review by 2027-02-04, or earlier if product introduces TenantId/CustomerId/AccountId on work orders or JWT claims, or when the linked multi-tenant ticket closes.

References

  • SH-116 — Completion document: fields + media categorization
  • SH-221 — Server-derived tenant/customer scope for Work Order domain (deferred enforcement)
  • PR that relies on this ADR: Sea-Haven-Industries/shoc-backend#47
  • WorkOrderBoardQueryFilters.ApplyBaseScope
  • WorkOrderMediaAuthorization
  • ARCHITECTURE_AND_CODE_QUALITY.md §2, §10
  • REVIEW_AND_PR_FRAMEWORK.md §7, §8