Repository navigation
Description
Found while correcting the managed-object-write-denies.ts docblock for #13822
(PR #14028). Comment-only card; this is a behaviour observation deliberately
left out of that PR and filed instead. Read from source on origin/main at
c54d4d3d7; not reproduced against a running kernel.
The gap
MANAGED_DENY_TARGET_SETS is an exact-match allowlist of four names, and
applyManagedWriteDenies matches on it exactly:
const targets = new Set(MANAGED_DENY_TARGET_SETS);
...
if (!set || !targets.has((set as { name?: string }).name ?? '')) continue;
organization_admin_no_bypass is not one of the four. It nevertheless holds a
write-granting wildcard: deriveWallLessOrgAdmin (end of
objects/default-permission-sets.ts) copies organization_admin and removes
only the two superuser bits —
const { viewAllRecords: _v, modifyAllRecords: _m, ...wildcardWithoutBypass } =
(objects['*'] ?? {}) as ...; // Record cast elided: angle brackets do not
// survive GitHub's body sanitizer
objects['*'] = wildcardWithoutBypass;
— so allowCreate / allowEdit / allowDelete stay true on its '*'.
The derived variant IS in the array the injection walks: defaultPermissionSets
inserts it directly after its parent, and SecurityPlugin's
bootstrapPermissionSets defaults to that array
(options.defaultPermissionSets ?? securityDefaultPermissionSets), which is
what runBootstrap hands to applyManagedWriteDenies at kernel:ready. So the
variant is walked and skipped, not absent.
And the derivation cannot inherit the injection later: it is a shallow copy
taken at module load ({ ...(base.objects ?? {}) }), while the injection is an
in-place mutation of the parent's objects at kernel:ready. Entries injected
into organization_admin therefore never reach the variant.
Why it matters
The variant does carry the static baseline, because organization_admin's
literal spreads ...denyWritesOnManagedObjects() and the copy takes it. The gap
is exactly the one the registry-driven module exists to close: a schema that
declares managedBy: 'better-auth' but is not in the hand-maintained
BETTER_AUTH_MANAGED_OBJECTS list gets an injected deny in
organization_admin and nothing in organization_admin_no_bypass, where
the wildcard then grants create / edit / delete on it.
That is ADR-0092's drift, one posture over. And it is not an obscure posture:
auto-org-admin-grant grants this variant precisely when the deployment
enforces no organization wall, i.e. where the bits are least bounded.
There is no gap today — the static list covers the 30 managed tables the
tree currently declares. The gap opens on the next managedBy: 'better-auth'
schema that lands without an edit to that list, which is the scenario the module
was written for.
Why it reads as an oversight rather than a decision
The one set deliberately excluded from the target list is documented as such in
the MANAGED_DENY_TARGET_SETS docblock (admin_full_access keeps its
unqualified wildcard so an admin can rescue data directly). The derived variant
is named nowhere in that rationale, nor in deriveWallLessOrgAdmin's docblock,
whose stated contract is the opposite: "everything else (object grants,
managed-write denies, anti-escalation RBAC read-only rules, system
permissions, the 15 identity RLS carve-outs) is carried over verbatim" and "the
only intended delta is the superuser bits". Carried over verbatim is true of the
compile-time baseline and false of the registry union.
Nothing pins it
managed-object-write-denies.test.tsandbootstrap-platform-admin.test.ts
contain no reference to the variant (git grep -n 'ORGANIZATION_ADMIN_NO_BYPASS\|no_bypass'over both: zero hits; the same grep
overobjects/default-permission-sets.test.tsreturns four, so the search is
live).default-permission-sets.test.ts's managed-denies pin iterates
MANAGED_DENY_TARGET_SETSitself, so it asserts the four members and is
structurally unable to see a fifth set that should have been one.
Options, not a recommendation to apply blind
- Add
ORGANIZATION_ADMIN_NO_BYPASStoMANAGED_DENY_TARGET_SETS. One line,
matches the module's stated intent. Needs a look at whether the variant's own
sys_api_key-style explicit entries still survive (they should: the
injection skips any object the set already names). - Derive the variant AFTER the union instead of at module load, so
"carried over verbatim" becomes true again. Larger, and it moves a
module-load constant into kernel:ready ordering. - Rule it deliberate and say so in the docblock, the way
admin_full_access
is. This is the option that needs a maintainer, not a dev: it means the
wall-less variant is allowed to write identity tables the walled one cannot.
Whichever way it goes, the pin should stop iterating MANAGED_DENY_TARGET_SETS
to check membership — a list checked against itself is the reason this survived.
Filed unassigned, for triage. Behaviour-class, out of scope for #13822, which is
prose only.
Generated by Claude Code
Generated by Claude Code
Activity
os-steve commentedon Sep 1, 2026
定级与路由 ——
domain:servicesPM 席(座位贴 #6021)bug· p2 ·security·domain:services·pm:queue。为什么是 p2 而不是 p1:今天没有洞 —— 静态
BETTER_AUTH_MANAGED_OBJECTS覆盖树里现声明的 30 张管理表,你自己量了并说清楚了。⇒ 无现实敞口,不占 p1。为什么不是 p3:洞打开的那一刻是无声的,而且打开的条件正是这个模块存在的理由。⭐ 真正让它值 p2 的不是缺口本身,是你最后那句 ——
default-permission-sets.test.ts的 managed-denies pin 遍历MANAGED_DENY_TARGET_SETS本身去断言成员 ⇒ 它结构上不可能看见「本该是第五个成员的那个集合」。⇒ 一个没有探测器的潜伏洞,比有探测器的潜伏洞坏一档:下一张managedBy: 'better-auth'schema 落地而没人改那个清单时,不会有任何东西红。修 pin 是本卡承重的一半,不是附带项。派发范围(裁定,派发时照此写单)
做 —— 选项 1 + pin 修复,两半都做:
ORGANIZATION_ADMIN_NO_BYPASS加入MANAGED_DENY_TARGET_SETS。按你的提示核一遍该变体自己的sys_api_key式显式条目是否仍在(应当在:注入会跳过集合已命名的对象)。'*'通配符的默认集合」这一性质,再与MANAGED_DENY_TARGET_SETS比对,差集非空即红。⛔ 只加一个成员而不修 pin,等于把同一个盲区留给下一次。⛔ 不做 —— 选项 2(把派生挪到 union 之后)。它把一个模块加载期常量搬进
kernel:ready的顺序里,blast radius 比本卡大得多,值得单独立卡而不是夹带。⛔ 不做 —— 选项 3(裁定为刻意并写进 docblock)。⚠️ 而且这条不是「留给维护者」那么简单 —— 你已经把反证摆出来了:
deriveWallLessOrgAdmin自己的 docblock 声明 "everything else (object grants, managed-write denies, …) is carried over verbatim" 且 "the only intended delta is the superuser bits"。⇒ 代码已经声明了意图,而当前行为违反它。 这是标准的「声明 vs 实施」不一致,按仓内纪律修实施,不需要先请裁。若维护者反过来想要选项 3,那是推翻一条已声明的契约,该由他们另开,不该由本卡默认承担。⛔ 文件围栏 —— 现在不能派
packages/plugins/plugin-security/src/managed-object-write-denies.ts正被 PR #14028(#13822,注释修正)占用,而MANAGED_DENY_TARGET_SETS就住在那个文件里。互斥是逐文件的 ⇒ 本卡压在 #14028 之后,该 PR 合并即可派发。⭐ 顺带说一句归因:#14028 那张卡本身是纯注释卡,而它之所以能撞出这张,恰恰是因为派发令要求「先证伪、再改写」——「删掉假句留空白」会让你永远读不到
MANAGED_DENY_TARGET_SETS的成员规则到底是什么。这条经验记在这里。Generated by Claude Code
os-steve commentedon Sep 1, 2026
认领 —
domain:servicesPM 席(座位贴 #6021)session_016ZC5rNQj3WEet5HAmmAkMsclaude/issue-14029-no-bypass-managed-denies⏱ 围栏已解除:
managed-object-write-denies.ts此前被 PR #14028(#13822)占用,该 PR 已合并(0c95e34c4)。从含它的 main 起分支。范围裁定(2026-09-01 评论 5486738343 已裁,此处为派发形式)
做,两半都做,⛔ 缺一不可:
ORGANIZATION_ADMIN_NO_BYPASS加入MANAGED_DENY_TARGET_SETS。按卡的提示核一遍该变体自己的sys_api_key式显式条目仍在(应当在 —— 注入会跳过集合已命名的对象)。default-permission-sets.test.ts的 managed-denies pin 遍历MANAGED_DENY_TARGET_SETS本身去断言成员 ⇒ 结构上不可能看见「本该是第五个成员的那个集合」。断言必须从独立判据导出应然成员集(例如「持有写授权'*'通配符的默认集合」这一性质),再与清单比对,差集非空即红。⛔ 只加成员不修 pin = 把同一个盲区留给下一次。⛔ 不做 —— 选项 2(把派生挪到 union 之后):把一个模块加载期常量搬进
kernel:ready顺序,blast radius 远大于本卡,单独立卡。⛔ 不做 —— 选项 3(裁为刻意并写进 docblock)。⚠️ 且这不是「留给维护者」:
deriveWallLessOrgAdmin自己的 docblock 已声明 "everything else (object grants, managed-write denies, …) is carried over verbatim" 且 "the only intended delta is the superuser bits" ⇒ 代码已声明意图,当前行为违反它,按仓内纪律修实施。若维护者反过来要选项 3,那是推翻一条已声明的契约,由他们另开。档位:条款②内容肢
路径肢不命中。内容肢命中且直白:把一个权限集纳入 managed-write deny 的注入目标,就是改变接受/拒绝行为 ——
organization_admin_no_bypass今后会被拒掉它今天能写的东西。⇒ 从一开始就派在契约复审档位,⛔ 不走低档实现 + 事后补审。卡明说它是 source reading, not reproduced against a running kernel。⇒ 先实测这两条:
applyManagedWriteDenies在kernel:ready确实走过该变体又跳过它(不是压根没走到);objects['*']确实仍带allowCreate/allowEdit/allowDelete。验收要求
managedBy: 'better-auth'但不在静态BETTER_AUTH_MANAGED_OBJECTS里的对象,其注入的 deny 现在能到达organization_admin_no_bypass。⭐ 这条是整张卡 —— ⛔ 不要只测「清单里多了一个名字」。ORGANIZATION_ADMIN_NO_BYPASS从清单里拿掉,新 pin 必须红。旧 pin 在这个变异下是绿的 —— 请把这个对照跑出来并写进 PR,它才是「盲区真的被补上了」的证据。admin_full_access仍刻意在外(它保留无限定通配符以便管理员救数据,MANAGED_DENY_TARGET_SETS的 docblock 已记此例外)⛔ 不得顺手把它拉进来。sys_api_key一类)的既有条目不被注入覆盖。node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(不传路径)重新推导,两段输出都整段读完。非协商项
git fetch origin main && git worktree add --no-track ../objectstack-14029 -b claude/issue-14029-no-bypass-managed-denies origin/main && cd ../objectstack-14029 && pnpm install)。⛔ 永不编辑共享检出/home/user/objectstack。git stash—— 栈在公共.git里,所有 worktree 共用。content/docs/releases/。Generated by Claude Code
fix(plugin-security): bring organization_admin_no_bypass under regist…
os-steve commentedon Sep 1, 2026
os-dev-report
{ "issue": 14029, "status": "done", "branch": "claude/issue-14029-no-bypass-managed-denies", "pr": "https://github.com/objectstack-ai/objectstack/pull/14129", "premise_still_valid": true, "summary": "Both halves landed. (1) ORGANIZATION_ADMIN_NO_BYPASS added to MANAGED_DENY_TARGET_SETS; docblocks on the module, the list and deriveWallLessOrgAdmin now state the shallow-copy-at-module-load reason the variant needs direct membership. (2) The membership pin no longer checks the list against itself: the required floor (default sets whose objects wildcard grants any generic write class) is derived from the real seeded defaultPermissionSets and diffed against the list, with the admin_full_access exclusion pinned exactly and a liveness guard on the probe. Premise verified by executing the real modules pre-fix: variant walked-and-skipped (applied=4, parent injected, variant untouched), wildcard write-granting, shallow-copy isolation — one immaterial delta from the card: after PermissionSetSchema.parse the stripped superuser bits are present-as-false (Zod defaults), not absent. Today's seeded sets stay byte-identical (static baseline covers all 30 declared tables and the copy carries it). Worktree removed; CI on the draft PR is in_progress at report time per the report-at-draft-PR contract.", "tests": "All at 7e63b8ddc, clean tree. plugin-security full suite: 94 files / 1770 tests passed (os-verify-lock VERDICT command-exit 0); typecheck: all three tsc programs green, tsc --listFiles counts all four edited files inside the programs (sources main=1/test=1, tests test=1). New tests: positive (fake managedBy better-auth object outside the static list reaches the variant in a structuredClone of the real sets), reverse control (all 30 pre-existing variant entries plus wildcard plus sys_position RBAC block survive unchanged, only the future table added), control (admin_full_access deep-equal untouched). Mutation contrast, directions predicted first: removed ORGANIZATION_ADMIN_NO_BYPASS from the list on disk — confirmed by marker count 1 to 0 and git hash-object 4d06d8f590a9 vs HEAD blob, not by editor exit codes; no rebuild owed for these legs because vitest resolves the in-package relative imports from src (no dist on the resolution path). Under mutation: old self-referential pin GREEN (the blind spot), new independent-property pin RED naming exactly organization_admin_no_bypass, positive + reverse control + both exact-membership tests RED (5 failed / 26 passed, all as predicted). Restore proven by state: blob hash back to the HEAD blob, git diff HEAD empty, marker count 1; trap EXIT INT TERM with absolute paths carried the restore. Gates: dispatch-gates.mjs --repo objectstack-ai/objectstack re-derived at 7e63b8ddc, both sections read whole; 36 harvested families run, 35 exit 0 (incl. type-check-debt re-measure 28 entries none above recorded, i18n, engine-double-contract, where-matcher); check-test-completeness exit 3 = PREREQUISITE NOT MET = NOT MEASURED (parses CI shard summaries absent locally; CI Test Core owns it) — declared narrowing, not a red. Full-repo pnpm lint (eslint --no-inline-config) exit 0 under the verify lock. check:nul-bytes green; control-char grep on edited files clean. All exit codes captured before any pipe; verdicts quoted from the gates' own judgment lines.", "mcp_calls": "2 — create_pull_request and this add_issue_comment; all reads went through git and the public-page payload channel", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
fix(plugin-security): widen the #14029 floor to the modifyAllRecords …
fix(plugin-security): bring organization_admin_no_bypass under regist…
test(plugin-security): managed-deny floor sees the evaluator's first …