2026-05-17-permission-system-fix-design.md 8.8 KB

权限系统全面修复设计文档

背景

SQL 模型已定义完整的权限体系(sys_menu, sys_role, sys_role_menu, sys_role_data_scope_detail, sys_user_role),但后端和前端只实现了 CRUD 和 UI 配置,权限执行层几乎为空。本次修复补全所有缺失的权限执行逻辑。

修复范围

# 模块 优先级 描述
1 后端菜单权限守卫 P0 将已有的 require_menu_code() 应用到所有业务路由
2 后端数据权限过滤 P0 创建 data_scope 服务,在业务查询中按部门/用户/自定义范围过滤
3 前端用户角色选择器 P1 用户管理弹窗中添加角色多选下拉框
4 前端按钮级权限 P1 业务页面按钮添加 v-auth / hasAuth 权限控制
5 admin 无权限码安全漏洞 P2 修复空权限码返回 {all: true} 的问题
6 多角色权限合并 P2 后端菜单码已天然合并;数据权限需实现并集逻辑

模块 1:后端菜单权限守卫

现状

  • app/core/menu_guard.py 已定义 require_menu_code(menu_code) 函数
  • 函数逻辑:super_admin 放行 → 查用户菜单权限码 → 不匹配则 403
  • 但从未在任何路由中 import 或使用

修改方案

在业务路由的每个端点上叠加 Depends(require_menu_code("xxx")) 作为第二层守卫。

原则:

  • 系统管理接口(租户、角色、菜单 CRUD)保持 require_admin_or_super_admin,仅 super_admin 可操作
  • 业务接口使用 require_menu_code
  • 列表接口用 xxx:list,创建用 xxx:add,更新用 xxx:edit,删除用 xxx:delete
  • require_menu_code 内部已先调用 require_admin_or_super_admin,所以路由只需一个 Depends

影响文件(注:业务 API 文件已在工程清理中移除,仅 admin.py / workflow.py / leave.py 保留):

  • backend/app/api/admin.py — 系统管理部分(用户管理加 system:user:list 等)
  • backend/app/api/workflow.py — 工作流接口
  • backend/app/api/leave.py — 请假申请

perms 值来源:直接使用 sys_menu 表中已定义的 perms 字段值。

需要新增的 sys_menu 按钮(F)类型记录

当前 SQL 中 sys_menu 大多只有 M(目录) 和 C(页面) 类型。为了让 require_menu_code 能精确控制到增删改操作,需要在 SQL 中为每个 C 类型菜单补充 F 类型按钮权限:

  • xxx:list — 查看列表
  • xxx:add — 新增
  • xxx:edit — 编辑
  • xxx:delete — 删除

这些 F 类型记录的 parent_id 指向对应的 C 类型菜单。此步骤为 数据初始化,需生成 SQL 脚本。


模块 2:后端数据权限过滤

现状

  • sys_role.data_scope 字段存在(1-5 级别)
  • sys_role_data_scope_detail 表和 CRUD 已实现
  • 没有任何业务查询读取 data_scope 进行过滤

设计

新文件:backend/app/core/data_scope.py

def apply_data_scope(query, model, user_info: dict, db: Session) -> query:
    """在 SQLAlchemy query 上叠加数据权限过滤条件。

    参数:
        query: 原始 SQLAlchemy query 对象
        model: ORM 模型类(需要有 create_dept_id, create_by 字段)
        user_info: get_current_user 返回的 dict(含 user_id, dept_id)
        db: 数据库 session

    返回:
        添加了数据权限过滤条件的 query
    """

过滤逻辑

data_scope 条件
1 全部数据 无附加过滤
2 本部门 model.create_dept_id == user.dept_id
3 本部门及以下 model.create_dept_id IN (user.dept_id + 子部门ID列表)
4 仅本人 model.create_by == user.user_id
5 自定义 并集:(create_dept_id IN 自定义部门) OR (create_by IN 自定义用户)

多角色合并(并集语义)

用户可能有多个角色,每个角色有自己的 data_scope。合并策略:

  1. 任一角色 data_scope=1(全部) → 不过滤
  2. 否则,收集所有角色的有效过滤范围,取并集
  3. 并集计算:将所有 scope 2/3 转为具体部门 ID 集合,scope 4 转为用户 ID 集合,scope 5 直接用自定义范围 → 全部合并后构建 OR 条件

集成方式

在 admin.py 等路由的 list 接口中:

