docs(plugin-auth): 纠正 admin 审计的「绕过生命周期钩子」断言,并把实测钉住 (#4940) - #4970
Merged
Conversation
#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
…in-audit-bypass-claim
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4940
议题的 open question 是「装了 plugin-audit 时,这两条路径是不是每次写两条审计」。没有推理,只有实测:开 showcase 栈 +
AuditPlugin,打真实路由,读sys_audit_log。一、实测结果
驱动
POST /auth/admin/create-user/set-user-password/import-users,逐行 dump:admin/create-usersys_user(better-auth adapter)create(整行快照进new_value)create(event: user.admin_created)record_id上两条action:'create'must_change_password印戳sys_userupdate(字段 diff)admin/set-user-passwordsys_account(credential)update(event: user.admin_password_set)admin/import-users逐行sys_usercreateadmin/import-usersrun 级action:'import',record_id: null原始输出(create-user 那三条):
第三条只可能由
engine.registerHook('afterInsert', writeAudit)写出 —— 这就是对「绕过生命周期钩子」的直接反证。sys_account:该用户确有 1 条 credential 行,而以它的 id 查sys_audit_log得 0 条(SKIP_OBJECTS含sys_account)。议题提醒的「两个对象结论可能不同」成立,而且正是决定性的那一半。二、结论:保留纪律、更换理由(与 #4942 同款)
有重复,但不去掉显式行。取舍依据就是上表:
sys_audit_log表都没有。admin 铸造一个可登录账号,其合规痕迹不该挂在可选插件上。sys_account在SKIP_OBJECTS里 ——set-user-password真正写的是 credential,通用 writer 对它零产出。若按「钩子已覆盖」删掉显式行,「管理员重置了某人密码」这件事将完全无痕。这是最容易被误删的一条,所以写进注释也钉进测试。passwordGenerated/mustChangePassword/placeholderEmail/membershipCreated),从行快照里推不出来。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 例全红,且报错原文正好显出唯一幸存的那条显式行:即断言是承重的,不是恒真的。改动已还原,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.ts—writeAdminAudit的 JSDocpackages/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 的改动)并重建、复跑全绿。
验证
一个小陷阱记下来:admin 身份路由在 better-auth
admin插件未开时返回 501,而bootStack不暴露 auth 插件覆写。唯一够得着的 env 是OS_SCIM_ENABLED——buildPluginList里admin: pluginConfig.admin ?? scimEffective(SCIM 强制打开 admin),属有据可依的推导,已在测试头注明。Generated by Claude Code