forked from wangziqi/gongxue-base
修复管理员班级可见范围
This commit is contained in:
96
apps/server/src/classes/classes.controller.spec.ts
Normal file
96
apps/server/src/classes/classes.controller.spec.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { ClassesController } from './classes.controller';
|
||||
import { ClassesService } from './classes.service';
|
||||
import { OperationLogsService } from '../operation-logs/operation-logs.service';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { CaslAction, SubjectName } from '../authorization';
|
||||
|
||||
describe('ClassesController - class data scope', () => {
|
||||
const service = {
|
||||
getAccessibleClassIds: jest.fn(),
|
||||
assertClassAccess: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
getSchedule: jest.fn(),
|
||||
getAttendanceSummary: jest.fn(),
|
||||
getStudents: jest.fn(),
|
||||
getTeachers: jest.fn(),
|
||||
};
|
||||
const authzService = { can: jest.fn() };
|
||||
const logService = {};
|
||||
const notificationsService = {};
|
||||
let controller: ClassesController;
|
||||
|
||||
const request = (permissions: string[] = [], isSuperAdmin = false) => ({
|
||||
user: {
|
||||
id: 21,
|
||||
username: 'user',
|
||||
permissions,
|
||||
isSuperAdmin,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
authzService.can.mockImplementation((req, action, subject) => {
|
||||
if (subject !== SubjectName.Class) return false;
|
||||
if (req.user.isSuperAdmin && action === CaslAction.Manage) return true;
|
||||
if (action === CaslAction.Create) return req.user.permissions.includes('class:create');
|
||||
if (action === CaslAction.Update) return req.user.permissions.includes('class:edit');
|
||||
return false;
|
||||
});
|
||||
service.getAccessibleClassIds.mockResolvedValue(undefined);
|
||||
service.assertClassAccess.mockResolvedValue(undefined);
|
||||
service.findAll.mockResolvedValue([]);
|
||||
service.findOne.mockResolvedValue({ id: 8 });
|
||||
service.getSchedule.mockResolvedValue([]);
|
||||
service.getAttendanceSummary.mockResolvedValue({});
|
||||
service.getStudents.mockResolvedValue([]);
|
||||
service.getTeachers.mockResolvedValue([]);
|
||||
controller = new ClassesController(
|
||||
service as unknown as ClassesService,
|
||||
logService as unknown as OperationLogsService,
|
||||
notificationsService as unknown as NotificationsService,
|
||||
authzService as never,
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['class creator', request(['class:view', 'class:create'])],
|
||||
['class editor', request(['class:view', 'class:edit'])],
|
||||
['super admin', request([], true)],
|
||||
])('allows %s to list all classes', async (_label, req) => {
|
||||
await controller.findAll({}, req);
|
||||
|
||||
expect(service.getAccessibleClassIds).toHaveBeenCalledWith(21, true);
|
||||
expect(service.findAll).toHaveBeenCalledWith({}, undefined);
|
||||
});
|
||||
|
||||
it('keeps view-only users scoped to their assigned classes', async () => {
|
||||
service.getAccessibleClassIds.mockResolvedValue([8]);
|
||||
|
||||
await controller.findAll({}, request(['class:view']));
|
||||
|
||||
expect(service.getAccessibleClassIds).toHaveBeenCalledWith(21, false);
|
||||
expect(service.findAll).toHaveBeenCalledWith({}, [8]);
|
||||
});
|
||||
|
||||
it('allows a class creator to read an unassigned class through every detail endpoint', async () => {
|
||||
const req = request(['class:view', 'class:create']);
|
||||
|
||||
await controller.findOne('8', req);
|
||||
await controller.getSchedule('8', {}, req);
|
||||
await controller.getAttendanceSummary('8', {}, req);
|
||||
await controller.getStudents('8', req);
|
||||
await controller.getTeachers('8', req);
|
||||
|
||||
expect(service.assertClassAccess).toHaveBeenCalledTimes(5);
|
||||
expect(service.assertClassAccess).toHaveBeenCalledWith(21, 8, true);
|
||||
});
|
||||
|
||||
it('requires a view-only user to be assigned before reading class details', async () => {
|
||||
await controller.findOne('8', request(['class:view']));
|
||||
|
||||
expect(service.assertClassAccess).toHaveBeenCalledWith(21, 8, false);
|
||||
});
|
||||
});
|
||||
@@ -54,12 +54,20 @@ export class ClassesController {
|
||||
private readonly authz: AuthorizationService,
|
||||
) {}
|
||||
|
||||
private assertReadAccess(req: AuthenticatedRequest, classId: number) {
|
||||
// Legacy: Manage (super_admin) or Update (class:edit) grants broad class access
|
||||
const canManageAll =
|
||||
private canManageAllClasses(req: AuthenticatedRequest): boolean {
|
||||
return (
|
||||
this.authz.can(req, CaslAction.Manage, SubjectName.Class) ||
|
||||
this.authz.can(req, CaslAction.Update, SubjectName.Class);
|
||||
return this.service.assertClassAccess(req.user.id, classId, canManageAll);
|
||||
this.authz.can(req, CaslAction.Create, SubjectName.Class) ||
|
||||
this.authz.can(req, CaslAction.Update, SubjectName.Class)
|
||||
);
|
||||
}
|
||||
|
||||
private assertReadAccess(req: AuthenticatedRequest, classId: number) {
|
||||
return this.service.assertClassAccess(
|
||||
req.user.id,
|
||||
classId,
|
||||
this.canManageAllClasses(req),
|
||||
);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@@ -67,8 +75,7 @@ export class ClassesController {
|
||||
async findAll(@Query() query: QueryClassDto, @Request() req: AuthenticatedRequest) {
|
||||
const classIds = await this.service.getAccessibleClassIds(
|
||||
req.user.id,
|
||||
this.authz.can(req, CaslAction.Manage, SubjectName.Class) ||
|
||||
this.authz.can(req, CaslAction.Update, SubjectName.Class),
|
||||
this.canManageAllClasses(req),
|
||||
);
|
||||
return this.service.findAll(query, classIds);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user