Skip to content

非 admin 自助新建 key 报错 errors.auth.forbidden(403,写路径被 admin-only 路由拦截) #1259

@GOSICK-Angel

Description

@GOSICK-Angel

现象

非 admin 用户(持有 canLoginWebUi 的 key)在「用户管理」页点击「新建」给自己创建 key 时,提交后 toast 报错,显示原始 i18n key 文案 errors.auth.forbidden(即页面上看到的 "errors auth forbidden"),无法创建 key。

admin 用户不受影响。

复现步骤

  1. 用一个 role=usercanLoginWebUi=true 的 key 登录 Web UI。
  2. 进入「用户管理」页(非 admin 视图)。
  3. 点击主「新建」按钮 —— 非 admin 时打开的是「给自己建 key」的 AddKeyDialog。
  4. 填写后提交。
  5. 期望:成功创建自己的 key;实际:toast 显示 errors.auth.forbidden,创建失败(HTTP 403)。

根因分析

这是「自助建 key 的写路径被 admin-only 路由拦截」的真实 403 bug,叠加一个错误文案显示 bug。

逐跳链路:

  1. 入口:src/app/[locale]/dashboard/users/users-page-client.tsx:803-821 —— 非 admin 时主「新建」按钮打开 AddKeyDialoguserId=selfUser.id, isAdmin=false),这是有意提供的自助功能。
  2. 提交:forms/add-key-form.tsx:79 -> addKey() -> src/lib/api-client/v1/actions/keys.ts:18 -> apiPost("/api/v1/users/${userId}/keys")
  3. 被拦:该路由 src/app/api/v1/resources/keys/router.ts:90 写死 requireAuth("admin")src/lib/api/v1/_shared/auth-middleware.ts:103-110 判定 session.user.role !== "admin" -> 返回 403 + errorCode: "auth.forbidden"
  4. 关键矛盾:addKey action 本身其实允许自助建 key(src/actions/keys.ts:128session.user.id === data.userId 即放行),但请求在中间件就被拦掉,根本到不了 action。
  5. 文案降级:fetcher.ts 封成 ApiError(errorCode:"auth.forbidden") -> _compat.toActionResult 原样带回 errorCode(未经过 errors.tsAPI_ERROR_MESSAGE_KEYS 映射)-> add-key-form.tsx:100 getErrorMessage(tErrors, "auth.forbidden") -> t("auth.forbidden")
  6. 缺键:messages/*/errors.json 只有 PERMISSION_DENIED,没有 auth.forbidden 键 -> next-intl 渲染缺失键的兜底路径字符串 -> 用户看到 errors.auth.forbidden

对照证据:读路径 getUsers()src/lib/api-client/v1/actions/users.ts:42-50)对同样的 403 做了 :self 兜底(fallback 到 read-tier 的 /api/v1/users:selfusers/router.ts:132),所以非 admin 能正常看到自己和自己的 key;但写路径 addKey() 没有任何 self 端点/兜底,于是自助建 key 必 403。

两个缺陷

  • 功能根因:非 admin 自助建 key 缺少 self 写端点。UI 把自助建 key 暴露给非 admin,但 POST /api/v1/users/:userId/keys 是 admin-only;读有 :self 兜底、写没有。
  • 显示缺陷:即便 403 合理,toast 也只显示原始 i18n key(auth.forbidden 未映射成 PERMISSION_DENIED)。

引入提交

7e2a7223refactor: add v1 REST management OpenAPI (#1140)(2026-04-30)。

该提交把 add-key-formaddKey 从直接调用 Server Action(@/actions/keys,自带自助放行)改为走 admin-only 的 REST 路由 POST /api/v1/users/:userId/keys,并在同一提交里只给读路径加了 :self 兜底、漏给写路径加对应 self 端点/兜底,从而引入回归。后续 b58866300c587039 仅调整 :reveal/:enable/:renew 注册顺序,与本 bug 无关。

修复建议

方案 A(推荐,对齐读路径):

  • 新增 read-tier 的自助建 key 端点(如 POST /api/v1/users:self/keys),内部强制 userId = session.user.id,复用现有 addKey action(已自带 self 校验)。
  • addKey() client 仿 getUsers()isAdmin=false 时直接打 self 端点,或 isAdminForbidden 时回退。

叠加修复显示缺陷:

  • _compat.toActionResultApiError 统一用 getApiErrorMessageKey() 映射(auth.forbidden -> PERMISSION_DENIED)再回传,避免所有表单重复踩坑。

方案 B(不推荐):把 POST /users/:userId/keys 的 tier 从 admin 降为 read,靠 action 已有的 PERMISSION_DENIED 兜越权。改动更小,但会放行只读 token 写入,且需复核 GET .../keys 降级后的越权面,风险更高。

影响范围

  • 所有非 admin(自助)用户均无法新建自己的 key。
  • admin 不受影响。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions