Skip to content

最后一个管理员的「删除」路径无守卫:目标不持本地密码时,SCIM/admin remove 可删掉环境最后一个管理员 #5941

Description

@baozhoutao

发现于 #5892 / PR #5939 的实现过程(范围外,未在该 PR 修)。

现状

#5892 只要求守住 ban(sys_user.banned = true),PR #5939 因此把不变量装在 sys_userbeforeUpdate 上。删除这条路径上的同一条不变量,今天由两段代码覆盖,而两段都不判断「管理员还剩几个」:

  1. 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 出来的用户)时,守卫整段跳过。

  1. 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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions