采购员无接口权限:权限矩阵改为服务启动时对账,不再依赖补迁移 #156

Closed
opened 2026-08-29 15:58:18 +08:00 by ila · 2 comments
Owner

所属与来源

  • 关联工单:#148 建任务时规格匹配异步化(其新增接口是本缺陷的暴露场景)、#137 权限菜单(同一权限体系)。
  • 来源:用户于 2026-08-29 反馈:采购任务需要人工干预时,「AI 匹配」与「人工选择」保存均提示「对不起,您没有该接口访问权限,请联系管理员」。
  • 类型:Server / 接口权限同步机制缺陷。
  • 设计证据:不涉及界面与导航,无需原型。
  • 工具回退说明:本工单通过 Gitea API 创建;当前会话未提供 Gitea MCP 工具,按 AGENTS.md「Gitea 交互与工单最小读取」记录回退原因。

根因(提交 11c407d 复核)

采购员的接口权限存储在 casbin_rule 表,由 ensurePurchaserRoleAndPolicies() 依据代码中的 access.PurchaserAPIs() 全量重建(先删除 ptype='p' AND v0=purchaser 再重建)。

该函数仅被两条迁移调用:

  • server/cmd/migrate/migration/version-local/1786701700000_purchaser_role.go
  • server/cmd/migrate/migration/version-local/1787885400000_goauto_menus.go:35

而 #148 新增的三个接口位于更晚的迁移批次(1787983400000_purchase_spec_match_work_item.go)。迁移按版本号一次性执行、执行后记录 version 不再重跑,因此上述两条早已完成的迁移不会再被触发去重建策略。

结果:server/app/goauto/access/purchaser.go 中已登记的三条

{"查看采购规格匹配", "/api/admin/v1/purchase-tasks/:taskId/matching", "GET", true},
{"重新尝试采购规格匹配", "/api/admin/v1/purchase-tasks/:taskId/matching/requeue", "POST", true},
{"人工选择采购规格", "/api/admin/v1/purchase-tasks/:taskId/matching/manual", "POST", true},

从未写入 casbin_rule,采购员调用即被拒绝。管理员账号不受影响(go-admin 的 admin 角色绕过 casbin),因此该缺陷只在采购员账号下暴露。

这是结构性问题,不是一次性疏漏

