Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-security): bring organization_admin_no_bypass under registry-driven managed-write denies - #14129
Conversation
…ry-driven managed-write denies (#14029) The derived wall-less org-admin variant holds a write-granting '*' wildcard but was not in MANAGED_DENY_TARGET_SETS, so applyManagedWriteDenies walked it and skipped it at kernel:ready — and because deriveWallLessOrgAdmin takes a shallow copy at module load, injections into the parent could never propagate either. Add the variant to the target list, and replace the self-referential membership pin with one that derives the required floor (write-granting wildcard sets) from the real seeded sets and diffs it against the list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 14 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 b2de742f7544ae107161ddca367ff9636ee14e50 && git checkout b2de742f7544ae107161ddca367ff9636ee14e50
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bf3bbf13b9be4f3996726a239cfe5642395c2396 8d5cca2a8908c8154efa4abf19f2c18a50baa9a9 && git checkout -B drift-repro bf3bbf13b9be4f3996726a239cfe5642395c2396 && git merge --no-ff 8d5cca2a8908c8154efa4abf19f2c18a50baa9a9
node scripts/docs-audit/affected-docs.mjs --json bf3bbf13b9be4f3996726a239cfe5642395c2396 |
os-steve
commented
Sep 1, 2026
契约复审:FAIL —— 逐字采纳,标签保留,返工已派在契约复审档位转录核验(采信前必做): ⇒ 每轮在档、零回退 ⇒ 裁决合法,取逐字采纳。以下为复审席原文,⛔ 未改写、未删节、未润色: 契约复审:FAIL结论一行增补与对照均实;楼层滤器漏 modifyAllRecords 型通配集,补一行判定即清。 阻塞项
非阻塞
我核了什么 / 我没核什么核了(源码与 diff 逐行):
没核(NOT MEASURED,由谁测):
PM 侧处置
⭐ 这条阻塞项值得单独记一笔它是本卡要消灭的那个缺陷类的更小一号形态,而且只有读了评估器的写语义才看得见 —— 只看通配上的三个 CRUD 位是看不出来的。复审席自己把这层说穿了:新 docblock 承诺「a future write-granting set that is not added here fails a pin」,而按当前滤器,一个
Generated by Claude Code |
…write route (contract-review blocking item)
The independent-property floor in default-permission-sets.test.ts recognised
only the three CRUD flags, but the evaluator grants writes by a second route:
MODIFY_ALL_WRITE_KEYS + objPerm.modifyAllRecords (permission-evaluator.ts:224)
covers allowEdit, allowDelete and the destructive class. A future default set
shaped '*': { allowRead: true, modifyAllRecords: true } therefore held
edit/delete on every future identity table in the evaluator's own terms, yet
had all three CRUD flags false, escaped the floor and tripped no pin —
contradicting the MANAGED_DENY_TARGET_SETS docblock promise that a
write-granting set not listed there fails a pin. Same "a set that should be a
member can hide" class this card exists to kill, one size smaller.
- default-permission-sets.test.ts: add `|| wc.modifyAllRecords === true` to
the floor filter. Value test (`=== true`) kept deliberately: Zod
materialises both superuser bits with .default(false)
(permission.zod.ts:268), so they are present-as-false and a key-existence
test would misfire today.
- managed-object-write-denies.ts: align the docblock — the floor is "grants
any generic write class via the three write flags OR modifyAllRecords",
not "grants create/edit/delete".
- .changeset: the static baseline covers 28 managed tables
(BETTER_AUTH_MANAGED_OBJECTS), not 30 (non-blocking item).
Zero behaviour delta on today's tree: organization_admin and
admin_full_access carry all three CRUD flags true, so the derived floor set
is unchanged. Measured: with a temporary '*': { allowRead: true,
modifyAllRecords: true } probe set absent from MANAGED_DENY_TARGET_SETS, the
widened pin goes red naming exactly the probe; the old three-flag filter
stays green on the same probe (the blind spot). Probe removed; restore
proven by blob hash against HEAD. plugin-security suite 94 files / 1770
tests green; all three tsc programs green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMsos-steve
commented
Sep 1, 2026
Rework for the contract-review FAIL — blocking item 1 + non-blocking 1, at |
os-steve
commented
Sep 1, 2026
契约复审(返工复核):PASS —— 逐字采纳,双载体标签已清转录核验: ⇒ 每轮在档、零回退 ⇒ 逐字采纳。以下为复审席原文,⛔ 未改写、未删节、未润色: 契约复审(返工复核):PASS结论一行阻塞项与计数均实清, 非阻塞
我核了什么 / 我没核什么核了(全部源码与 diff 逐行,
没核(NOT MEASURED,由谁测):
PM 侧收口
⭐ 非阻塞项已立卡,⛔ 不夹带进本 PR复审席的裁量很准,本席原样采纳:本轮 docblock 把楼层改成了穷举式定义("三写位 OR ⇒ 但残余是真的,本席已独立复核: ⇒ 已另立卡承载,⛔ 不塞进这个 PR。这是同一缺陷类的第三次缩小:先是「集合不在表里」,再是「楼层漏 Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
…grant route — allowTransfer (#14137) The independent-property floor (#14029) read the three CRUD write flags and modifyAllRecords, missing the evaluator's FIRST grant route: the direct bit read off OPERATION_TO_PERMISSION (transfer: 'allowTransfer'), ENFORCED today through the insert/update owner_id door (#3004). A transfer-only wildcard default set would never have been required to be a managed-deny target. Extend the floor with a value test (=== true, never key-existence — the bits are present-as-false via .default(false), #14129 first review), sync both exhaustive docblocks to name the route, and pin: positive transfer-only shape, read-only reverse control, dedicated present-as-false pin, and a zero-delta invariance sweep over the seeded sets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
Fixes#14029
Clause-②: yes — content limb. Bringing a permission set under managed-write-deny injection changes accept/reject behaviour:
organization_admin_no_bypasswill be refused generic create/edit/delete on any future registry-declaredmanagedBy: 'better-auth'object that the static baseline misses, writes its wildcard grants today. Surface touched:MANAGED_DENY_TARGET_SETSinpackages/plugins/plugin-security/src/managed-object-write-denies.ts(thekernel:readyinjection target list) plus the membership pins. No spec/Zod schema, API shape, or wire format changes; on today's tree the seeded permission sets are byte-identical (the staticBETTER_AUTH_MANAGED_OBJECTSbaseline already covers all 28 declared managed tables, and the variant carries it via the derivation copy).The gap
MANAGED_DENY_TARGET_SETSnamed four sets andapplyManagedWriteDeniesmatches exactly, so atkernel:readythe injection walked the derivedorganization_admin_no_bypassvariant and skipped it. The variant is a shallow copy oforganization_admintaken at module load (deriveWallLessOrgAdminstrips only theviewAllRecords/modifyAllRecordssuperuser bits), which means (a) its'*'wildcard still grants create/edit/delete, and (b) entries injected into the parent'sobjectscan never propagate to it.deriveWallLessOrgAdmin's own docblock declares "managed-write denies … carried over verbatim" and "the only intended delta is the superuser bits" — declared-vs-enforced, so the implementation is what gets fixed.Premise verification (the card was a source reading)
Measured by executing the real modules (temporary vitest probe, 4/4 green pre-fix, then removed):
defaultPermissionSets(the arraybootstrapPermissionSetsdefaults to andrunBootstraphands toapplyManagedWriteDeniesat thekernel:readyhook, security-plugin.ts line 3612). Applying a fakemanagedBy: 'better-auth'schema outside the static list yieldedapplied: 4— parent injected, variant untouched.objects['*']carriesallowCreate/allowEdit/allowDeletealltrue. One immaterial delta from the card's wording: afterPermissionSetSchema.parsethe stripped superuser bits are present-as-false(Zod boolean defaults), not absent — the effect is identical.objectsafter derivation does not reach the variant.Both halves
ORGANIZATION_ADMIN_NO_BYPASSis now a member ofMANAGED_DENY_TARGET_SETS. Docblocks updated on the module, the list, andderiveWallLessOrgAdmin(the derivation now states that the registry union reaches the variant by direct membership, not by the copy).default-permission-sets.test.tsderives the required floor from an independent property — default sets whoseobjects['*']wildcard grants any generic write class, read from the real seededdefaultPermissionSets— and diffs it againstMANAGED_DENY_TARGET_SETS; a non-empty difference is red. The one documented exclusion (admin_full_access, admin rescue path) is pinned exactly, likeEDIT_EXCEPTIONS. A liveness guard pins the property derivation itself so an empty probe cannot pass vacuously.Verification (all at
7e63b8ddc, clean tree)managedBy: 'better-auth'object outside the static baseline now reaches the variant — new test injectssys_future_identity_tableinto a structuredClone of the real seeded sets and asserts the deny entry lands onorganization_admin_no_bypass(and still on the parent).ORGANIZATION_ADMIN_NO_BYPASSwas removed from the list on disk (marker count 1 to 0, blob4d06d8f…to16abaa7…, both against the HEAD blob). Under that mutation: the OLD self-referential pin ("each write-granting target set denies create/edit/delete on every managed object") stayed green — the blind spot — while the NEW independent-property pin went red naming exactly['organization_admin_no_bypass']; the positive test, reverse control, and both exact-membership tests also went red (5 failed / 26 passed, every direction as predicted). Restore proven by state: blob hash equal to the HEAD blob,git diff HEADempty, marker count 1. The mutation reaches the run via in-package relative imports under vitest (source resolution — no dist leg to rebuild for these suites).'*'wildcard, thesys_positionRBAC read-only block) survives unchanged; only the future table is added.admin_full_accessuntouched by the injection (deep-equal before/after) and still outside the list (pinned); the four existing target sets keep their existing pins unchanged and green — the fix adds a fifth target, it does not move the four.@objectstack/plugin-securityfull suite 94 files / 1770 tests green;typecheck(all three tsc programs) green;tsc --listFilesshows all four edited files inside the compiled programs (sources in main+test, tests in the test program).node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no path args), both sections read whole — 36 harvested families run locally, 35 exit 0 (includingcheck:type-check-debtre-measure,check:i18n,check:engine-double-contract,check:where-matcher);scripts/check-test-completeness.mjsexited 3 = PREREQUISITE NOT MET = NOT MEASURED (it parses CI shard summaries that do not exist locally; CI's Test Core shards own it). Full-repopnpm lint(eslint, no-inline-config) exit 0. All exit codes captured before any pipe.Release note
.changeset/no-bypass-managed-deny-target.md— patch on@objectstack/plugin-security.Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
Generated by Claude Code
Generated by Claude Code