mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-03 02:13:31 +00:00
123 lines
5.5 KiB
Markdown
123 lines
5.5 KiB
Markdown
# 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
|