Repository navigation
Commit 331613c
fix(plugin-security)!: the raw-SQL read scope (getReadFilter) answers the deny sentinel where the delegated prologue refuses (#25150)
Fixes #25106
Clause-②: no (narrowing) — on a walled posture, the security service's
raw-SQL read scope for an on-behalf-of context whose live active
organization the delegator's membership does not back is the deny
sentinel, where it used to be a composed scope.
Metadata-protocol: none
Seat call of record: `6328262579` on the card (option A, the provider
mirror). The claim `6328928374` carries `Clause-②: no`. This body adds
the `(narrowing)` arm and the BREAKING banner, following #25063's
precedent under the amended 「甲」 (the dispatch says to follow it and say
so); the level is `patch`, per ruling A's level axis
(`Metadata-protocol: none`). ⛔ Disclosure (#9387 ruling `5323310017`):
this body states the rule and an abstract reach only.
## The rule
ADR-0021 D-C makes the raw-SQL read scope the find path's scope, and
`getReadFilter`'s own doc promises agreement with the engine middleware.
Under `isolated` or `group`, the delegated-principal prologue
(`resolveOperationPrincipals`) refuses every operation of an
on-behalf-of context whose live active organization the delegator's own
membership does not back (ADR-0090 D10, landed with #25063).
`computeDelegatedReadFilter` mirrored the prologue's dangling-delegator
refusal but not that one, so for such a context the provider answered a
composed scope where the find path refuses.
`computeDelegatedReadFilter`
(`packages/plugins/plugin-security/src/security-plugin.ts`) now asks
`vetOrganizationClaim` once, on the resolved-delegator arm, right after
the dangling-delegator mirror and before the delegator's sets resolve:
the prologue's position. It passes the prologue's inputs (the live
organization, the delegator's memberships, the posture) and answers
`RLS_DENY_FILTER` itself, by reference, where the prologue refuses. The
refusal logs at `warn` (a refusal is not a fault), as the prologue's
does. No error code, no new rule text: `vetOrganizationClaim` stays the
one spelling of "a membership backs this organization". Nothing else in
the file moves: not the prologue, not `getReadFilter`, not
`canReadObject`.
## Reach, abstractly
In process only. No served door builds the shape (readings `6328225139`,
seat-verified in `6328262579`): the one producer of an on-behalf-of
context, the `/mcp` OAuth door, vets the organization against the same
membership, and the engine's own composition runs behind the prologue.
The population is an in-process caller that hands such a context to the
published provider, or to the analytics service.
## Boundary, before and after
- **Before:** under `isolated` or `group`, the provider answered a
composed read scope for that context, while the engine's find refused it
`403 PERMISSION_DENIED`.
- **After:** the provider answers the deny sentinel, so a door that
bypasses the engine reads zero rows, on every object (public, private
and organization-less alike: the rule is about the context).
- **Unchanged:** the `single` posture; an on-behalf-of context with no
active organization; an on-behalf-of context in an organization the
delegator belongs to; a context without `onBehalfOf`; a dangling
delegator (still the sentinel); system contexts.
## Pins
All in
`packages/plugins/plugin-security/src/delegated-organization-membership.test.ts`,
the rule's existing file (real `ObjectQL` from source, real `SqlDriver`,
real `SecurityPlugin`; the delegator rebuilt from stored rows). 17 new
cases; the file goes from 25 to 42.
- **Refusal (6):** `group` and `isolated` × a public, a private and an
organization-less object: `getReadFilter` answers
`toBe(RLS_DENY_FILTER)`.
- **Preservation (9):** per walled posture, an in-set delegated context,
the undelegated caller, a delegated context with no active organization
and a system context; plus the `single` posture. Each scope is pinned by
the rows it admits.
- **Parity (2):** per walled posture, a table over the contexts the
prologue decides × the three objects: find refused `403
PERMISSION_DENIED` ⇔ the provider's deny sentinel, and where find
admits, the scope admits exactly the rows find returns. The table
asserts its refused set, so neither side is vacuous.
One-time proofs (not left as tests):
- **Before/after answers.** The provider's answer, the rows it admits
and the find outcome were recorded for 45 cells (three postures × five
context kinds × three objects) on the unmodified provider and again with
the fix. 39 answers are byte-identical (scope JSON and admitted rows).
The 6 that changed are exactly the refusal cells, each of which find
refuses.
- **Ablation.** With the fix committed, `scripts/ablation-replace.mjs`
replaced the `vetOrganizationClaim` ask with `false` (anchor x1 → x0,
blob `130693ab6579` → `447156f4c27f`, marker on disk x1), and the file
ran: `Tests 8 failed | 34 passed (42)`. The 8 are the 6 refusal cells
and the 2 parity cells; every preservation cell and all 25 prologue
cells stayed green. Restored by the tool: blob after restore = HEAD blob
`130693ab6579`, `git diff HEAD` empty (re-read independently: `git
status --porcelain` empty, `git hash-object` = `HEAD:` blob). The
subject is imported from source (`./security-plugin.js`), so there is no
`dist/` leg.
- **Red first.** Before the fix, the 6 refusal cells were red and every
other case green (`Tests 6 failed | 28 passed`, with the temporary
measuring cell).
The six existing `plugin-security` files the readings flagged as
building delegated contexts through `getReadFilter` under a walled
posture all stay green: their delegated cells are dangling-delegator or
`single`-posture cases, or reach the prologue directly. No pin needed
flipping.
## Tests and gates
Read on head `03a9717ca` (the fix `58623eceb` plus a clean merge of
`origin/main` `459afea1f`, which moved `packages/lint` and three
driver-sql test files only; `@objectstack/lint` was rebuilt after it).
Every run went through `scripts/pm/os-verify-lock.sh`; each exit code
was captured before any pipe.
- **Dependency closure (①):** `pnpm --workspace-concurrency=2 --filter
'@objectstack/plugin-security^...' build` (19 packages): `VERDICT
command-exit 0`.
- **The package (②):** `pnpm --filter @objectstack/plugin-security exec
vitest run --maxWorkers=2`: `Test Files 207 passed (207)`, `Tests 4253
passed | 45 skipped (4298)`. `pnpm --filter @objectstack/plugin-security
typecheck`: exit 0 (`check:test-typecheck: OK`; the new cases' file is
in `tsconfig.test.json`'s program, counted with `--listFiles`). Read on
`58623eceb`; on `03a9717ca` the rule's file and the six flagged files
re-ran: `Test Files 7 passed (7)`, `Tests 340 passed (340)`.
- **The derived gate families (③):** `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` on `03a9717ca` derived 69
commands; 67 exit 0. Two exit 3, `PREREQUISITE NOT MET`, NOT MEASURED
locally: `pnpm check:dual-build-cjs-loads` (needs every workspace
package's `dist/`) and `pnpm check:i18n` (needs the built CLI and the
build closures of nine packages). This diff changes no translation key,
label or build output shape; CI's required `Lint & Repo Gates` runs
both. `--ran` reconciliation: `69 derived, 67 run, 2 NOT-MEASURED, 0
UNRUN`, exit 0.
- **Not run locally, declared to CI:** `test:repo` (the diff changes
none of its tests), the type-check lanes and the path-scheduled CI jobs
`dispatch-gates` names.
- **Full dogfood battery:** owed on this head (the title carries `!`);
its reading is reported in the `os-dev-report` comment on the card.
## Acceptance notes
- **`canReadObject` does not vet the organization** (#25107 carries it,
and #25107 remains open). After this change, an in-process analytics
read of the refused shape is admitted at the object level and then
answers zero rows through this scope, not a `403`.
- **`plugin-audit`'s `parent-record-read-gate.ts` header is stale:** it
still says the security service's read scope does not carry the
delegator intersection, which `getReadFilter`'s doc and the engine's
ADR-0139 D5.1 (as amended) contradict. The routing it describes stays
safe. Documentation drift, outside this claim's surface; no carrier.
- **The parity table covers the contexts the prologue decides.** An
object-admission refusal (the D10 grant intersection) is
`canReadObject`'s half, not the row scope's, for any caller; the
preservation cell for a context with no active organization leaves the
private object out for that reason.
- **Observed, not investigated:** under a walled posture, the engine's
find under a system context that carries an active organization returned
only that organization's rows in this fixture, while the provider
answers no scope for any system context. That is what its doc promises
(it mirrors the middleware's system skip); noted only.
- A review at `CONTRACT_REVIEW_TIER` is owed before the queue (the seat
arranges it).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UnHcTADjeg3VwxF21QHj7W)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 2aaee3b commit 331613c
3 files changed
Lines changed: 192 additions & 2 deletions
File tree
- .changeset
- packages/plugins/plugin-security/src
| 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 | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
42 | 53 | | |
43 | 54 | | |
44 | 55 | | |
45 | 56 | | |
46 | 57 | | |
47 | 58 | | |
| 59 | + | |
48 | 60 | | |
49 | 61 | | |
50 | 62 | | |
| |||
65 | 77 | | |
66 | 78 | | |
67 | 79 | | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
68 | 83 | | |
69 | 84 | | |
70 | 85 | | |
| |||
201 | 216 | | |
202 | 217 | | |
203 | 218 | | |
204 | | - | |
| 219 | + | |
205 | 220 | | |
206 | 221 | | |
207 | 222 | | |
| |||
305 | 320 | | |
306 | 321 | | |
307 | 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 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6081 | 6081 | | |
6082 | 6082 | | |
6083 | 6083 | | |
6084 | | - | |
| 6084 | + | |
| 6085 | + | |
| 6086 | + | |
| 6087 | + | |
6085 | 6088 | | |
6086 | 6089 | | |
6087 | 6090 | | |
| |||
6106 | 6109 | | |
6107 | 6110 | | |
6108 | 6111 | | |
| 6112 | + | |
| 6113 | + | |
| 6114 | + | |
| 6115 | + | |
| 6116 | + | |
| 6117 | + | |
| 6118 | + | |
| 6119 | + | |
| 6120 | + | |
| 6121 | + | |
| 6122 | + | |
| 6123 | + | |
| 6124 | + | |
| 6125 | + | |
| 6126 | + | |
| 6127 | + | |
| 6128 | + | |
| 6129 | + | |
| 6130 | + | |
| 6131 | + | |
| 6132 | + | |
6109 | 6133 | | |
6110 | 6134 | | |
6111 | 6135 | | |
| |||
0 commit comments