forked from wangziqi/gongxue-base
fix: prune unused RBAC permissions
This commit is contained in:
@@ -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')
|
||||
) {
|
||||
|
||||
@@ -37,13 +37,9 @@ const PermissionsPage: React.FC = () => {
|
||||
class: '班级管理',
|
||||
schedule: '排课管理',
|
||||
attendance: '考勤管理',
|
||||
learning: '学习记录',
|
||||
exam: '考试管理',
|
||||
sync: '数据同步',
|
||||
integration: '集成配置',
|
||||
department: '部门管理',
|
||||
notification: '通知中心',
|
||||
profile: '个人资料',
|
||||
ai: 'AI 模型配置',
|
||||
};
|
||||
|
||||
|
||||
@@ -118,18 +118,30 @@ const RolesPage: React.FC = () => {
|
||||
|
||||
const groupNames: Record<string, string> = {
|
||||
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(() => [
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<string>(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) =>
|
||||
|
||||
Reference in New Issue
Block a user