Repository navigation
Commit 9112bcd
fix(objectql): file-field hydration asks canReadObject before reading sys_file, so a record read logs no access denial (#24434)
Fixes #24335
Clause-②: no (no accept set or public surface moves: the served bodies
and every refusal are unchanged; owes the seat's review of record at
`TIER_DEFAULT` before the queue)
## What changed
`resolveFileReferences` (`packages/objectql/src/engine.ts`) read
`sys_file` as the caller and took the refused arm on the throw. A member
with read on the record's object and without `sys_file` read therefore
made a `sys_file` access attempt on every by-id and list read, and
plugin-security logged `[Security] Access denied: operation 'find' on
object 'sys_file' is not permitted for positions [org_member, everyone]`
at WARN for each one.
The engine now **asks before it reads**, as the parent-record read gate
does ("A skipped object is not an access attempt",
`parent-record-read-gate.ts`):
- New private `callerMayReadSysFile` asks the registered security
service's `canReadObject('sys_file', caller)`. It goes through the slot
the engine already resolves (`resolveReadableIdsSecurity`, wired by
`ObjectQLPlugin`) and uses the caller's envelope minus its
operation-private keys (`withoutOperationPrivateKeys`, as
`readParentDerivedFiles` already does). No new security API.
- `false`: the refused arm is served without the read.
- `true`: the caller's read goes ahead exactly as before, and the
middleware still decides.
- Unasked (no usable security service, no `canReadObject`, or an ask
that throws): the read goes to the middleware as before. The ask can
only narrow what is read.
- The refused arm moves verbatim into `serveRefusedFileHydration`. It is
shared by the ask and by the existing `PERMISSION_DENIED` catch, which
stays for any refusal the admission did not foresee.
- ⛔ `plugin-security` is not touched: its refusal, its log line and its
level are unchanged.
## Why this shape (four axes)
- **Real business need:** measured. Every member of a deployment whose
app grants no `sys_file` read hits this on every record read (the WARN
reproduced below on the booted composition).
- **Long-term soundness:** engine-only. The security layer stays the
single judge of every read it sees, and `canReadObject` is its own
object-level gate, arm for arm. The alternative, a context flag marking
the hydration read internal, would cross into `plugin-security`
(`domain:services`) and add a bypass marker.
- **Guarding against AI mistakes:** no bypass marker means there is
nothing an author or agent can set to mute a denial. The refused value
contract (`FileRefusedValueSchema`) is unchanged.
- **Startup scope:** no new API, option, error code or gate.
## Measurements (M, booted composition)
New dogfood pin
`packages/qa/dogfood/test/file-field-hydration-no-denial.dogfood.test.ts`.
It runs on a real org-bound `bootStack` with the real `SecurityPlugin`,
the real `StorageServicePlugin` (local adapter, real bytes), and one
record object holding a field-owned file. "Before" is measured on the
same boot with `canReadObject` withdrawn from the service, which is the
engine's pre-change path.
| read | reader | before: security denials | after: security denials |
body |
|---|---|---|---|---|
| `GET /data/fhn_doc/ID` | member, no `sys_file` read | 1 (`on object
'sys_file'`) | **0** | byte-identical (`res.text()`), file hydrated |
| `GET /data/fhn_doc` | member, no `sys_file` read | 1 | **0** |
byte-identical, file hydrated |
| `GET /data/fhn_doc/ID` and list | reader with `sys_file` read | 0 | 0
| byte-identical, same hydrated value |
| `GET /data/sys_file/ID` | member | - | 1 | `403 PERMISSION_DENIED`,
unchanged |
Run on `df035c834`: `pnpm --filter @objectstack/dogfood exec vitest run
test/file-field-hydration-no-denial.dogfood.test.ts`, 5 passed.
**Ablation (one, of the ask):** done with `scripts/ablation-replace.mjs`
(anchor hit once, blob `0186fa3c` → `b21c19e7`). The ask was disabled,
objectql was rebuilt, and `ablation-dist-preflight` found the marker in
4 `dist/` files. The two member WARN-count pins went red with exactly
the issue's line: `expected [ "[Security] Access denied: operation
'find' on object 'sys_file' is not permitted for positions [org_member,
everyone]" ] to deeply equal []` (2 failed, 3 passed). Restore: blob
back to `0186fa3c`, `git diff HEAD` empty, rebuilt, `--absent` preflight
clean, 5 passed.
**Cost of the ask:** one `canReadObject` per hydration call, which is
once per request for both by-id and list (measured: 1 ask per by-id
read, 1 per list read), never per row. It is a cached permission check
with no store read: 6.4 µs per ask (mean of 300 iterations on a shared
box). The refused `sys_file` read it replaces cost about 1.2 ms. A
reader with `sys_file` read pays the extra ask. End-to-end by-id latency
on the shared box was within noise.
## Pins beside it
`packages/objectql/src/engine-file-hydrate-refused.test.ts` reuses that
file's existing driver double, so no new double is added. Its `boot`
gains a `security` slot option and an attempts log on the refusing
middleware. New cases:
- A refused admission makes no `sys_file` read and serves the refused
marker. The ask is about `sys_file`, carries the caller's envelope and
no `__` keys.
- The list and by-id bodies are byte-identical to the unasked engine's,
and that comparison is armed: the unasked engine did attempt the read.
- A direct `sys_file` read is still `PERMISSION_DENIED` / 403 and still
reaches the security layer.
- Controls: an admitted reader is hydrated; an admitted read that is
still refused takes the same arm; with no `canReadObject`, or an ask
that throws, the engine reads as before; a record with no file id asks
nothing.
Pin sweep: the existing pins on the refused arm and its `debug` line
(`engine-file-hydrate-refused.test.ts`,
`engine-file-hydrate-parent-derived.test.ts`, plugin-security
`file-field-hydration-refused.test.ts`, service-storage
`field-owned-file-hydration-door-agreement.test.ts`) wire no security
service into the slot, so they still exercise the read-then-refuse arm,
and they stay green unmodified.
## Verification record (head `df035c834`, after merging `origin/main`
`e22defb13`)
- `pnpm --filter @objectstack/objectql typecheck`: exit 0. `pnpm
--filter @objectstack/dogfood typecheck`: exit 0. Both were run before
and after the merge.
- `pnpm --filter @objectstack/objectql exec vitest run`: 445 files, 8366
tests passed.
- Dogfood pin: 5 passed (after the merge, on rebuilt `objectql`,
`plugin-auth` and `platform-objects`).
- `node scripts/pm/dispatch-gates.mjs --commands` (no paths), 74
families. All 72 runnable families exit 0 on `df035c834`. `--ran`
reconciliation: `74 derived, 72 run, 2 NOT-MEASURED, 0 UNRUN`.
- NOT MEASURED: `check:query-options-erasure` (repo-wide eslint scan).
The first local attempt was in flight when the container killed the
worker (exit 137), and the retry hit the 280 s local window. CI's `Lint
& Repo Gates` runs it.
- NOT MEASURED: `check:type-check-debt` (repo-wide tsc census). It hit
the same window and is left to CI.
- `check:dual-build-cjs-loads` first exited 3 (PREREQUISITE NOT MET: 8
unbuilt packages). It was built for and re-run, and passes: 111 require
entry points load.
- `node scripts/check-platform-checklist.mjs --changed-from
origin/main`: OK, 0 lost.
## Acceptance notes
- `canReadObject` mirrors the middleware's object-level arms but not its
public-form grant (`publicFormGrant`), which admits `find` only on the
form's own target object. A form targeting `sys_file` is not a realistic
declaration, so no such caller reaches this path. Noted, not filed.
- Locally, an interrupted gate run left `packages/spec/dist` without its
`.d.ts`, and a spec rebuild restored it. This was local build state, not
a finding.
- The dogfood pin is declared to `domain:cli` at review, per the claim.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01YXbt8cZ1Adcz5iV8AdwBXJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 4d12b23 commit 9112bcd
4 files changed
Lines changed: 541 additions & 39 deletions
File tree
- .changeset
- packages
- objectql/src
- qa/dogfood/test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
30 | 37 | | |
31 | 38 | | |
32 | 39 | | |
33 | 40 | | |
34 | | - | |
| 41 | + | |
35 | 42 | | |
36 | 43 | | |
37 | 44 | | |
| |||
142 | 149 | | |
143 | 150 | | |
144 | 151 | | |
145 | | - | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
146 | 155 | | |
147 | | - | |
| 156 | + | |
148 | 157 | | |
149 | 158 | | |
150 | 159 | | |
| |||
167 | 176 | | |
168 | 177 | | |
169 | 178 | | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
170 | 183 | | |
171 | 184 | | |
172 | 185 | | |
173 | | - | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
174 | 190 | | |
175 | 191 | | |
176 | 192 | | |
| |||
184 | 200 | | |
185 | 201 | | |
186 | 202 | | |
187 | | - | |
| 203 | + | |
188 | 204 | | |
189 | 205 | | |
190 | 206 | | |
| |||
296 | 312 | | |
297 | 313 | | |
298 | 314 | | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13305 | 13305 | | |
13306 | 13306 | | |
13307 | 13307 | | |
13308 | | - | |
13309 | | - | |
13310 | | - | |
13311 | | - | |
| 13308 | + | |
| 13309 | + | |
| 13310 | + | |
| 13311 | + | |
| 13312 | + | |
| 13313 | + | |
| 13314 | + | |
| 13315 | + | |
| 13316 | + | |
| 13317 | + | |
| 13318 | + | |
| 13319 | + | |
| 13320 | + | |
13312 | 13321 | | |
13313 | 13322 | | |
13314 | 13323 | | |
| |||
13355 | 13364 | | |
13356 | 13365 | | |
13357 | 13366 | | |
| 13367 | + | |
| 13368 | + | |
| 13369 | + | |
| 13370 | + | |
| 13371 | + | |
| 13372 | + | |
| 13373 | + | |
13358 | 13374 | | |
13359 | 13375 | | |
13360 | 13376 | | |
| |||
13408 | 13424 | | |
13409 | 13425 | | |
13410 | 13426 | | |
| 13427 | + | |
| 13428 | + | |
| 13429 | + | |
| 13430 | + | |
13411 | 13431 | | |
13412 | | - | |
13413 | | - | |
13414 | | - | |
13415 | | - | |
13416 | | - | |
13417 | | - | |
13418 | | - | |
13419 | | - | |
13420 | | - | |
13421 | | - | |
13422 | | - | |
13423 | | - | |
13424 | | - | |
13425 | | - | |
13426 | | - | |
13427 | | - | |
13428 | | - | |
13429 | | - | |
13430 | | - | |
13431 | | - | |
13432 | | - | |
13433 | | - | |
13434 | | - | |
13435 | | - | |
13436 | | - | |
13437 | | - | |
13438 | | - | |
13439 | | - | |
13440 | | - | |
13441 | | - | |
| 13432 | + | |
13442 | 13433 | | |
13443 | 13434 | | |
13444 | 13435 | | |
| |||
13485 | 13476 | | |
13486 | 13477 | | |
13487 | 13478 | | |
| 13479 | + | |
| 13480 | + | |
| 13481 | + | |
| 13482 | + | |
| 13483 | + | |
| 13484 | + | |
| 13485 | + | |
| 13486 | + | |
| 13487 | + | |
| 13488 | + | |
| 13489 | + | |
| 13490 | + | |
| 13491 | + | |
| 13492 | + | |
| 13493 | + | |
| 13494 | + | |
| 13495 | + | |
| 13496 | + | |
| 13497 | + | |
| 13498 | + | |
| 13499 | + | |
| 13500 | + | |
| 13501 | + | |
| 13502 | + | |
| 13503 | + | |
| 13504 | + | |
| 13505 | + | |
| 13506 | + | |
| 13507 | + | |
| 13508 | + | |
| 13509 | + | |
| 13510 | + | |
| 13511 | + | |
| 13512 | + | |
| 13513 | + | |
| 13514 | + | |
| 13515 | + | |
| 13516 | + | |
| 13517 | + | |
| 13518 | + | |
| 13519 | + | |
| 13520 | + | |
| 13521 | + | |
| 13522 | + | |
| 13523 | + | |
| 13524 | + | |
| 13525 | + | |
| 13526 | + | |
| 13527 | + | |
| 13528 | + | |
| 13529 | + | |
| 13530 | + | |
| 13531 | + | |
| 13532 | + | |
| 13533 | + | |
| 13534 | + | |
| 13535 | + | |
| 13536 | + | |
| 13537 | + | |
| 13538 | + | |
| 13539 | + | |
| 13540 | + | |
| 13541 | + | |
| 13542 | + | |
| 13543 | + | |
| 13544 | + | |
| 13545 | + | |
| 13546 | + | |
| 13547 | + | |
| 13548 | + | |
| 13549 | + | |
| 13550 | + | |
| 13551 | + | |
| 13552 | + | |
| 13553 | + | |
| 13554 | + | |
| 13555 | + | |
| 13556 | + | |
| 13557 | + | |
| 13558 | + | |
| 13559 | + | |
13488 | 13560 | | |
13489 | 13561 | | |
13490 | 13562 | | |
| |||
0 commit comments