@router.get("/customers")
async def list_customers(
    ...,
    user: dict = Depends(require_menu_code("biz:customer:list")),
    db: Session = Depends(get_db),
):
    query = db.query(BizCustomer).filter(BizCustomer.del_flag == 0, ...)
    query = apply_data_scope(query, BizCustomer, user, db)
    ...

依赖信息

需要 user_info dict 中包含 dept_id。当前 JWT payload 中 region_id 即为 dept_id(在 public.py 登录时已设置)。


模块 3:前端用户角色选择器

现状

  • 后端 UserCreateBody / UserUpdateBody 已支持 roleIds 字段
  • 后端 GET /web/admin/roles/options 已返回角色下拉列表
  • web-admin/src/views/system/user/index.vue 表单中没有角色选择器

修改方案

在 user/index.vue 的创建/编辑弹窗中:

  1. 添加角色多选下拉框(el-select multiple)
  2. 调用 GET /web/admin/roles/options 获取角色列表
  3. formData 中添加 roleIds 字段
  4. 提交时一并传给后端

参考:modules/user-dialog.vue 中已有类似实现,可参考其角色选择逻辑。


模块 4:前端按钮级权限

现状

  • v-auth 指令已实现(读取 route.meta.authList)
  • hasAuth() composable 已实现
  • 但实际业务页面中几乎未使用

修改方案

在每个业务页面的操作按钮上添加权限控制:

<!-- 新增按钮 -->
<el-button v-if="hasAuth('add')" type="primary" @click="handleAdd">新增</el-button>

<!-- 编辑按钮 -->
<el-button v-if="hasAuth('edit')" link type="primary" @click="handleEdit(row)">编辑</el-button>

<!-- 删除按钮 -->
<el-button v-if="hasAuth('delete')" link type="danger" @click="handleDelete(row)">删除</el-button>

权限标识来源:sys_menu 中 menu_type='F' 的按钮权限记录的 perms 字段。

注意:当前 v-auth 指令从 route.meta.authList 读取,而 hasAuth() 从 userStore.getUserInfo.buttons 读取。需确保按钮权限列表在登录时正确加载到这两个位置。

涉及页面

  • src/views/system/user/index.vue
  • src/views/system/role/index.vue(角色管理本身也需按钮权限)
  • src/views/basic-info/customer/index.vue
  • src/views/basic-info/cell-sample-type/index.vue
  • src/views/basic-info/collection-region/index.vue
  • src/views/basic-info/collection-protocol-no/index.vue
  • src/views/basic-info/sample-code/index.vue
  • 其他业务页面

模块 5:admin 无权限码安全修复

现状

backend/app/api/public.py 中 /web/auth/menu-codes 端点:

if not codes:
    return {"isSuperAdmin": False, "all": True, "menuCodes": []}  # ← 安全漏洞

当 admin 用户没有任何菜单权限时,返回 all: true,前端将此视为拥有所有权限。

修改方案

后端:当 codes 为空时返回 all: false,不再赋予全部权限。

if not codes:
    return {"isSuperAdmin": False, "all": False, "menuCodes": []}

前端:确保 MenuProcessor.ts 正确处理 all: false + 空 menuCodes 的情况——显示无菜单/无权限页面。


模块 6:多角色权限合并

后端菜单权限码

iter_user_menu_perm_codes() 已通过 sys_user_role → sys_role_menu → sys_menu 链路查询,天然支持多角色菜单并集(DISTINCT 去重)。无需修改。

后端数据权限

新增函数 resolve_user_data_scope_union(db, user_id) -> DataScopeFilter:

  1. 查询用户所有有效角色的 data_scope 和 scope_details
  2. 合并策略:
    • 任一角色 scope=1 → 返回 "全部"(不过滤)
    • 收集所有角色的有效范围:
      • scope=2 → 加入 user.dept_id 到部门集合
      • scope=3 → 加入 user.dept_id + 所有子部门到部门集合
      • scope=4 → 加入 user.user_id 到用户集合
      • scope=5 → 加入 scope_details 中的 dept 和 user 到各自集合
    • 最终条件:create_dept_id IN 部门集合 OR create_by IN 用户集合
  3. 返回统一的过滤条件对象

不在本次修复范围

  • 菜单祖先自动包含(P3,前端已通过 el-tree 不设 check-strictly 间接处理)
  • 工作流权限控制(独立模块,另行设计)
  • 小程序端权限(独立模块)

影响评估

  • 后端:新增 1 个文件(data_scope.py),修改 ~6 个路由文件
  • 前端:修改 ~10 个页面文件,不新增文件
  • 数据库:新增 sys_menu F 类型按钮权限记录的 SQL 脚本
  • 风险:低。所有修改基于已有的基础设施,不改变数据模型