Repository navigation
fix(plugin-security)!: the raw-SQL read scope (getReadFilter) answers the deny sentinel where the delegated prologue refuses - #25150
Conversation
…el where the delegated prologue refuses computeDelegatedReadFilter now asks vetOrganizationClaim once on the resolved-delegator arm, with the inputs the operation prologue passes, right after the dangling-delegator mirror and before the delegator's sets resolve. On a walled posture, a delegated context whose live active organization the delegator's own membership does not back answers RLS_DENY_FILTER itself, as the find path refuses it (ADR-0090 D10, ADR-0021 D-C). The single posture, a context with no active organization, an in-set delegated context, an undelegated caller and a system context keep their answers. Pins in delegated-organization-membership.test.ts: refusal cells under both walled postures on a public, a private and an organization-less object; preservation cells; and a parity cell against the engine find. Claude-Session: https://claude.ai/code/session_01UnHcTADjeg3VwxF21QHj7W Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 09dffc6d0108cd9935d5445efe8215e4ee5da84c && git checkout 09dffc6d0108cd9935d5445efe8215e4ee5da84c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 459afea1f69ff69422de2a7a2c7674b6f7ea1aca 03a9717ca74695f7f9b8ea238fdae69611d7dfc3 && git checkout -B drift-repro 459afea1f69ff69422de2a7a2c7674b6f7ea1aca && git merge --no-ff 03a9717ca74695f7f9b8ea238fdae69611d7dfc3
node scripts/docs-audit/affected-docs.mjs --json 459afea1f69ff69422de2a7a2c7674b6f7ea1aca
|
Contract reviewServed-tier: Rendered at 2026-10-22T01:18Z on card #25106's body and all eight comments, PR #25150's body, file list and net diff against ① Derived judgments(a) The one accept-set change, confined to (b) The refused cells are exactly the prologue's, under both walled postures: right. Read side by side, the new branch and the prologue's branch ( (c) Every other cell is unchanged, by construction: right. (d) The pins are the right cells and are not vacuous: right. Six refusal cells (two walled postures × three objects) assert the sentinel by identity. Nine preservation cells (per walled posture: in-set delegated, undelegated, no active organization, system; plus (e) One bound on the prose, not the code. "No served door builds the shape" holds at the shipped default; as the precedent record on PR #25101 noted, with the grants cache on (a non-default configuration) the ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对
Changeset sentences checked against the diff:
All hold. Generated by Claude Code |
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:
6328262579on the card (option A, the provider mirror). The claim6328928374carriesClause-②: 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 ispatch, per ruling A's level axis (Metadata-protocol: none). ⛔ Disclosure (#9387 ruling5323310017): 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. Underisolatedorgroup, 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).computeDelegatedReadFiltermirrored 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 asksvetOrganizationClaimonce, 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 answersRLS_DENY_FILTERitself, by reference, where the prologue refuses. The refusal logs atwarn(a refusal is not a fault), as the prologue's does. No error code, no new rule text:vetOrganizationClaimstays the one spelling of "a membership backs this organization". Nothing else in the file moves: not the prologue, notgetReadFilter, notcanReadObject.Reach, abstractly
In process only. No served door builds the shape (readings
6328225139, seat-verified in6328262579): the one producer of an on-behalf-of context, the/mcpOAuth 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
isolatedorgroup, the provider answered a composed read scope for that context, while the engine's find refused it403 PERMISSION_DENIED.singleposture; an on-behalf-of context with no active organization; an on-behalf-of context in an organization the delegator belongs to; a context withoutonBehalfOf; 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 (realObjectQLfrom source, realSqlDriver, realSecurityPlugin; the delegator rebuilt from stored rows). 17 new cases; the file goes from 25 to 42.groupandisolated× a public, a private and an organization-less object:getReadFilteranswerstoBe(RLS_DENY_FILTER).singleposture. Each scope is pinned by the rows it admits.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):
scripts/ablation-replace.mjsreplaced thevetOrganizationClaimask withfalse(anchor x1 → x0, blob130693ab6579→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 blob130693ab6579,git diff HEADempty (re-read independently:git status --porcelainempty,git hash-object=HEAD:blob). The subject is imported from source (./security-plugin.js), so there is nodist/leg.Tests 6 failed | 28 passed, with the temporary measuring cell).The six existing
plugin-securityfiles the readings flagged as building delegated contexts throughgetReadFilterunder a walled posture all stay green: their delegated cells are dangling-delegator orsingle-posture cases, or reach the prologue directly. No pin needed flipping.Tests and gates
Read on head
03a9717ca(the fix58623ecebplus a clean merge oforigin/main459afea1f, which movedpackages/lintand three driver-sql test files only;@objectstack/lintwas rebuilt after it). Every run went throughscripts/pm/os-verify-lock.sh; each exit code was captured before any pipe.pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-security^...' build(19 packages):VERDICT command-exit 0.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 intsconfig.test.json's program, counted with--listFiles). Read on58623eceb; on03a9717cathe rule's file and the six flagged files re-ran:Test Files 7 passed (7),Tests 340 passed (340).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon03a9717caderived 69 commands; 67 exit 0. Two exit 3,PREREQUISITE NOT MET, NOT MEASURED locally:pnpm check:dual-build-cjs-loads(needs every workspace package'sdist/) andpnpm 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 requiredLint & Repo Gatesruns both.--ranreconciliation:69 derived, 67 run, 2 NOT-MEASURED, 0 UNRUN, exit 0.test:repo(the diff changes none of its tests), the type-check lanes and the path-scheduled CI jobsdispatch-gatesnames.!); its reading is reported in theos-dev-reportcomment on the card.Acceptance notes
canReadObjectdoes not vet the organization (plugin-security: delegation probes and diagnostics (canReadObject / canWriteObject / canExport, field mask, explain) answer for an on-behalf-of context the operation prologue now refuses (PR #25101) #25107 carries it, and plugin-security: delegation probes and diagnostics (canReadObject / canWriteObject / canExport, field mask, explain) answer for an on-behalf-of context the operation prologue now refuses (PR #25101) #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 a403.plugin-audit'sparent-record-read-gate.tsheader is stale: it still says the security service's read scope does not carry the delegator intersection, whichgetReadFilter'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.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.CONTRACT_REVIEW_TIERis owed before the queue (the seat arranges it).Generated by Claude Code