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 readonly authz: AuthorizationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private assertReadAccess(req: AuthenticatedRequest, classId: number) {
|
private canManageAllClasses(req: AuthenticatedRequest): boolean {
|
||||||
// Legacy: Manage (super_admin) or Update (class:edit) grants broad class access
|
return (
|
||||||
const canManageAll =
|
|
||||||
this.authz.can(req, CaslAction.Manage, SubjectName.Class) ||
|
this.authz.can(req, CaslAction.Manage, SubjectName.Class) ||
|
||||||
this.authz.can(req, CaslAction.Update, SubjectName.Class);
|
this.authz.can(req, CaslAction.Create, SubjectName.Class) ||
|
||||||
return this.service.assertClassAccess(req.user.id, classId, canManageAll);
|
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()
|
@Get()
|
||||||
@@ -67,8 +75,7 @@ export class ClassesController {
|
|||||||
async findAll(@Query() query: QueryClassDto, @Request() req: AuthenticatedRequest) {
|
async findAll(@Query() query: QueryClassDto, @Request() req: AuthenticatedRequest) {
|
||||||
const classIds = await this.service.getAccessibleClassIds(
|
const classIds = await this.service.getAccessibleClassIds(
|
||||||
req.user.id,
|
req.user.id,
|
||||||
this.authz.can(req, CaslAction.Manage, SubjectName.Class) ||
|
this.canManageAllClasses(req),
|
||||||
this.authz.can(req, CaslAction.Update, SubjectName.Class),
|
|
||||||
);
|
);
|
||||||
return this.service.findAll(query, classIds);
|
return this.service.findAll(query, classIds);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user