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

123 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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