From 7edb4ff853da5417775b7ac7784074ceeb97c102 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Fri, 17 Jul 2026 17:43:45 +0800 Subject: [PATCH] fix: prune unused RBAC permissions --- .../pages/Attendance/attendance-workspace.ts | 2 +- apps/admin/src/pages/Permissions/index.tsx | 4 - apps/admin/src/pages/Roles/index.tsx | 12 +++ apps/server/src/rbac/rbac.permissions.spec.ts | 2 +- apps/server/src/rbac/rbac.seed.spec.ts | 19 +++- apps/server/src/rbac/rbac.service.ts | 87 ++++++++++++++----- 6 files changed, 97 insertions(+), 29 deletions(-) diff --git a/apps/admin/src/pages/Attendance/attendance-workspace.ts b/apps/admin/src/pages/Attendance/attendance-workspace.ts index dcaf3b9..c49485a 100644 --- a/apps/admin/src/pages/Attendance/attendance-workspace.ts +++ b/apps/admin/src/pages/Attendance/attendance-workspace.ts @@ -9,7 +9,7 @@ export function getAttendanceExperience( ): AttendanceExperience { const domains = getRoleDomains(roles, permissions); if ( - permissions.includes('attendance:manage') || + permissions.includes('attendance:edit') || domains.has('academic') || domains.has('super') ) { diff --git a/apps/admin/src/pages/Permissions/index.tsx b/apps/admin/src/pages/Permissions/index.tsx index 91607dc..d6e0687 100644 --- a/apps/admin/src/pages/Permissions/index.tsx +++ b/apps/admin/src/pages/Permissions/index.tsx @@ -37,13 +37,9 @@ const PermissionsPage: React.FC = () => { class: '班级管理', schedule: '排课管理', attendance: '考勤管理', - learning: '学习记录', - exam: '考试管理', sync: '数据同步', integration: '集成配置', - department: '部门管理', notification: '通知中心', - profile: '个人资料', ai: 'AI 模型配置', }; diff --git a/apps/admin/src/pages/Roles/index.tsx b/apps/admin/src/pages/Roles/index.tsx index 1bf7be4..7e6bac2 100644 --- a/apps/admin/src/pages/Roles/index.tsx +++ b/apps/admin/src/pages/Roles/index.tsx @@ -118,18 +118,30 @@ const RolesPage: React.FC = () => { const groupNames: Record = { dashboard: '数据面板', + notification: '通知中心', student: '学生管理', + 'student-scope': '学生范围权限', + teacher: '教师管理', + 'teacher-workspace': '教师工作台', room: '宿舍管理', occupancy: '入住管理', expense: '费用管理', bill: '账单管理', deposit: '押金管理', + wallet: '学生余额', classroom: '教室管理', organization: '机构管理', rental: '租赁订单', + class: '班级管理', + schedule: '排课管理', + attendance: '考勤管理', + 'attendance-scope': '考勤范围权限', log: '操作日志', user: '用户管理', role: '角色管理', + sync: '数据同步', + integration: '集成配置', + ai: 'AI 配置', }; const columns = useMemo(() => [ diff --git a/apps/server/src/rbac/rbac.permissions.spec.ts b/apps/server/src/rbac/rbac.permissions.spec.ts index 4c14765..3a8d1ac 100644 --- a/apps/server/src/rbac/rbac.permissions.spec.ts +++ b/apps/server/src/rbac/rbac.permissions.spec.ts @@ -10,7 +10,7 @@ describe('preset role permissions', () => { it('keeps teachers read-only in scheduling while preserving class attendance access', () => { const teacher = permissionsFor('teacher'); - expect(teacher.groups).toEqual(['notification', 'profile']); + expect(teacher.groups).toEqual(['notification']); expect(teacher.extras).toEqual( expect.arrayContaining([ 'teacher-workspace:view', diff --git a/apps/server/src/rbac/rbac.seed.spec.ts b/apps/server/src/rbac/rbac.seed.spec.ts index 286a020..15ca361 100644 --- a/apps/server/src/rbac/rbac.seed.spec.ts +++ b/apps/server/src/rbac/rbac.seed.spec.ts @@ -30,7 +30,13 @@ describe('RbacService seedData', () => { ), create: jest.fn((value) => value), save: jest.fn(async (value) => value), - find: jest.fn(async () => permissions), + find: jest.fn(async (options?: any) => { + if (options?.where?.code) { + return permissions.filter((permission) => permission.code === 'profile:view'); + } + return permissions; + }), + remove: jest.fn(async (value) => value), }; const roleRepo = { findOne: jest.fn(async ({ where }: any) => @@ -58,7 +64,7 @@ describe('RbacService seedData', () => { expect(teacherRole.name).toBe('任课老师'); expect(teacherRole.permissions.map((permission) => permission.code)).toEqual( expect.arrayContaining([ - 'profile:view', + 'notification:view', 'teacher-workspace:view', 'schedule:view', 'attendance:create', @@ -67,6 +73,7 @@ describe('RbacService seedData', () => { expect(teacherRole.permissions.map((permission) => permission.code)).not.toContain( 'schedule:create', ); + expect(teacherRole.permissions.map((permission) => permission.code)).not.toContain('profile:view'); expect(teacherRole.permissions.map((permission) => permission.code)).not.toContain('student:view'); expect(teacherRole.permissions.map((permission) => permission.code)).not.toContain('class:view'); expect(teacherRole.permissions.map((permission) => permission.code)).not.toContain('room:view'); @@ -106,7 +113,13 @@ describe('RbacService legacy role consolidation', () => { findOne: jest.fn(async ({ where }: any) => permissions.find((item) => item.code === where.code) ?? null), create: jest.fn((value) => value), save: jest.fn(async (value) => value), - find: jest.fn(async () => permissions), + find: jest.fn(async (options?: any) => { + if (options?.where?.code) { + return permissions.filter((permission) => permission.code === 'profile:view'); + } + return permissions; + }), + remove: jest.fn(async (value) => value), }; const roleRepo = { findOne: jest.fn(async () => targetRole), diff --git a/apps/server/src/rbac/rbac.service.ts b/apps/server/src/rbac/rbac.service.ts index 6342185..6187a8e 100644 --- a/apps/server/src/rbac/rbac.service.ts +++ b/apps/server/src/rbac/rbac.service.ts @@ -15,7 +15,6 @@ import { const PRESET_PERMISSIONS: Array<{ code: string; name: string; group: string }> = [ { code: 'dashboard:view', name: '查看数据面板', group: 'dashboard' }, - { code: 'profile:view', name: '查看个人资料', group: 'profile' }, { code: 'notification:view', name: '查看通知', group: 'notification' }, { code: 'student:view', name: '查看学生管理', group: 'student' }, { code: 'student:basic-view', name: '查看学生基础信息', group: 'student-scope' }, @@ -88,25 +87,62 @@ const PRESET_PERMISSIONS: Array<{ code: string; name: string; group: string }> = { code: 'attendance:edit', name: '编辑全部考勤', group: 'attendance' }, { code: 'attendance:self-edit', name: '编辑任教班级考勤', group: 'attendance-scope' }, { code: 'attendance:export', name: '导出考勤', group: 'attendance' }, - { code: 'attendance:generate', name: '按课表生成考勤', group: 'attendance' }, - { code: 'learning:create', name: '创建学习任务', group: 'learning' }, - { code: 'learning:edit', name: '编辑学习任务', group: 'learning' }, - { code: 'learning:delete', name: '删除学习任务', group: 'learning' }, - { code: 'exam:create', name: '创建考试', group: 'exam' }, - { code: 'exam:edit', name: '编辑考试', group: 'exam' }, - { code: 'exam:delete', name: '删除考试', group: 'exam' }, { code: 'sync:trigger', name: '触发数据同步', group: 'sync' }, { code: 'sync:read', name: '查看同步状态', group: 'sync' }, { code: 'integration:trigger', name: '触发集成', group: 'integration' }, { code: 'integration:read', name: '查看集成状态', group: 'integration' }, - { code: 'department:view', name: '查看部门', group: 'department' }, - { code: 'department:edit', name: '编辑部门', group: 'department' }, - { code: 'department:delete', name: '删除部门', group: 'department' }, { code: 'ai:config:read', name: '查看 AI 配置', group: 'ai' }, { code: 'ai:config:write', name: '修改 AI 配置', group: 'ai' }, { code: 'ai:config:test', name: '测试 AI 连接', group: 'ai' }, ]; +const DEPRECATED_PERMISSION_CODES = [ + 'profile:view', + 'attendance:generate', + 'learning:create', + 'learning:edit', + 'learning:delete', + 'exam:create', + 'exam:edit', + 'exam:delete', + 'department:view', + 'department:edit', + 'department:delete', + // Legacy permission codes from older admin UI / seed data. + 'student:add', + 'student:update', + 'room:add', + 'room:update', + 'occupancy:add', + 'occupancy:update', + 'attendance:add', + 'attendance:update', + 'attendance:delete', + 'attendance:batch', + 'bill:export', + 'deposit:collect', + 'expense:add', + 'expense:update', + 'class:add', + 'class:update', + 'schedule:add', + 'schedule:update', + 'classroom:add', + 'classroom:update', + 'rental:add', + 'rental:update', + 'role:add', + 'role:update', + 'user:add', + 'user:update', + 'archive:view', + 'archive:import', + 'archive:export', + 'report:generate', +] as const; + +const DEPRECATED_PERMISSION_CODE_SET = new Set(DEPRECATED_PERMISSION_CODES); + export const PRESET_ROLES: Array<{ name: string; code: string; @@ -130,7 +166,7 @@ export const PRESET_ROLES: Array<{ code: 'teacher', description: '查看自己的排课、今日课程和任教班级考勤', isSystem: true, - permissionGroups: ['notification', 'profile'], + permissionGroups: ['notification'], extraPermissions: [ 'teacher-workspace:view', 'schedule:view', @@ -151,11 +187,8 @@ export const PRESET_ROLES: Array<{ 'schedule', 'attendance', 'classroom', - 'learning', - 'exam', 'dashboard', 'notification', - 'profile', ], extraPermissions: [ 'teacher-workspace:view', @@ -180,7 +213,6 @@ export const PRESET_ROLES: Array<{ 'wallet', 'dashboard', 'notification', - 'profile', ], extraPermissions: ['student:basic-view'], legacyNames: ['宿管老师', '宿管', '财务'], @@ -191,7 +223,7 @@ export const PRESET_ROLES: Array<{ code: 'classroom_operations', description: '管理教室、教室排期、外部机构和租赁订单', isSystem: true, - permissionGroups: ['classroom', 'rental', 'organization', 'notification', 'profile'], + permissionGroups: ['classroom', 'rental', 'organization', 'notification'], legacyNames: ['机构负责人'], legacyCodes: ['institution_head'], }, @@ -207,9 +239,7 @@ export const PRESET_ROLES: Array<{ 'integration', 'sync', 'ai', - 'department', 'notification', - 'profile', ], }, ]; @@ -253,7 +283,8 @@ export class RbacService { where: { code: 'user:delete' }, }); const allPerms = (await this.permRepo.find()).filter( - (permission) => permission.code !== 'user:delete', + (permission) => + permission.code !== 'user:delete' && !DEPRECATED_PERMISSION_CODE_SET.has(permission.code), ); // Step 2: 幂等插入预置角色 @@ -288,6 +319,22 @@ export class RbacService { await this.permRepo.remove(deprecatedUserDeletePermission); } + const deprecatedPermissions = await this.permRepo.find({ + where: { code: In([...DEPRECATED_PERMISSION_CODES]) }, + }); + if (deprecatedPermissions.length > 0) { + const deprecatedIds = new Set(deprecatedPermissions.map((permission) => permission.id)); + for (const role of allRoles) { + const permissions = role.permissions ?? []; + if (permissions.some((permission) => deprecatedIds.has(permission.id))) { + role.permissions = permissions.filter((permission) => !deprecatedIds.has(permission.id)); + await this.roleRepo.save(role); + } + } + await this.permRepo.remove(deprecatedPermissions); + this.logger.log(`已清理废弃权限点: ${deprecatedPermissions.map((p) => p.code).join(', ')}`); + } + // Step 3: 合并旧角色并构建新的职责权限矩阵 for (const preset of PRESET_ROLES) { const matchesPreset = (role: Role) =>