Repository navigation
fix(plugin-security): an organization administrator's refused assignment write answers the same for a row it cannot read as for a missing one - #29932
Conversation
…n the caller's own read An organization administrator's refused assignment write answers the same for a row it cannot read as for a missing one (ADR-0090 D12; ruling of record 5985885287): the organization branch of the delegated-administration gate reads the row a removal addresses under the caller's own context, and a row that read does not return is refused exactly as an id naming no row is. A row the caller reads keeps the boundary guidance. Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…t write at the HTTP door Behind a real organization wall, an organization administrator's PATCH and DELETE of an assignment row it cannot read answer exactly what an id naming no row answers; a row it reads keeps the boundary guidance. Adds the patch changeset for plugin-security. Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…ad contexts Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
… options erasure The posture rig's caller reads keep the engine query options typed, so the test surface of the query-options erasure ratchet stays at its ceiling. Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…ares Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
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 a75819f70b5f3159cc9aeec788129b3d2655dbf1 && git checkout a75819f70b5f3159cc9aeec788129b3d2655dbf1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7b2bb668dacc8d915c161791facf20be78138a99 51d9481a50435663746886fd79a8bca4aaed02f7 && git checkout -B drift-repro 7b2bb668dacc8d915c161791facf20be78138a99 && git merge --no-ff 51d9481a50435663746886fd79a8bca4aaed02f7
node scripts/docs-audit/affected-docs.mjs --json 7b2bb668dacc8d915c161791facf20be78138a99
|
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract-review-claim: session_01QSeKYn5CmmdShCJifm6WYj · head 51d9481 · 2026-11-03T13:43Z |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract reviewServed-tier: Reviewed on the net diff against ① Derived judgmentsWhat changes, and for whom. One thing: in the ORGANIZATION branch of the ADR-0090 D12 gate, the pre-image of a by-id removal is now the caller's own by-id read through the engine (
② Semver level
③ Boundary flags
④ Design judgmentTried to break it against the cited code; no must-fix found.
Implemented-by: VERDICT: PASS |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj 席位核对ACCEPT · head
|
Clause-②: no
Metadata-protocol: none
维护者速读
organization_admin/organization_admin_no_bypass)按 id 删除或修改授权行(sys_user_position、sys_user_permission_set)被拒时,对它读不到的行和根本不存在的 id 给出完全相同的回答(状态码、错误码、文案、details 全同)。委派管理闸门的组织分支改为以调用者自己的读取来判断要删除的那一行。single姿态下的行)判定与提示不变,原本放行的删除仍放行,租户级管理员不经过此分支。回滚即撤销本 PR。packages/plugins/plugin-security/src/delegated-admin-gate.ts的organizationTargets与organizationAdminFailure中空前像的拒绝;org-admin-assignment-boundary.posture.test.ts末尾新增的 describe。pnpm --filter @objectstack/plugin-security exec vitest run src/org-admin-assignment-boundary.posture.test.ts,预期全部通过(含group/isolated两个姿态下新增的用例)。What changes
DelegatedAdminGate.organizationAdminFailure(ADR-0090 D12, amended 2026-10-17) now takes its targets fromorganizationTargets: an insert's payload rows, or the one row a by-id removal names, read through the engine under the caller's own context.403 PERMISSION_DENIED, "the assignment row to remove could not be read", with the same details. That refusal stays at the gate and never reaches the engine.@objectstack/plugin-securitypatch.Tests
ObjectQL+ SQL driver +SecurityPlugin(org-admin-assignment-boundary.posture.test.ts, new top-level describe):groupandisolated, for both administrator variants and both tables, the update and the delete of a row the caller cannot read (precondition: the caller's own read returns nothing) answer exactly what the same write answers for an id naming no row, compared as a whole (status, code, sentence, details), and the row stays;single: a row of another organization that the caller reads keeps the guidance naming its organization.packages/qa/dogfood/test/org-admin-assignment-removal-unreadable-row.dogfood.test.ts, showcase behind a realisolatedwall):PATCHandDELETEof two assignment rows the organization owner cannot read answer exactly what an id naming no row answers; a row it reads keeps the guidance.organizationTargetscall swapped back for the target walk it replaced (node scripts/ablation-replace.mjs, anchor hit 1 time, blob 589c61df → a91f1b91):org-admin-assignment-boundary.posture.test.ts2 failed | 63 passed (the two walled postures' new case,orgAdmin delete sys_user_position: the unreadable row answers as no row); every control stayed green. Restored blob == HEAD blob 589c61df andgit diff HEADempty; the restored run 65/65.Acceptance notes
delegated-admin-gate.test.ts's organization harness reads through a fakeqlthat ignores context, so its foreign-row case stands for a caller who reads the row, and its expectation (the guidance naming the organization) is unchanged, as thesinglecontrol pins on the real engine.Verification
Final head 51d9481 (main merged at b6fe08a, clean)
pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 src/org-admin-assignment-boundary.posture.test.ts src/delegated-admin-gate.test.ts: 2 files, 202/202.pnpm --filter @objectstack/plugin-security typecheck: exit 0 (tsc, scripts tsc, test layer 0 errors).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives: 64 commands, each exit 0, captured before any pipe;--ranreconciliation: 64 derived, 64 run, 0 NOT-MEASURED.check:dual-build-cjs-loadsandcheck:type-check-debtran with the lock held and the dist they read built.At b6fe08a (the main merge; the two later commits change only the posture test's caller read)
pnpm turbo run build --filter=@objectstack/dogfood --concurrency=2: exit 0.pnpm --filter @objectstack/spec check:generated: exit 0.pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2(full suite): 241 files, 5225 passed, 45 skipped.pnpm --filter @objectstack/dogfood exec vitest run --project isolated test/org-admin-assignment-removal-unreadable-row.dogfood.test.ts: 3/3.pnpm --filter @objectstack/dogfood typecheck: exit 0.pnpm --filter @objectstack/dogfood test(full battery, once): 413 files passed, 3 skipped; 3415 tests passed, 28 skipped.Generated by Claude Code