发现于 #5892 / PR #5939 的实现过程(范围外,未在该 PR 修)。
现状
#5892 只要求守住 ban(sys_user.banned = true),PR #5939 因此把不变量装在 sys_user 的 beforeUpdate 上。删除这条路径上的同一条不变量,今天由两段代码覆盖,而两段都不判断「管理员还剩几个」:
packages/plugins/plugin-auth/src/auth-manager.ts:1239-1300 —— /delete-user、/admin/remove-user、/admin/ban-user 三条路径上的 break-glass 守卫。它判定的是最后一个本地 credential 账户持有人:
const targetCred = await adapter.findOne({ model: 'account', where: [
{ field: 'userId', value: targetId }, { field: 'providerId', value: 'credential' } ] });
if (targetCred) { /* 只有目标自己持有本地密码时才继续数 */ }
目标不持本地凭据(IdP 托管 / SCIM JIT provision 出来的用户)时,守卫整段跳过。
identity-write-guard.ts(ADR-0092 D2)拒绝用户上下文的 sys_user delete,但按设计放行 isSystem —— better-auth adapter 自己的写入正是 system。
可达的锁死
一个 enforced SSO 环境,最后一个管理员由 IdP 托管(无本地密码),IdP 侧把他移出组 → SCIM DELETE /Users/{id}(或 /admin/remove-user)→ 两段守卫都不触发,用户行被删除。环境里可能还留着一个持密码的非管理员逃生账户:能登录,但不能管理任何东西 —— 与 ADR-0024 D5.2 想避免的状态是同一类。
PR #5939 的 ban 守卫不覆盖这条:它是 beforeUpdate,delete 走 beforeDelete。
可能的方向(未决)
- 把「剩余 0 个未被 ban 的管理员则拒绝」的判定从
last-admin-ban-guard.ts 复用到 beforeDelete(同一份 resolveAdminUserIds,目标集换成被删行);
- 或者把 auth-manager 那段 HTTP 守卫的判据从「最后一个 credential 持有人」扩成「最后一个管理员」—— 但那段是 fail-open 的,与不变量要求的 fail-closed 方向相反,合并前需要先决定方向。
两条不等价:HTTP 守卫只覆盖 better-auth 的三条端点,引擎钩子覆盖所有写入路径(#5939 的头注释解释了为什么选后者)。
参考
Generated by Claude Code
发现于 #5892 / PR #5939 的实现过程(范围外,未在该 PR 修)。
现状
#5892 只要求守住 ban(
sys_user.banned = true),PR #5939 因此把不变量装在sys_user的beforeUpdate上。删除这条路径上的同一条不变量,今天由两段代码覆盖,而两段都不判断「管理员还剩几个」:packages/plugins/plugin-auth/src/auth-manager.ts:1239-1300——/delete-user、/admin/remove-user、/admin/ban-user三条路径上的 break-glass 守卫。它判定的是最后一个本地credential账户持有人:目标不持本地凭据(IdP 托管 / SCIM JIT provision 出来的用户)时,守卫整段跳过。
identity-write-guard.ts(ADR-0092 D2)拒绝用户上下文的sys_userdelete,但按设计放行isSystem—— better-auth adapter 自己的写入正是 system。可达的锁死
一个 enforced SSO 环境,最后一个管理员由 IdP 托管(无本地密码),IdP 侧把他移出组 → SCIM
DELETE /Users/{id}(或/admin/remove-user)→ 两段守卫都不触发,用户行被删除。环境里可能还留着一个持密码的非管理员逃生账户:能登录,但不能管理任何东西 —— 与 ADR-0024 D5.2 想避免的状态是同一类。PR #5939 的 ban 守卫不覆盖这条:它是
beforeUpdate,delete 走beforeDelete。可能的方向(未决)
last-admin-ban-guard.ts复用到beforeDelete(同一份resolveAdminUserIds,目标集换成被删行);两条不等价:HTTP 守卫只覆盖 better-auth 的三条端点,引擎钩子覆盖所有写入路径(#5939 的头注释解释了为什么选后者)。
参考
Generated by Claude Code