Skip to content

docs(plugin-auth): 纠正 admin 审计的「绕过生命周期钩子」断言,并把实测钉住 (#4940) - #4970

Merged
xuyushun441-sys merged 2 commits into
mainfrom
claude/issue-4940-admin-audit-bypass-claim
Aug 3, 2026
Merged

docs(plugin-auth): 纠正 admin 审计的「绕过生命周期钩子」断言,并把实测钉住 (#4940)#4970
xuyushun441-sys merged 2 commits into
mainfrom
claude/issue-4940-admin-audit-bypass-claim

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

Fixes #4940

议题的 open question 是「装了 plugin-audit 时,这两条路径是不是每次写两条审计」。没有推理,只有实测:开 showcase 栈 + AuditPlugin,打真实路由,读 sys_audit_log

一、实测结果

驱动 POST /auth/admin/create-user / set-user-password / import-users,逐行 dump:

路径 写入对象 plugin-audit 通用行 显式行 是否重复
admin/create-user sys_user(better-auth adapter) ✅ 1 条 create(整行快照进 new_value) ✅ 1 条 create(event: user.admin_created) — 同一 record_id 上两条 action:'create'
同上,must_change_password 印戳 sys_user ✅ 1 条 update(字段 diff)
admin/set-user-password sys_account(credential) ❌ 0 条 ✅ 1 条 update(event: user.admin_password_set)
admin/import-users 逐行 sys_user ✅ 每行 1 条 create ❌ 无逐行显式行
admin/import-users run 级 结构上无法产出 ✅ 1 条 action:'import', record_id: null

原始输出(create-user 那三条):

{"action":"create","object_name":"sys_user","record_id":"uEye…","user_id":"YmNv…",
 "metadata":"{\"event\":\"user.admin_created\",\"email\":\"measure.4940@example.com\",…}"}
{"action":"update","object_name":"sys_user","record_id":"uEye…","user_id":null,
 "old_value":"{\"must_change_password\":false}","new_value":"{\"must_change_password\":true}"}
{"action":"create","object_name":"sys_user","record_id":"uEye…","user_id":"YmNv…",
 "metadata":null,"new_value":"{\"id\":\"uEye…\",\"name\":\"Measure 4940\",\"email\":\"measure.4940@example.com\",…}"}

第三条只可能engine.registerHook('afterInsert', writeAudit) 写出 —— 这就是对「绕过生命周期钩子」的直接反证。sys_account:该用户确有 1 条 credential 行,而以它的 id 查 sys_audit_log0 条(SKIP_OBJECTSsys_account)。议题提醒的「两个对象结论可能不同」成立,而且正是决定性的那一半

二、结论:保留纪律、更换理由(与 #4942 同款)

有重复,但不去掉显式行。取舍依据就是上表:

  1. plugin-audit 是可选插件 —— 本包不依赖它;不装则连 sys_audit_log 表都没有。admin 铸造一个可登录账号,其合规痕迹不该挂在可选插件上。
  2. sys_accountSKIP_OBJECTS —— set-user-password 真正写的是 credential,通用 writer 对它零产出。若按「钩子已覆盖」删掉显式行,「管理员重置了某人密码」这件事将完全无痕。这是最容易被误删的一条,所以写进注释也钉进测试。
  3. 两条行的 metadata 不是一回事 —— 通用行是字段 diff / 整行快照;显式行记的是管理员的决策(passwordGenerated / mustChangePassword / placeholderEmail / membershipCreated),从行快照里推不出来。
  4. import 的 run 级行是补集而非重复 —— actionFor 只映射 afterInsert/Update/Delete → create/update/delete,action:'import' + record_id: null 这个形状通用 writer 结构上产不出;它回答的是逐行台账回答不了的问题:谁跑了哪次导入、整体做了什么。

create-user 上那唯一一处真重复(2 条 create)是明知并接受的,也写进了注释和测试的精确计数 —— 将来若冒出第三个写者,那是 finding,不是悄悄多出来的一行。

三、原地驳斥而非删除

#4942 一致:该断言已抄进 cloud(cloud#1022)。从抄写件过来的读者若在 framework 这边什么都没看到,无法区分「这句被删是因为它错了」和「这句本来就不在」—— 默认反而会假设 framework 的注释才是过时那份。所以点名 + 逐跳锚点。

四、新增闸门(这次能钉住,#4942 不能)

#4942 没加测试,理由是 plugin-auth 不依赖 objectql/plugin-audit,跨包事实钉不进本包。本单不受这个限制:packages/qa/dogfood 同时依赖两者,于是新增 test/admin-identity-audit-trail.dogfood.test.ts(3 例,W1–W4),打真实路由断言上表每一格。纠正后的注释因此是套件强制的断言,而不是又一句无人核验的新claim —— 议题本身就是「写下来却从没被钉住的机制断言」造成的。

反向验证

sys_user 临时塞进 plugin-audit 的 SKIP_OBJECTS(即模拟旧注释描述的那个世界:通用 writer 看不见 better-auth 的 sys_user 写入),重建 plugin-audit 后 3 例全红,且报错原文正好显出唯一幸存的那条显式行:

× POST /admin/create-user: plugin-audit's hook fires for the better-auth write… 
× POST /admin/set-user-password: sys_account is in SKIP_OBJECTS…
× POST /admin/import-users: the run-level row complements the per-row rows…
Error: expected both a generic and an explicit create row — last saw 1 row(s):
  [{"a":"create","m":"{\"event\":\"user.admin_created\",…}"}]
Error: plugin-audit's per-row create row for the imported user — last saw 0 row(s): []
 Tests  3 failed (3)

即断言是承重的,不是恒真的。改动已还原,plugin-audit 已重建。

五、同族扫尾

全仓 grep 该措辞(bypass.*hook / hooks that plugin-audit / plugin-audit subscribes),没有第四、第五处。命中的其余各处是另一类、且成立的说法:plugin-pinyin-search/companion-projection.ts 与两个 dogfood 测试讲的是 direct driver 写入(确实不过 engine 钩子);#4942 已修的三处已随 main 合入。cloud 侧抄写由 cloud#1022 单独跟踪。

改动

  • packages/plugins/plugin-auth/src/admin-user-endpoints.tswriteAdminAudit 的 JSDoc
  • packages/plugins/plugin-auth/src/admin-import-users.ts — run 级审计旁的注释
  • packages/qa/dogfood/test/admin-identity-audit-trail.dogfood.test.ts — 新增闸门
  • .changeset/auth-admin-audit-hook-bypass-claim-corrected.md(空 frontmatter,不发版)

仅注释 + 测试,无运行时行为改变。与 #4942 文件不相交;#4942 已合入 main,本分支已 merge 最新 main(带入了 objectql / spec 的改动)并重建、复跑全绿。

验证

$ pnpm --filter @objectstack/plugin-auth typecheck
> tsc --noEmit          # 无输出

$ pnpm --filter @objectstack/plugin-auth test
 Test Files  29 passed (29)
      Tests  658 passed (658)

$ npx vitest run test/admin-identity-audit-trail.dogfood.test.ts --project isolated
 Test Files  1 passed (1)
      Tests  3 passed (3)

$ npx eslint <三个改动文件>     # 无输出,exit 0

一个小陷阱记下来:admin 身份路由在 better-auth admin 插件未开时返回 501,而 bootStack 不暴露 auth 插件覆写。唯一够得着的 env 是 OS_SCIM_ENABLED —— buildPluginListadmin: pluginConfig.admin ?? scimEffective(SCIM 强制打开 admin),属有据可依的推导,已在测试头注明。


Generated by Claude Code

claude added 2 commits August 3, 2026 17:23
#4940)

`writeAdminAudit` in admin-user-endpoints.ts and the run-level insert in
admin-import-users.ts both justified their explicit sys_audit_log row with
the stale mechanism claim #4802 refuted one layer up: "better-auth writes
bypass the ObjectQL lifecycle hooks that plugin-audit subscribes to".

The hooks do fire. Measured on the real routes (showcase stack +
AuditPlugin): POST /admin/create-user leaves two `action: 'create'` rows on
the new sys_user — the explicit one and plugin-audit's row snapshot — and
each imported user gets plugin-audit's own per-row create row.

The explicit rows are kept; only the reason changes. plugin-audit is
optional; sys_account IS in its SKIP_OBJECTS so /admin/set-user-password
produces zero generic rows and the explicit row is the sole trail of an
administrative password reset; and the import's run-level row
(action: 'import', record_id: null) is a shape plugin-audit's `actionFor`
cannot emit. The stale sentences are refuted in place rather than deleted,
matching #4802 — the claim was copied into cloud (cloud#1022).

Every measured fact is pinned by a new dogfood gate, so the corrected
comment is enforced rather than being a fresh unverified claim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 3, 2026 5:32pm

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth.

9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx (via @objectstack/plugin-auth)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/plugin-auth)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-auth)
  • content/docs/permissions/authentication.mdx (via @objectstack/plugin-auth)
  • content/docs/permissions/sso.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-auth)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-auth)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-auth)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@xuyushun441-sys
xuyushun441-sys marked this pull request as ready for review August 3, 2026 18:09
@xuyushun441-sys
xuyushun441-sys added this pull request to the merge queue Aug 3, 2026
Merged via the queue into main with commit 88264cc Aug 3, 2026
25 checks passed
@xuyushun441-sys
xuyushun441-sys deleted the claude/issue-4940-admin-audit-bypass-claim branch August 3, 2026 18:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants