feat(authz): 重写权限模板分配与班级授权边界

拆分能力、分配范围和业务关系,固定管理员能力并收紧组织级写入口。
补齐内容、课表、设备会话和实时通道授权,兼容旧库分配及历史快照。
重写权限分配预览,按实际可操作范围展示维护入口,合并学生入口并独立学年升级页。
增加授权攻击矩阵及静态门禁,更新隔离回归与开发契约。

验证:授权矩阵 23 段通过,Node 24 下 Web 63/63,类型检查、lint 与文档严格构建通过。
This commit is contained in:
LoveChengke committed 2026-10-31 09:40:23 +08:00
1 parent 682818dd4a
commit 66be3a63b1
115 files changed
+4463 -2747

No files matched your search

+16 -11
View File
@@ -255,7 +255,9 @@ try {
emitted
.filter((e) => e.event === SOCKET_EVENTS.coursewareChanged)
.every((e) =>
e.rooms.every((room) => [classes[0]!.id, classes[1]!.id].some((id) => room === `class:${id}`)),
e.rooms.every((room) =>
[classes[0]!.id, classes[1]!.id].some((id) => room === `class:${id}:courseware`),
),
),
);
await new Promise<void>((resolve) => server.close(() => resolve()));
@@ -357,24 +359,27 @@ try {
state = await prisma.clientDevice.findUniqueOrThrow({ where: { id: device.id } });
check('取消目标后忽略旧任务进度', state.updateProgress === '');
const teacherDeviceKey = 'courseware-teacher-device';
await request('/clients/heartbeat', tokens[0], { deviceKey: teacherDeviceKey, clientVersion: '1.0.0' });
let teacherDevice = await prisma.clientDevice.findUniqueOrThrow({ where: { deviceKey: teacherDeviceKey } });
check('教师电脑设备不绑定班级', teacherDevice.classId === null);
await prisma.clientDevice.update({
where: { id: teacherDevice.id },
data: { updateTargetVersion: '2.0.0' },
check(
'教师会话不能注册设备心跳',
(await request('/clients/heartbeat', tokens[0], { deviceKey: teacherDeviceKey, clientVersion: '1.0.0' }))
.status === 403,
);
const teacherDevice = await prisma.clientDevice.create({
data: { deviceKey: teacherDeviceKey, updateTargetVersion: '2.0.0' },
});
check(
'教师电脑可上报自己的更新进度',
'旧未绑定设备也不能由教师冒报进度',
(
await request('/clients/update-progress', tokens[0], {
deviceKey: teacherDeviceKey,
progress: { ...updateProgress, version: '2.0.0' },
})
).status === 200,
).status === 403,
);
check(
'拒绝请求不改变旧设备进度',
(await prisma.clientDevice.findUniqueOrThrow({ where: { id: teacherDevice.id } })).updateProgress === '',
);
teacherDevice = await prisma.clientDevice.findUniqueOrThrow({ where: { deviceKey: teacherDeviceKey } });
check('教师电脑更新进度已记录', JSON.parse(teacherDevice.updateProgress).stage === 'downloading');
const snapshot = await exportSnapshot(prisma, 'sqlite');
check(
'备份包含课件、可见范围、推送和回执',