case 4712 - customer contacts limited to the routes the customer screens use

Customer contacts had the Customer role's Select and read rights on most record
types and could call any route whose staff role check they passed, including
several with no role check (security review F13 to F17). Step 1 of 4712: the
token middleware now answers 403 (same body as other 403s) to Customer and
HeadOffice users for any route not in CustomerContactRoutes, the routes the
customer screens use (read from raven-client). Anonymous routes are always
allowed. New routes stay closed to contacts unless added on purpose.
Ownership of the records these routes return follows in the next steps.

Checked: full integration suite 103 passed, 5 skipped, the same 2 known
failures as the baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 13:44:02 -07:00
parent 7b40caec8b
commit 2b1adaa496
4 changed files with 118 additions and 6 deletions

View File

@@ -855,7 +855,7 @@ cases"): 4710 (F5, F41), 4711 (F1), 4712 (F13 to F17), 4715 (F21), 4716 (F18), 4
the AyaNova half of F39), Sockeye 4681 (F40). The Sockeye half of F39 (repeatable licence fetch) moved to a later case: AyaNova reinstalls and logs any key it fetches without comparing it with the installed one (trial servers with data would log an E1020 error on every fetch), and deployed older servers cannot be changed, so it needs an AyaNova-side change plus a request flag that Sockeye honours. Not in 8.3.0: F32, F42 (John's later cases); F37 accepted. When a
case is done, its findings' status becomes *fixed in case NNNN*.
## Customer contact route allowlist (draft, for the F13 to F17 fix)
## Customer contact route allowlist (implemented in case 4712)
The API routes the client can call for a customer contact (`UserType` Customer or HeadOffice),
read from `raven-client` on 2026-10-23: the customer menu in `api/initialize.js` (user
@@ -876,15 +876,18 @@ each id route must also check.
|---|---|---|
| Login, session (already anonymous or exempt) | `auth`, `auth/tfa-authenticate`, `auth/reset-password`, `notify/hello`, `translation/subset[/{id}]`, `logo/{size}`, `license/lc` (F10) | — |
| Own account | `auth/change-password`, `auth/totp`, `auth/totp-validate`, `auth/totp-disable/{id}`, `user-option/{id}` GET and PUT | `{id}` = the caller (`totp-disable` already; check `user-option`) |
| App shell | `global-biz-setting/client`, `enum-list/list/{key}`, `form-custom/{formkey}`, `translation/list`, `server-info`, `notify/new-count`, `notify/app-notifications`, `DELETE notify/{id}` | `notify/{id}`: the caller's own notification (pass 7) |
| App shell | `global-biz-setting/client`, `enum-list/list/{key}`, `form-custom/{formkey}`, `translation/list`, `translation/subset` (logged-in version, used to load translations), `server-info`, `notify/new-count`, `notify/app-notifications`, `DELETE notify/{id}` | `notify/{id}`: the caller's own notification (pass 7) |
| Data lists (work orders, CSRs) | `data-list` (server already forces the customer filter), `data-list/listfields`, `data-list-column-view…`, `data-list-filter…`, `tag-list/list`, `pick-list/list` from the filter editor | saved filters: already scoped to the user |
| Work order (customer view) | `workorder/customer/{id}`, `attachment/list` (work order only), `attachment/download/{id}` (download key), `report/render-job` with report `-100` only, `job-operations/status|logs/{gid}`, `report/request-cancel/{gid}`, `report/download/{file}` | work order belongs to the contact's customer (or a customer of its head office): **F13**; job and report file belong to the caller (pass 4) |
| Service requests | `csr` POST/PUT, `csr/{id}` GET/DELETE (not `accept`/`reject`), `unit/customerunit` POST, `pick-list/list` and `pick-list/single` for Customer (head-office variant), Unit (own customer) and UnitModel only, with the server's own template | CSR and unit belong to the contact's customer; `CustomerId` forced on create: **F14**, **F15** |
| Notification subscriptions | `notify-subscription/list`, `notify-subscription/{id}` GET/DELETE, `notify-subscription` POST/PUT, `work-order-status/list`, `quote-status/list` (names only, used by the editor) | `UserId` forced to the caller: **F17** |
To confirm while fixing: whether the `open-object-handler` routes (`attachment/parent`,
`form-custom/form-key`, `user/inside-type`, `search/ancestor`) are used when a customer opens
a notification; whether `server-info` (about page) must stay open to contacts.
Confirmed while fixing (case 4712): for customer users the client's open-object handler only navigates
to the customer screens, so its staff routes (`attachment/parent`, `form-custom/form-key`,
`user/inside-type`, `search/ancestor`) are not needed; `server-info` stays open (the About page calls it
for everyone; F29 trims what it returns). The list lives in
`server/AyaNova/ControllerHelpers/CustomerContactRoutes.cs`; the token middleware answers 403 to a
contact for anything else (also closes F8's suffix exemption for contacts).
**Shape of the fix.** One check in the token middleware, which already loads `UserType` for
every request: for Customer and HeadOffice users, compare the matched endpoint's route