access/purchaser.go 新增条目不会触发任何编译错误或测试失败,也没有任何机制提醒实施者补一条迁移。后续待做工单(#135 备货采购、#137 权限菜单、#143 版本发布等)均会新增接口,同一问题必然复发,且症状具有迷惑性——代码里权限写着 true,运行时却报无权限。

目标

  1. 修复当前三个接口对采购员不可用的问题。
  2. 消除「新增接口必须记得补迁移」这一隐性要求,使权限矩阵变更自动生效。
  3. 权限强度不变:仍以代码中的 AdminAPIs 为唯一事实源,仍执行全量重建以保证减权生效。

非目标

  • 不修改 AdminAPIs 中任何一条的 Purchaser 取值。
  • 不改变采购员角色的定义与其他属性。
  • 不改动 sys_menu 与角色菜单绑定的既有语义(#137 定义的「只增不删」不在本工单范围)。
  • 不引入界面上的权限配置能力。
  • 不改动用户自建角色与其策略。

实施方案

一、权限同步改为服务启动时对账

  1. 将 ensurePurchaserRoleAndPolicies() 的调用从「版本化迁移」改为「每次服务启动时执行一次」,与代码中的 AdminAPIs / PurchaserAPIs() 对账。
  2. 对账保持现有语义:
    • sys_api 中缺失的条目补齐;
    • 采购员的 casbin_rule 策略全量重建,保证代码中减权后旧策略不会残留;
    • 仅处理 v0 = purchaser 的策略,不得触碰其他角色与用户自建策略。
  3. 对账必须幂等:连续启动多次结果一致,不产生重复记录。
  4. 对账失败时的行为需明确:建议记录明确错误并阻止启动(权限未对齐即对外提供服务是更危险的状态),具体策略实施时确认并在工单说明。
  5. 保留既有迁移文件不动(历史记录),但其内的策略重建逻辑不再是唯一入口。

二、防复发测试

  1. 新增测试:断言 AdminAPIs 中每一条在对账执行后都能在 casbin_rule(采购员部分按 PurchaserAPIs())与 sys_api 中找到对应记录。
  2. 新增测试:代码中移除某条采购员权限后,对账执行后该策略在 casbin_rule 中不再存在(验证减权生效)。
  3. 新增测试:对账不影响其他角色与自建策略。

三、当前缺失条目

  1. 第一节的对账机制上线后,#148 的三个接口会在启动时自动补齐,无需单独编写补丁迁移。验收需实际确认这三个接口对采购员可用。

安全边界

  • 权限矩阵的唯一事实源仍为代码中的 access/purchaser.go,不下放到界面配置。
  • 全量重建语义保持不变,确保减权可靠生效。
  • 对账范围严格限定为 GoAuto 的 API 与采购员角色策略,不得影响其他角色、用户自建策略与菜单绑定。
  • 不放宽任何接口的权限;Purchaser: false 的接口仍对采购员关闭。

验收标准

  • 采购员账号可正常调用 /matching、/matching/requeue、/matching/manual 三个接口,人工干预与 AI 重试均可保存成功。
  • 服务启动后 casbin_rule 中采购员策略与 PurchaserAPIs() 完全一致(多余的被清除、缺失的被补齐)。
  • sys_api 中包含 AdminAPIs 的全部条目。
  • 连续多次启动结果一致,不产生重复记录。
  • 在代码中新增一条采购员接口并重启后,该权限无需补迁移即自动生效。
  • 在代码中移除一条采购员接口并重启后,对应策略被清除。
  • 其他角色的策略与用户自建策略未被改动。
  • Purchaser: false 的接口对采购员仍然拒绝。
  • 既有迁移文件未被删除或改写其历史语义。

验证方式

  • go test ./app/goauto/access/... ./cmd/migrate/...
  • 启动对账的幂等性测试:连续执行两次对账,断言结果一致。
  • 手工验证:以采购员账号完成一次「AI 重新匹配」与一次「人工选择规格」的保存。
  • 在既有库(已跑过历史迁移)上验证缺失策略被自动补齐。
  • 未覆盖的部署环境如实回写。

依赖、并行与风险

  • 无前置依赖,建议优先实施——当前采购任务的人工干预功能对采购员完全不可用。
  • 风险:启动时对账失败会影响服务可用性。缓解:第 4 项明确失败策略;对账逻辑本身已在既有迁移中运行多次,行为可控。
  • 风险:对账误伤其他角色策略。缓解:严格按 v0 = purchaser 限定范围,并配针对性测试。
  • 回退:还原提交后回到迁移触发方式,届时需手工补一条迁移恢复当前策略。

文档影响

  • Wiki Architecture-and-Code-Map:权限矩阵的同步时机由版本化迁移改为服务启动对账。
  • Wiki Deployment-and-Operations(若存在):说明新增接口权限无需额外迁移,重启即生效。
  • 按 Wiki-first 门禁:先改线上页面并回读 revision,再执行一轮 sync 与一轮 sync --check,把页面与 revision 写回本工单。

状态

待实施。

## 所属与来源 - 关联工单:#148 建任务时规格匹配异步化(其新增接口是本缺陷的暴露场景)、#137 权限菜单(同一权限体系)。 - 来源:用户于 2026-08-29 反馈:采购任务需要人工干预时,「AI 匹配」与「人工选择」保存均提示「对不起,您没有该接口访问权限,请联系管理员」。 - 类型:Server / 接口权限同步机制缺陷。 - 设计证据:不涉及界面与导航,无需原型。 - 工具回退说明:本工单通过 Gitea API 创建;当前会话未提供 Gitea MCP 工具,按 `AGENTS.md`「Gitea 交互与工单最小读取」记录回退原因。 ## 根因(提交 11c407d 复核) 采购员的接口权限存储在 `casbin_rule` 表,由 `ensurePurchaserRoleAndPolicies()` 依据代码中的 `access.PurchaserAPIs()` **全量重建**(先删除 `ptype='p' AND v0=purchaser` 再重建)。 该函数仅被两条迁移调用: - `server/cmd/migrate/migration/version-local/1786701700000_purchaser_role.go` - `server/cmd/migrate/migration/version-local/1787885400000_goauto_menus.go:35` 而 #148 新增的三个接口位于更晚的迁移批次(`1787983400000_purchase_spec_match_work_item.go`)。迁移按版本号一次性执行、执行后记录 version 不再重跑,因此上述两条早已完成的迁移**不会再被触发去重建策略**。 结果:`server/app/goauto/access/purchaser.go` 中已登记的三条 ```go {"查看采购规格匹配", "/api/admin/v1/purchase-tasks/:taskId/matching", "GET", true}, {"重新尝试采购规格匹配", "/api/admin/v1/purchase-tasks/:taskId/matching/requeue", "POST", true}, {"人工选择采购规格", "/api/admin/v1/purchase-tasks/:taskId/matching/manual", "POST", true}, ``` 从未写入 `casbin_rule`,采购员调用即被拒绝。管理员账号不受影响(go-admin 的 admin 角色绕过 casbin),因此该缺陷只在采购员账号下暴露。 ### 这是结构性问题,不是一次性疏漏 `access/purchaser.go` 新增条目**不会触发任何编译错误或测试失败**,也没有任何机制提醒实施者补一条迁移。后续待做工单(#135 备货采购、#137 权限菜单、#143 版本发布等)均会新增接口,同一问题必然复发,且症状具有迷惑性——代码里权限写着 `true`,运行时却报无权限。 ## 目标 1. 修复当前三个接口对采购员不可用的问题。 2. 消除「新增接口必须记得补迁移」这一隐性要求,使权限矩阵变更自动生效。 3. 权限强度不变:仍以代码中的 `AdminAPIs` 为唯一事实源,仍执行全量重建以保证减权生效。 ## 非目标 - 不修改 `AdminAPIs` 中任何一条的 `Purchaser` 取值。 - 不改变采购员角色的定义与其他属性。 - 不改动 `sys_menu` 与角色菜单绑定的既有语义(#137 定义的「只增不删」不在本工单范围)。 - 不引入界面上的权限配置能力。 - 不改动用户自建角色与其策略。 ## 实施方案 ### 一、权限同步改为服务启动时对账 1. 将 `ensurePurchaserRoleAndPolicies()` 的调用从「版本化迁移」改为「**每次服务启动时执行一次**」,与代码中的 `AdminAPIs` / `PurchaserAPIs()` 对账。 2. 对账保持现有语义: - `sys_api` 中缺失的条目补齐; - 采购员的 `casbin_rule` 策略**全量重建**,保证代码中减权后旧策略不会残留; - 仅处理 `v0 = purchaser` 的策略,**不得触碰其他角色与用户自建策略**。 3. 对账必须幂等:连续启动多次结果一致,不产生重复记录。 4. 对账失败时的行为需明确:建议记录明确错误并阻止启动(权限未对齐即对外提供服务是更危险的状态),具体策略实施时确认并在工单说明。 5. 保留既有迁移文件不动(历史记录),但其内的策略重建逻辑不再是唯一入口。 ### 二、防复发测试 6. 新增测试:断言 `AdminAPIs` 中每一条在对账执行后都能在 `casbin_rule`(采购员部分按 `PurchaserAPIs()`)与 `sys_api` 中找到对应记录。 7. 新增测试:代码中移除某条采购员权限后,对账执行后该策略在 `casbin_rule` 中不再存在(验证减权生效)。 8. 新增测试:对账不影响其他角色与自建策略。 ### 三、当前缺失条目 9. 第一节的对账机制上线后,#148 的三个接口会在启动时自动补齐,**无需单独编写补丁迁移**。验收需实际确认这三个接口对采购员可用。 ## 安全边界 - 权限矩阵的唯一事实源仍为代码中的 `access/purchaser.go`,不下放到界面配置。 - 全量重建语义保持不变,确保减权可靠生效。 - 对账范围严格限定为 GoAuto 的 API 与采购员角色策略,不得影响其他角色、用户自建策略与菜单绑定。 - 不放宽任何接口的权限;`Purchaser: false` 的接口仍对采购员关闭。 ## 验收标准 - [ ] 采购员账号可正常调用 `/matching`、`/matching/requeue`、`/matching/manual` 三个接口,人工干预与 AI 重试均可保存成功。 - [ ] 服务启动后 `casbin_rule` 中采购员策略与 `PurchaserAPIs()` 完全一致(多余的被清除、缺失的被补齐)。 - [ ] `sys_api` 中包含 `AdminAPIs` 的全部条目。 - [ ] 连续多次启动结果一致,不产生重复记录。 - [ ] 在代码中新增一条采购员接口并重启后,该权限**无需补迁移即自动生效**。 - [ ] 在代码中移除一条采购员接口并重启后,对应策略被清除。 - [ ] 其他角色的策略与用户自建策略未被改动。 - [ ] `Purchaser: false` 的接口对采购员仍然拒绝。 - [ ] 既有迁移文件未被删除或改写其历史语义。 ## 验证方式 - `go test ./app/goauto/access/... ./cmd/migrate/...` - 启动对账的幂等性测试:连续执行两次对账,断言结果一致。 - 手工验证:以采购员账号完成一次「AI 重新匹配」与一次「人工选择规格」的保存。 - 在既有库(已跑过历史迁移)上验证缺失策略被自动补齐。 - 未覆盖的部署环境如实回写。 ## 依赖、并行与风险 - 无前置依赖,建议优先实施——当前采购任务的人工干预功能对采购员完全不可用。 - 风险:启动时对账失败会影响服务可用性。缓解:第 4 项明确失败策略;对账逻辑本身已在既有迁移中运行多次,行为可控。 - 风险:对账误伤其他角色策略。缓解:严格按 `v0 = purchaser` 限定范围,并配针对性测试。 - 回退:还原提交后回到迁移触发方式,届时需手工补一条迁移恢复当前策略。 ## 文档影响 - Wiki `Architecture-and-Code-Map`:权限矩阵的同步时机由版本化迁移改为服务启动对账。 - Wiki `Deployment-and-Operations`(若存在):说明新增接口权限无需额外迁移,重启即生效。 - 按 Wiki-first 门禁:先改线上页面并回读 revision,再执行一轮 `sync` 与一轮 `sync --check`,把页面与 revision 写回本工单。 ## 状态 待实施。
Author
Owner

实施完成,待验收

已按 2026-08-29 用户授权实施 #156 的权限启动对账,并完成本机 Admin/API 重启及权限写入验证。

实现

  • 新增 ReconcilePurchaserPermissions,API 启动、路由注册和监听端口之前执行。
  • 每次启动在单个事务内:
    1. 幂等确保采购员角色;
    2. 以 AdminAPIs 权限矩阵幂等补齐全部 sys_api;
    3. 仅删除并重建 ptype=p, v0=purchaser 的 Casbin 策略;
    4. 不修改其他角色策略。
  • 对账任一步失败即终止 API 启动,避免带着残缺权限提供服务。
  • 未新增数据库迁移,未修改历史迁移;#156 改为每次启动对账。
  • 实现过程中发现 GORM 字符串条件不会给 FirstOrCreate 的新增记录填入 path/action,已在本次启动对账中改用结构化条件,并用测试锁定。

自动化验证

  • go test ./app/goauto/access/... ./cmd/migrate/... ./cmd/api/...:通过。
  • go test ./...(server):通过。
  • .\scripts\verify.ps1 -Component server:通过。
  • python dev_scripts/harness.py check --strict:通过。
  • git diff --check:通过。
  • 覆盖:连续两次幂等、全部 Admin API 入库、采购员策略与矩阵一致、管理员专属接口仍禁止、其他角色不受影响、废弃采购员授权可清除、失败时事务回滚且 API 启动失败。

本机 MySQL / Admin API 实证

首次重启前:

  • 采购员策略总数:61
  • #148 三个规格匹配接口的采购员策略:0

首次启动对账后:

  • 采购员策略总数:64
  • 三个目标策略各 1 条:
    • GET /api/admin/v1/purchase-tasks/:taskId/matching
    • POST /api/admin/v1/purchase-tasks/:taskId/matching/requeue
    • POST /api/admin/v1/purchase-tasks/:taskId/matching/manual
  • 三个对应 sys_api 各 1 条
  • 其他角色策略总数保持 0
  • Admin/API 运行中,健康检查 HTTP 200

随后第二次重启验证幂等:

  • 采购员策略仍为 64
  • 三个目标策略仍为 3 条(各 1)
  • Admin/API 运行中,健康检查 HTTP 200

文档与提交

  • Wiki Architecture-and-Code-Map 已在线更新并回读,revision:d0222d4337992bedcf2b973bc3bb9d43922c0ea3
  • 已执行一次 harness.py sync 和一次 sync --check
  • Deployment-and-Operations 页面当前不存在,按工单约定未创建虚假运维页
  • 提交:7c18159 fix(#156): reconcile purchaser permissions at startup
  • 已推送 origin/main

尚需人工验收

浏览器当前登录的是管理员会话(可见“系统管理/开发工具”等管理员菜单),不是采购员会话,因此没有用管理员权限冒充采购员接口验收。为避免改变真实采购任务状态,也未点击“AI 重试/人工保存”。

待采购员账号实际登录后,验证采购任务详情的匹配读取、AI 重试和人工保存不再返回 403;管理员专属接口仍不可访问。工单保持打开、待用户验收。

## 实施完成,待验收 已按 2026-08-29 用户授权实施 #156 的权限启动对账,并完成本机 Admin/API 重启及权限写入验证。 ### 实现 - 新增 `ReconcilePurchaserPermissions`,API 启动、路由注册和监听端口之前执行。 - 每次启动在单个事务内: 1. 幂等确保采购员角色; 2. 以 `AdminAPIs` 权限矩阵幂等补齐全部 `sys_api`; 3. 仅删除并重建 `ptype=p, v0=purchaser` 的 Casbin 策略; 4. 不修改其他角色策略。 - 对账任一步失败即终止 API 启动,避免带着残缺权限提供服务。 - 未新增数据库迁移,未修改历史迁移;#156 改为每次启动对账。 - 实现过程中发现 GORM 字符串条件不会给 `FirstOrCreate` 的新增记录填入 path/action,已在本次启动对账中改用结构化条件,并用测试锁定。 ### 自动化验证 - `go test ./app/goauto/access/... ./cmd/migrate/... ./cmd/api/...`:通过。 - `go test ./...`(server):通过。 - `.\scripts\verify.ps1 -Component server`:通过。 - `python dev_scripts/harness.py check --strict`:通过。 - `git diff --check`:通过。 - 覆盖:连续两次幂等、全部 Admin API 入库、采购员策略与矩阵一致、管理员专属接口仍禁止、其他角色不受影响、废弃采购员授权可清除、失败时事务回滚且 API 启动失败。 ### 本机 MySQL / Admin API 实证 首次重启前: - 采购员策略总数:61 - #148 三个规格匹配接口的采购员策略:0 首次启动对账后: - 采购员策略总数:64 - 三个目标策略各 1 条: - `GET /api/admin/v1/purchase-tasks/:taskId/matching` - `POST /api/admin/v1/purchase-tasks/:taskId/matching/requeue` - `POST /api/admin/v1/purchase-tasks/:taskId/matching/manual` - 三个对应 `sys_api` 各 1 条 - 其他角色策略总数保持 0 - Admin/API 运行中,健康检查 HTTP 200 随后第二次重启验证幂等: - 采购员策略仍为 64 - 三个目标策略仍为 3 条(各 1) - Admin/API 运行中,健康检查 HTTP 200 ### 文档与提交 - Wiki `Architecture-and-Code-Map` 已在线更新并回读,revision:`d0222d4337992bedcf2b973bc3bb9d43922c0ea3` - 已执行一次 `harness.py sync` 和一次 `sync --check` - `Deployment-and-Operations` 页面当前不存在,按工单约定未创建虚假运维页 - 提交:`7c18159 fix(#156): reconcile purchaser permissions at startup` - 已推送 `origin/main` ### 尚需人工验收 浏览器当前登录的是管理员会话(可见“系统管理/开发工具”等管理员菜单),不是采购员会话,因此没有用管理员权限冒充采购员接口验收。为避免改变真实采购任务状态,也未点击“AI 重试/人工保存”。 待采购员账号实际登录后,验证采购任务详情的匹配读取、AI 重试和人工保存不再返回 403;管理员专属接口仍不可访问。工单保持打开、待用户验收。
Author
Owner

用户于 2026-08-29 明确确认本工单通过验收。验收结论已记录,现关闭工单。没有新的长期事实变化,本次不重复同步 Wiki。

用户于 2026-08-29 明确确认本工单通过验收。验收结论已记录,现关闭工单。没有新的长期事实变化,本次不重复同步 Wiki。
ila closed this issue 2026-08-29 20:44:37 +08:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OPC/goauto#156