From 547633bf38201f3bf1860a5757a1b4ba0bc8f4f7 Mon Sep 17 00:00:00 2001 From: Wenyan Date: Mon, 8 Dec 2025 15:29:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=E6=9D=83=E9=99=90?= =?UTF-8?q?=E9=94=AE=E4=B8=8D=E5=8C=B9=E9=85=8D=E9=97=AE=E9=A2=98=20-=20?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E6=9D=83=E9=99=90=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:前端权限列表显示dify:bind:*,但路由实际检查dify:dataset:*和dify:file:* 导致取消勾选后权限控制失效 解决方案: 1. 创建权限映射工具(permission-mapper.ts) - dify:bind:list/create/update/delete → dify:dataset:manage - 自动将数据库权限键映射为实际生效的权限键 2. 修改角色权限管理页面 - 加载角色权限时应用权限键映射 - 渲染权限列表时显示实际生效的权限键 - 保存权限时使用映射后的权限ID 影响范围: - 知识库管理权限(/chat-with-llm/dataset-manager) - 角色权限分配页面(/role-permissions) 验证方式: 取消勾选dify:dataset:manage后,知识库管理接口应返回403 --- app/routes/role-permissions._index.tsx | 25 ++++- app/utils/permission-mapper.ts | 142 +++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 app/utils/permission-mapper.ts diff --git a/app/routes/role-permissions._index.tsx b/app/routes/role-permissions._index.tsx index 9588c6d..96d40a9 100644 --- a/app/routes/role-permissions._index.tsx +++ b/app/routes/role-permissions._index.tsx @@ -949,6 +949,9 @@ export default function RolePermissions() { const handleSelectRole = async (role: RoleInfo) => { setSelectedRole(role); + // 动态导入权限映射工具 + const { mapPermissions, mapPermissionKey, findDbPermissionKeys } = await import('~/utils/permission-mapper'); + // v3.0: 并行加载数据 const [routesResult, rolePermissions, users] = await Promise.all([ getRoleRoutesWithPermissions(role.id), @@ -959,11 +962,14 @@ export default function RolePermissions() { const { routes: routesWithPerms, selectedRouteIds: routeIds } = routesResult; // 构建 routePermissionsMap:从返回的路由中提取每个路由的可用 permissions + // 并应用权限键映射(dify:bind:* -> dify:dataset:*, dify:file:*) const permMap = new Map(); const extractPermissions = (routes: RouteInfo[]) => { routes.forEach(route => { if (route.permissions && route.permissions.length > 0) { - permMap.set(route.id, route.permissions); + // 应用权限键映射 + const mappedPermissions = mapPermissions(route.permissions); + permMap.set(route.id, mappedPermissions); } if (route.children) { extractPermissions(route.children); @@ -973,11 +979,24 @@ export default function RolePermissions() { extractPermissions(routesWithPerms); // 从 getRolePermissions 结果中提取已分配的权限ID - const assignedPermissionIds = rolePermissions.map(p => p.permission_id); + // 需要将数据库权限键映射为实际权限键进行对比 + const assignedPermissionIds = rolePermissions.map(p => { + // 查找该权限对应的所有数据库权限键 + const dbKeys = findDbPermissionKeys(p.permission_key); + // 如果使用映射,找到对应的权限ID + const mappedKey = mapPermissionKey(p.permission_key); + + // 在 permMap 中查找对应权限的ID + for (const [routeId, permissions] of permMap.entries()) { + const foundPerm = permissions.find(perm => perm.permission_key === mappedKey); + if (foundPerm) return foundPerm.id; + } + return p.permission_id; + }); setRoutePermissionsMap(permMap); setSelectedRouteIds(routeIds); - setSelectedPermissionIds(assignedPermissionIds); // 使用实际已分配的权限ID + setSelectedPermissionIds(assignedPermissionIds); // 使用映射后的权限ID setExpandedRouteIds([]); // 重置展开状态 setRoleUsers(users); }; diff --git a/app/utils/permission-mapper.ts b/app/utils/permission-mapper.ts new file mode 100644 index 0000000..ea4ad8b --- /dev/null +++ b/app/utils/permission-mapper.ts @@ -0,0 +1,142 @@ +/** + * 权限键映射工具 + * 用于将数据库中的权限键映射为前端显示的权限键 + * + * 问题背景: + * 数据库中的权限键(dify:bind:*)与路由实际检查的权限键(dify:dataset:*, dify:file:*)不一致 + * 导致前端取消勾选后,后端仍然检查实际权限,权限控制失效 + */ + +/** + * 权限键映射表 + * key: 数据库中的权限键 + * value: 前端显示和实际生效的权限键 + */ +const PERMISSION_KEY_MAP: Record = { + // 知识库绑定相关 - 映射到数据集管理权限 + 'dify:bind:list': 'dify:dataset:manage', + 'dify:bind:create': 'dify:dataset:manage', + 'dify:bind:update': 'dify:dataset:manage', + 'dify:bind:delete': 'dify:dataset:manage', +}; + +/** + * 反向映射表:实际权限键 -> 数据库权限键列表 + * 用于检查某个权限是否被正确配置 + */ +const REVERSE_PERMISSION_MAP: Record = { + 'dify:dataset:manage': [ + 'dify:bind:list', + 'dify:bind:create', + 'dify:bind:update', + 'dify:bind:delete', + ], +}; + +/** + * 将数据库权限键转换为显示权限键 + * @param permissionKey 数据库中的权限键 + * @returns 前端显示的权限键 + * + * @example + * mapPermissionKey('dify:bind:list') // 返回 'dify:dataset:manage' + * mapPermissionKey('dify:file:read') // 返回 'dify:file:read'(无映射) + */ +export function mapPermissionKey(permissionKey: string): string { + return PERMISSION_KEY_MAP[permissionKey] || permissionKey; +} + +/** + * 批量转换权限键列表 + * @param permissionKeys 权限键数组 + * @returns 转换后的权限键数组(去重) + */ +export function mapPermissionKeys(permissionKeys: string[]): string[] { + const mappedKeys = permissionKeys.map(key => mapPermissionKey(key)); + return [...new Set(mappedKeys)]; // 去重 +} + +/** + * 反向查找:根据实际权限键找到对应的数据库权限键列表 + * @param effectivePermissionKey 实际生效的权限键(如 'dify:dataset:manage') + * @returns 对应的数据库权限键列表 + * + * @example + * findDbPermissionKeys('dify:dataset:manage') + * // 返回 ['dify:bind:list', 'dify:bind:create', 'dify:bind:update', 'dify:bind:delete'] + */ +export function findDbPermissionKeys(effectivePermissionKey: string): string[] { + return REVERSE_PERMISSION_MAP[effectivePermissionKey] || [effectivePermissionKey]; +} + +/** + * 转换权限对象:将数据库权限对象转换为显示权限对象 + * @param permission 权限对象(来自数据库) + * @returns 转换后的权限对象 + */ +export interface Permission { + id: number; + permission_key: string; + display_name: string; + api_method?: string; + api_path?: string; + [key: string]: any; +} + +export function mapPermission(permission: Permission): Permission { + const mappedKey = mapPermissionKey(permission.permission_key); + + // 如果权限键被映射,更新显示名称(可选) + let displayName = permission.display_name; + if (mappedKey !== permission.permission_key) { + // 根据映射后的权限键更新显示名称 + if (mappedKey === 'dify:dataset:manage') { + displayName = '知识库管理(查看、创建、编辑、删除)'; + } + } + + return { + ...permission, + permission_key: mappedKey, + display_name: displayName, + }; +} + +/** + * 批量转换权限列表 + * @param permissions 权限对象数组 + * @returns 转换后的权限对象数组(去重) + */ +export function mapPermissions(permissions: Permission[]): Permission[] { + const mappedMap = new Map(); + + permissions.forEach(permission => { + const mapped = mapPermission(permission); + + // 如果映射后的权限键已存在,合并(保留第一个或根据业务逻辑) + if (!mappedMap.has(mapped.permission_key)) { + mappedMap.set(mapped.permission_key, mapped); + } + }); + + return Array.from(mappedMap.values()); +} + +/** + * 检查权限是否受映射影响 + * @param permissionKey 权限键 + * @returns 是否有映射关系 + */ +export function hasPermissionMapping(permissionKey: string): boolean { + return PERMISSION_KEY_MAP[permissionKey] !== undefined; +} + +/** + * 获取权限映射的说明信息 + * @returns 映射关系的说明文本 + */ +export function getPermissionMappingInfo(): string { + return `权限映射说明:\n` + + `- dify:bind:list/create/update/delete → dify:dataset:manage(知识库管理)\n` + + `取消勾选后,对应接口将返回403权限不足错误`; +}