Gate each search domain on its own permission

Global search is gated on assets.view, and that was the whole check.
Behind it the route queried twelve domains: applications, the knowledge
base, guides, the staff directory, assets, measuring tools, USB devices,
printed parts, custom fields, notifications, hostnames, and vendor and
model reference data, plus addresses and subnets for an IP-shaped query.
None of them consulted their own permission.

So anyone who could look up an asset number could also read the staff
directory out of the HR database, the USB checkout history and who holds
which drive, and the network inventory. The SEARCH_DOMAINS settings look
like they bound this but they are site feature toggles, not
authorization.

Each domain now answers only to a caller who holds its permission.
Assets, custom fields and vendor/model stay on assets.view because all
three emit asset rows routed to asset pages.

caller_holds rather than require_permission: refusing the whole request
because one domain is off-limits would make search useless to anyone
holding less than everything. A domain the caller cannot see contributes
nothing and the rest still answers.

This is what makes assets.view safe to grant the guest role again. It
left the grantable set in 3474028 because of the leak above, and that
took the shop floor's asset lookup with it - a signed-out visitor at a
machine could no longer search for the asset number in front of them.
Now the grant reaches assets and nothing else.

The first version of the leak test passed with the gates removed. The
staff directory is an external HR database and is unreachable under
test, so the domain silently returned nothing and the assertion proved
nothing. It patches _employee_rows now, and fails when the gate goes.

Scope: product
(cherry picked from commit 0a01ddcf25)
This commit is contained in:
cproudlock
2026-08-31 08:34:04 -04:00
parent dd52666d6b
commit 4d0baaff77
5 changed files with 160 additions and 23 deletions

View File

@@ -128,6 +128,23 @@ ADR-007 and ADR-002.
### Fixed
- **Global search answered twelve domains behind one permission.** The route is
gated on `assets.view`, and that was the entire check: looking up an asset
number also read out of the staff directory, the USB checkout history, the
notification log and the network inventory, none of which consult their own
permission. The `SEARCH_DOMAINS` settings are site feature toggles, not
authorization, and were never doing that job. Each domain now answers only to
a caller holding its permission - `employees.view` for the directory,
`usb.view` for checkouts, `network.view` for address and subnet matches,
`computers.view` for hostnames. Asset, custom-field and vendor/model results
stay on `assets.view`, since all three emit asset rows. The check is
`caller_holds` rather than `require_permission`: a search that refused the
whole request because one domain was off-limits would be useless, so a domain
the caller cannot see simply contributes nothing. This is also what makes
`assets.view` safe to hand to the guest role again, which is what lets a
signed-out visitor look up an asset number without also getting the staff
directory.
- **The printer supplies card was ordered by printer name, so the emptiest
cartridge was not first and sometimes was not shown at all.** Rows sorted on
"is critical" and then alphabetically, and critical is everything at or below