Repository navigation
fix(plugin-security)!: an external object that states no organization scope reads as empty for every caller, the platform owner included - #30146
Conversation
… scope reads as empty for every caller, the platform owner included Under a walled tenancy posture, the Layer 0 deny an external object earns by stating no organization scope (ADR-0015 §10) is a verdict on the object's configuration, not a filter on the reader's organization. The verified platform owner's read crossing still lifts the organization filter and the org-less deny everywhere else; it no longer lifts this one, and the recorded verdict stays `deny`. Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…e reads as empty for every caller Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…eady-registered) Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 17 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 1b34afcc82cbf730a4298b8e01f32f3705e88a26 && git checkout 1b34afcc82cbf730a4298b8e01f32f3705e88a26
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55337f4ce35dbe62434b23517547a0b5052b936c 471f23549959eeb65866de78dc14eaf577262e68 && git checkout -B drift-repro 55337f4ce35dbe62434b23517547a0b5052b936c && git merge --no-ff 471f23549959eeb65866de78dc14eaf577262e68
node scripts/docs-audit/affected-docs.mjs --json 55337f4ce35dbe62434b23517547a0b5052b936c
|
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract-review-claim: session_01QSeKYn5CmmdShCJifm6WYj · head 471f235 · 2026-11-04T16:33Z |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract reviewServed-tier: Inputs: the PR body, its file list, the net diff against the merge base Check-runs on the head: every check is completed and green or an expected skip; none is still running. ① Derived judgments
② Semver level
The dev's open question, A or B: A — keep ③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj 席位核对ACCEPT · head
|
Clause-②: no (narrowing) — under the
isolatedandgrouppostures, an external object registered after the boot that states no organization scope reads as empty for every caller, the declared platform owner included; nothing is widened, and thesingleposture is unchanged.Metadata-protocol: none
Skills-docs: none — the published texts already state this rule for every caller, and this diff changes none of them: the ADR-0087 D3 entry
external-object-organization-scope-declared("every read is denied at Layer 0"), and the once-warning inexternal-organization-scope.ts. The ADR-0015 §10 amendment that states it is in #29866 (Tier H, still open).维护者速读
isolated/group姿态下,启动后才注册、且未声明组织范围的外部对象,对每个调用者读取都为空,平台所有者也不例外;平台所有者在其它对象上跨组织墙读取的行为不变,成员与single姿态不变。main上已有的文案(D3 条目、一次性告警)本就这样写;ADR-0015 §10 的修订在 docs(adr): under a walled tenancy posture every external object states its organization scope (ADR-0015 §10) #29866(Tier H,未合并)。packages/plugins/plugin-security/src/security-plugin-rls-filter.ts跨墙分支前新增的条件deniedForUndeclaredExternalScope;②packages/plugins/plugin-security/src/platform-owner-wall-bypass.test.ts新增的顶层 describe;③packages/runtime/src/plugin-driver-external-object.spike.test.ts的 (h5c)。pnpm --filter @objectstack/plugin-security exec vitest run src/platform-owner-wall-bypass.test.ts,预期 26 个用例全部通过。What changes
denyan external object earns by stating no organization scope judges the object's configuration, not the reader's organization, so it stands for every caller, the verified platform owner included. IncomputeLayeredRlsFilterthe verified platform owner's read crossing (theOS_PLATFORM_OWNER_EMAILread door) now stands down on exactly that verdict, and the owner probe is not consulted there. The recorded verdict staysdeny.isolatedequality, thegroupunion and the org-less deny, on local objects and on an external object that declares its ownorganization_id. Writes stay walled for everyone. Members and thesingleposture are unchanged. No type, verdict ladder or published export moves.patchchangeset on@objectstack/plugin-security, titledfix(plugin-security)!:, with a BREAKING section (the rule, what is unchanged, who moves and the fix) and the ADR-0087 dispositionnot-required (already-registered external-object-organization-scope-declared): the existing entry already prescribes the operator's remedy.Why
Security-channel ruling of 2026-11-04 (option A): the configuration-defect deny on an external object that states no organization scope stands for the verified platform owner too. The texts already on
mainsay so: the D3 entry's "every read is denied at Layer 0", and the once-warning naming "denies every read and write". The ADR-0015 §10 amendment lands separately in #29866.The declarations, justified
Clause-②: no (narrowing)and the!. Measured against the published release,@objectstack/plugin-security@18.3.0(npmlatest): itsdist/index.jscarries no undeclared-external-scope verdict (0 hits forexternalOrganizationScopeUndeclared, against 1 hit for the owner crossing's audit event name as the positive control in the same file). So this diff narrows one principal's reads relative to the published release, on a configuration the boot already refuses by name when the stack declares it. Nothing is widened. It is a runtime security narrowing, not an accept-set widening, sono.Metadata-protocol: none. Nothing an author can write changes; the three declarations are the onesmainalready accepts. The level follows the kind:patch.Tests
bootgains an optionalschema):[ADR-0015 §10] an external object that states no organization scope reads as empty for every caller, the platform owner included, 7 cases.isolated: the verified owner reads the deny sentinel (by identity), the recorded verdict isdeny, no crossing is logged, and nosys_userrow is read.group: the same.organization_id, and a member there keeps the equality.isolatedandgroup.singleis unchanged for the owner and a member.plugin-driver-external-object.spike.test.ts(h5c), beside (h5)/(h5b), on the realisolatedboot: the owner'sGET /data/spike_lateanswers 200 with no row, and no read without the deny sentinel reaches the remote. CONTROL in the same case: the owner'sGET /data/spike_walledstill crosses the wall and reads every organization's rows.trueand plugin-security rebuilt, the 3 owner cases above and (h5c) went red and every other case stayed green; restored and rebuilt, all green. Readings below.Acceptance notes
Verification
All readings are at or source-equal to head
471f235499:packages/is byte-identical from111074d2f6(the fix commit) to471f235499, and the later commits touch only the changeset and merge one skills-onlymaincommit. Heavy runs went throughscripts/pm/os-verify-lock.sh, and each reading is that wrapper'sVERDICTline.Unit: the crossing's pins and the federated Layer 0 pins (34/34)
Full plugin-security suite and typechecks
The first runtime typecheck exited 1 at
check:test-typecheckonorganization-ownership-boot-guard.turso-remote.kernel.test.ts, a file this diff does not touch. Its import@objectstack/driver-tursois an optional peer that the runtime closure build does not build, so it had nodist/here. Afterturbo run build --filter=@objectstack/driver-turso, the same typecheck passed (one&&chain,VERDICT command-exit 0).Spike harness: the real isolated boot (20/20)
The spike resolves
@objectstack/plugin-securitythrough itsdist/, so plugin-security was rebuilt first. The new condition's text is indist/index.jsanddist/index.mjs.One-shot ablation (not committed)
scripts/ablation-replace.mjsin WRAP mode, under the lock. The anchor is the new condition insecurity-plugin-rls-filter.ts(1 hit, replaced bytrue). Blobf1bb2ff4e4became099c25dc90.ablation-dist-preflight.mjs --absentpassed (marker absent from all 6 built files; the source marker was lost).3 failed | 23 passed (26). Theisolated,groupand org-less owner cases went red.1 failed | 19 passed (20). (h5c) went red.f1bb2ff4e4),git diff HEADis empty, and the whole-treegit status --porcelainis clean. After the rebuild, the present-mode preflight passed (marker in 2 built files) and the suites read 26/26 and 20/20.Derived gate families: 60 derived, 57 run, 3 NOT MEASURED, 0 unrun
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 60 commands off merge base987ff33b8. Each command ran with its exit captured before any pipe, and--ranreconciled the records:471f235499, all exit 0. Among them,check-adr-0087-registrationreads:[BREAKING+bang+clause-②-narrowing] not-required (already-registered).check-comment-mask-corpus(exit 0) is the repo-wide comment-mask sweep. It was taken at0135956739, whose sources equal471f235499's.Lint & Repo Gates:check:dual-build-cjs-loads: PREREQUISITE NOT MET, because it needs every package'sdist/.check:i18n: PREREQUISITE NOT MET, because it needs the workspace CLI build.check:type-check-debt: its closure build never got the shared lock (queue-timeout). The two touched packages' own typechecks, both exit 0, give the narrower reading.Full dogfood battery (the title carries a bang)
test:repovitest project (Test Coreruns it on the PR and in the merge group). Neither changed test file is in arepoproject list.