forked from wangziqi/gongxue-base
fix: prune unused RBAC permissions
This commit is contained in:
@@ -9,7 +9,7 @@ export function getAttendanceExperience(
|
|||||||
): AttendanceExperience {
|
): AttendanceExperience {
|
||||||
const domains = getRoleDomains(roles, permissions);
|
const domains = getRoleDomains(roles, permissions);
|
||||||
if (
|
if (
|
||||||
permissions.includes('attendance:manage') ||
|
permissions.includes('attendance:edit') ||
|
||||||
domains.has('academic') ||
|
domains.has('academic') ||
|
||||||
domains.has('super')
|
domains.has('super')
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -37,13 +37,9 @@ const PermissionsPage: React.FC = () => {
|
|||||||
class: '班级管理',
|
class: '班级管理',
|
||||||
schedule: '排课管理',
|
schedule: '排课管理',
|
||||||
attendance: '考勤管理',
|
attendance: '考勤管理',
|
||||||
learning: '学习记录',
|
|
||||||
exam: '考试管理',
|
|
||||||
sync: '数据同步',
|
sync: '数据同步',
|
||||||
integration: '集成配置',
|
integration: '集成配置',
|
||||||
department: '部门管理',
|
|
||||||
notification: '通知中心',
|
notification: '通知中心',
|
||||||
profile: '个人资料',
|
|
||||||
ai: 'AI 模型配置',
|
ai: 'AI 模型配置',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -118,18 +118,30 @@ const RolesPage: React.FC = () => {
|
|||||||
|
|
||||||
const groupNames: Record<string, string> = {
|
const groupNames: Record<string, string> = {
|
||||||
dashboard: '数据面板',
|
dashboard: '数据面板',
|
||||||
|
notification: '通知中心',
|
||||||
student: '学生管理',
|
student: '学生管理',
|
||||||
|
'student-scope': '学生范围权限',
|
||||||
|
teacher: '教师管理',
|
||||||
|
'teacher-workspace': '教师工作台',
|
||||||
room: '宿舍管理',
|
room: '宿舍管理',
|
||||||
occupancy: '入住管理',
|
occupancy: '入住管理',
|
||||||
expense: '费用管理',
|
expense: '费用管理',
|
||||||
bill: '账单管理',
|
bill: '账单管理',
|
||||||
deposit: '押金管理',
|
deposit: '押金管理',
|
||||||
|
wallet: '学生余额',
|
||||||
classroom: '教室管理',
|
classroom: '教室管理',
|
||||||
organization: '机构管理',
|
organization: '机构管理',
|
||||||
rental: '租赁订单',
|
rental: '租赁订单',
|
||||||
|
class: '班级管理',
|
||||||
|
schedule: '排课管理',
|
||||||
|
attendance: '考勤管理',
|
||||||
|
'attendance-scope': '考勤范围权限',
|
||||||
log: '操作日志',
|
log: '操作日志',
|
||||||
user: '用户管理',
|
user: '用户管理',
|
||||||
role: '角色管理',
|
role: '角色管理',
|
||||||
|
sync: '数据同步',
|
||||||
|
integration: '集成配置',
|
||||||
|
ai: 'AI 配置',
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = useMemo(() => [
|
const columns = useMemo(() => [
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ describe('preset role permissions', () => {
|
|||||||
it('keeps teachers read-only in scheduling while preserving class attendance access', () => {
|
it('keeps teachers read-only in scheduling while preserving class attendance access', () => {
|
||||||
const teacher = permissionsFor('teacher');
|
const teacher = permissionsFor('teacher');
|
||||||
|
|
||||||
expect(teacher.groups).toEqual(['notification', 'profile']);
|
expect(teacher.groups).toEqual(['notification']);
|
||||||
expect(teacher.extras).toEqual(
|
expect(teacher.extras).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
'teacher-workspace:view',
|
'teacher-workspace:view',
|
||||||
|
|||||||
@@ -30,7 +30,13 @@ describe('RbacService seedData', () => {
|
|||||||
),
|
),
|
||||||
create: jest.fn((value) => value),
|
create: jest.fn((value) => value),
|
||||||
save: jest.fn(async (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 = {
|
const roleRepo = {
|
||||||
findOne: jest.fn(async ({ where }: any) =>
|
findOne: jest.fn(async ({ where }: any) =>
|
||||||
@@ -58,7 +64,7 @@ describe('RbacService seedData', () => {
|
|||||||
expect(teacherRole.name).toBe('任课老师');
|
expect(teacherRole.name).toBe('任课老师');
|
||||||
expect(teacherRole.permissions.map((permission) => permission.code)).toEqual(
|
expect(teacherRole.permissions.map((permission) => permission.code)).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
'profile:view',
|
'notification:view',
|
||||||
'teacher-workspace:view',
|
'teacher-workspace:view',
|
||||||
'schedule:view',
|
'schedule:view',
|
||||||
'attendance:create',
|
'attendance:create',
|
||||||
@@ -67,6 +73,7 @@ describe('RbacService seedData', () => {
|
|||||||
expect(teacherRole.permissions.map((permission) => permission.code)).not.toContain(
|
expect(teacherRole.permissions.map((permission) => permission.code)).not.toContain(
|
||||||
'schedule:create',
|
'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('student:view');
|
||||||
expect(teacherRole.permissions.map((permission) => permission.code)).not.toContain('class:view');
|
expect(teacherRole.permissions.map((permission) => permission.code)).not.toContain('class:view');
|
||||||
expect(teacherRole.permissions.map((permission) => permission.code)).not.toContain('room: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),
|
findOne: jest.fn(async ({ where }: any) => permissions.find((item) => item.code === where.code) ?? null),
|
||||||
create: jest.fn((value) => value),
|
create: jest.fn((value) => value),
|
||||||
save: jest.fn(async (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 = {
|
const roleRepo = {
|
||||||
findOne: jest.fn(async () => targetRole),
|
findOne: jest.fn(async () => targetRole),
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
|
|
||||||
const PRESET_PERMISSIONS: Array<{ code: string; name: string; group: string }> = [
|
const PRESET_PERMISSIONS: Array<{ code: string; name: string; group: string }> = [
|
||||||
{ code: 'dashboard:view', name: '查看数据面板', group: 'dashboard' },
|
{ code: 'dashboard:view', name: '查看数据面板', group: 'dashboard' },
|
||||||
{ code: 'profile:view', name: '查看个人资料', group: 'profile' },
|
|
||||||
{ code: 'notification:view', name: '查看通知', group: 'notification' },
|
{ code: 'notification:view', name: '查看通知', group: 'notification' },
|
||||||
{ code: 'student:view', name: '查看学生管理', group: 'student' },
|
{ code: 'student:view', name: '查看学生管理', group: 'student' },
|
||||||
{ code: 'student:basic-view', name: '查看学生基础信息', group: 'student-scope' },
|
{ 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:edit', name: '编辑全部考勤', group: 'attendance' },
|
||||||
{ code: 'attendance:self-edit', name: '编辑任教班级考勤', group: 'attendance-scope' },
|
{ code: 'attendance:self-edit', name: '编辑任教班级考勤', group: 'attendance-scope' },
|
||||||
{ code: 'attendance:export', name: '导出考勤', group: 'attendance' },
|
{ 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:trigger', name: '触发数据同步', group: 'sync' },
|
||||||
{ code: 'sync:read', name: '查看同步状态', group: 'sync' },
|
{ code: 'sync:read', name: '查看同步状态', group: 'sync' },
|
||||||
{ code: 'integration:trigger', name: '触发集成', group: 'integration' },
|
{ code: 'integration:trigger', name: '触发集成', group: 'integration' },
|
||||||
{ code: 'integration:read', 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:read', name: '查看 AI 配置', group: 'ai' },
|
||||||
{ code: 'ai:config:write', name: '修改 AI 配置', group: 'ai' },
|
{ code: 'ai:config:write', name: '修改 AI 配置', group: 'ai' },
|
||||||
{ code: 'ai:config:test', 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<{
|
export const PRESET_ROLES: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
@@ -130,7 +166,7 @@ export const PRESET_ROLES: Array<{
|
|||||||
code: 'teacher',
|
code: 'teacher',
|
||||||
description: '查看自己的排课、今日课程和任教班级考勤',
|
description: '查看自己的排课、今日课程和任教班级考勤',
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
permissionGroups: ['notification', 'profile'],
|
permissionGroups: ['notification'],
|
||||||
extraPermissions: [
|
extraPermissions: [
|
||||||
'teacher-workspace:view',
|
'teacher-workspace:view',
|
||||||
'schedule:view',
|
'schedule:view',
|
||||||
@@ -151,11 +187,8 @@ export const PRESET_ROLES: Array<{
|
|||||||
'schedule',
|
'schedule',
|
||||||
'attendance',
|
'attendance',
|
||||||
'classroom',
|
'classroom',
|
||||||
'learning',
|
|
||||||
'exam',
|
|
||||||
'dashboard',
|
'dashboard',
|
||||||
'notification',
|
'notification',
|
||||||
'profile',
|
|
||||||
],
|
],
|
||||||
extraPermissions: [
|
extraPermissions: [
|
||||||
'teacher-workspace:view',
|
'teacher-workspace:view',
|
||||||
@@ -180,7 +213,6 @@ export const PRESET_ROLES: Array<{
|
|||||||
'wallet',
|
'wallet',
|
||||||
'dashboard',
|
'dashboard',
|
||||||
'notification',
|
'notification',
|
||||||
'profile',
|
|
||||||
],
|
],
|
||||||
extraPermissions: ['student:basic-view'],
|
extraPermissions: ['student:basic-view'],
|
||||||
legacyNames: ['宿管老师', '宿管', '财务'],
|
legacyNames: ['宿管老师', '宿管', '财务'],
|
||||||
@@ -191,7 +223,7 @@ export const PRESET_ROLES: Array<{
|
|||||||
code: 'classroom_operations',
|
code: 'classroom_operations',
|
||||||
description: '管理教室、教室排期、外部机构和租赁订单',
|
description: '管理教室、教室排期、外部机构和租赁订单',
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
permissionGroups: ['classroom', 'rental', 'organization', 'notification', 'profile'],
|
permissionGroups: ['classroom', 'rental', 'organization', 'notification'],
|
||||||
legacyNames: ['机构负责人'],
|
legacyNames: ['机构负责人'],
|
||||||
legacyCodes: ['institution_head'],
|
legacyCodes: ['institution_head'],
|
||||||
},
|
},
|
||||||
@@ -207,9 +239,7 @@ export const PRESET_ROLES: Array<{
|
|||||||
'integration',
|
'integration',
|
||||||
'sync',
|
'sync',
|
||||||
'ai',
|
'ai',
|
||||||
'department',
|
|
||||||
'notification',
|
'notification',
|
||||||
'profile',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -253,7 +283,8 @@ export class RbacService {
|
|||||||
where: { code: 'user:delete' },
|
where: { code: 'user:delete' },
|
||||||
});
|
});
|
||||||
const allPerms = (await this.permRepo.find()).filter(
|
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: 幂等插入预置角色
|
// Step 2: 幂等插入预置角色
|
||||||
@@ -288,6 +319,22 @@ export class RbacService {
|
|||||||
await this.permRepo.remove(deprecatedUserDeletePermission);
|
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: 合并旧角色并构建新的职责权限矩阵
|
// Step 3: 合并旧角色并构建新的职责权限矩阵
|
||||||
for (const preset of PRESET_ROLES) {
|
for (const preset of PRESET_ROLES) {
|
||||||
const matchesPreset = (role: Role) =>
|
const matchesPreset = (role: Role) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user