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

+4 -1
View File
@@ -17,6 +17,8 @@ description: 名单、班级会话、任课关系与年度升级。
## 学生名单
Web 统一从“学生管理”查看和维护名单;旧 `/roster` 地址转到同一页面,不再提供重复菜单。班级详情中的名单与课程按当前班级的生效范围展示维护入口,任课关系本身不授予跨班名单、课程或班级设置管理权。
Student 存学号、姓名、班级、性别、家长电话和状态,没有密码或角色字段。
学号为必填唯一标识,格式为 1–32 位字母、数字或 . _ -;不会自动生成登录名。
具有名单管理键的管理员、本班班主任或全校名单范围的授权员工管理学生;只读账号按名单读取键和范围查看历史。
@@ -35,8 +37,9 @@ Student 存学号、姓名、班级、性别、家长电话和状态,没有密
## 权限、学年与归档
角色给默认权限模板,实际入口由可编辑权限组决定;岗位组还能配置年级或部范围。
管理员固定拥有全部能力;教师按岗位模板及每项分配的范围获得能力,未显式分配时使用教师默认模板与固定任教范围。
范围不代替科目写权限,详见[角色与权限](../dev/permissions.md)。
“学期周次”只维护教学周与日期区间;年度轨道、执行计划与补跑统一在管理员专用的“学年升级”页维护。
学期周数受全校可配置上限约束,默认上限 30、允许 1–60,见[课表](schedules.md)。
学年升级按计划时间运行,执行前保存快照,以年份幂等;启动发现漏跑需管理员预览补跑。
默认高中轨道按高一、高二、高三排列,毕业学段归档,其余升一级;也可配置其他轨道。新班不复制任课关系。归档页查看届别历史与转出记录,
+29 -17
View File
@@ -12,18 +12,18 @@ API 前缀 /api,JWT 使用 Authorization: Bearer。成功/失败结构以 `lib
## 身份与班级
| 方法与路径(省略 /api) | 行为及边界 |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| POST /auth/login、/auth/class-login | 员工/班级登录,共享登录限流;学生无账号 |
| GET /auth/me;POST /auth/logout;PATCH /auth/password | 当前身份、退出、员工本人改密码 |
| GET/POST /classes;GET/PATCH/DELETE /classes/:id | 班级查询/管理,读取 class:read;写入 class:manage 与目标范围,创建/删除需全校范围 |
| PATCH /classes/:id/class-account、/classes/:id/head-teacher | 班级凭据/班主任维护 |
| PUT /classes/:id/subject-teachers | 任课关系设置,Course 为唯一来源 |
| GET/PATCH /classes/:id/student-grade-query | 查看/修改班级个人成绩查询开关,班级端不能修改 |
| GET/POST /classes/:id/students;DELETE /classes/:id/students/:studentId | 本班名单及维护,受名单功能权限和班级关系约束 |
| /students、/teachers | 名单与账号 CRUD;读/写使用不同权限键,具体角色限制读取对应模块 |
| POST /teachers/:id/reset-password | 指定新密码或默认重置;学生无对应接口 |
| /permissions/me、/catalog、/groups、/users | 当前有效权限、目录、组与账号分组 |
| 方法与路径(省略 /api) | 行为及边界 |
| ----------------------------------------------------------------------- | -------------------------------------------------------------- |
| POST /auth/login、/auth/class-login | 员工/班级登录,共享登录限流;学生无账号 |
| GET /auth/me;POST /auth/logout;PATCH /auth/password | 当前身份、退出、员工本人改密码 |
| GET/POST /classes;GET/PATCH/DELETE /classes/:id | 班级查询/管理,读取 class:read;写入 class:manage 且仅管理员 |
| PATCH /classes/:id/class-account、/classes/:id/head-teacher | 班级凭据 class:account + 班主任;班主任分配仅管理员 |
| PUT /classes/:id/subject-teachers | 任课关系设置,Course 为唯一来源 |
| GET/PATCH /classes/:id/student-grade-query | 查看/修改班级个人成绩查询开关,班级端不能修改 |
| GET/POST /classes/:id/students;DELETE /classes/:id/students/:studentId | 本班名单及维护,受名单功能权限和班级关系约束 |
| /students、/teachers | 名单与账号 CRUD;读/写使用不同权限键,具体角色限制读取对应模块 |
| POST /teachers/:id/reset-password | 指定新密码或默认重置;学生无对应接口 |
| /permissions/me、/catalog、/groups、/users | 当前有效权限、目录、组与账号分组 |
## 教学与文件
@@ -58,11 +58,11 @@ API 前缀 /api,JWT 使用 Authorization: Bearer。成功/失败结构以 `lib
| POST /clients/update-progress | 设备上传下载、校验、等待、安装和失败状态,校验设备班级归属 |
| GET /clients/devices;DELETE /clients/devices/:id | 设备读取/维护,分别检查功能键和范围 |
| POST /clients/commands | remote:control + 对应范围,白名单动作 |
| POST /clients/updates | client:update,安装策略 immediate/after-school/idle |
| POST /clients/updates | client:update-push,安装策略 immediate/after-school/idle |
| GET /update/check | 员工需 update:check;强制刷新另检查 update:force |
| GET /update/client/releases | 缓存版本读取,按路由允许的读取权限 |
| POST /update/client/fetch、/update/client/releases;DELETE /update/client/releases/:version | 安装包写入口统一 client:update |
| GET /update/client/download/:version | 已登录下载,不挂员工权限键,供班级端更新 |
| POST /update/client/fetch、/update/client/releases;DELETE /update/client/releases/:version | 安装包写入口仅管理员 client:update |
| GET /update/client/download/:version | 本班 CLASS_DEVICE 或 client:read + 有效班级范围 |
| /term;POST /term/auto;GET /term/holidays | 学期读取/编辑/假日建议,term:read/term:write |
| GET/PUT /term/promotion;POST /term/promotion/run | 升级计划与执行,promotion:manage + 全校范围,执行要求确认与计划哈希 |
| /archives、/archives/transferred、/archives/transferred/:id/restore | 历史归档需 archive:manage 全校范围;撤回转出需 roster:manage;不提供手动创建归档 |
@@ -83,8 +83,8 @@ API 前缀 /api,JWT 使用 Authorization: Bearer。成功/失败结构以 `lib
| GET /permissions/scopes | permission:manage;返回可选年级、部建议和计划年级 |
| GET /permissions/groups/:id/members | permission:manage;包含显式分组及角色默认回落的生效成员 |
| GET /permissions/audit?page=&pageSize= | permission:manage;分页操作人、目标、前后 JSON,pageSize 最大 100 |
| POST/PATCH/DELETE /permissions/groups[/:id] | permission:manage;范围校验与防提权;默认组仅 ADMIN 可改,内置组不可删 |
| PUT /permissions/users/:id/groups | groupIds 整体替换;默认组角色须匹配,空数组恢复角色默认模板;禁止越键越范围 |
| POST/PATCH/DELETE /permissions/groups[/:id] | 仅 ADMIN;仅可分配键,范围校验,教师默认模板固定 TEACHING,内置组不可删 |
| PUT /permissions/users/:id/groups | assignments 整体替换,每项 groupId/scope/scopeValue;兼容 groupIds 按建议范围分配;空数组恢复教师默认模板;ADMIN 不可分配 |
| GET /students?status=&includeArchived= | roster:read;默认 active,归档单独筛选;历史状态不会再被在读条件覆盖 |
| POST /students | roster:manage;仅 active/inactive,学号重复 409,班级不得归档 |
| PATCH /students/:id | roster:manage;仅资料字段;classId/status 明确返回 400 |
@@ -105,3 +105,15 @@ API 前缀 /api,JWT 使用 Authorization: Bearer。成功/失败结构以 `lib
生命周期历史以 kind 为事实源,mode 为派生兼容字段;新响应另含 batchId、academicYear、fromGrade/toGrade、fromSchool/toSchool。
归档详情含 inactive 列表、inactiveCount 和按学生去重的 transferInCount;升级记录含 operatorId/operatorName、detailsJson、snapshotSha256、error。
所有学生与历史写入在同一事务,提交后再广播旧班与新班。班级详情只有持 roster:read 且在对应范围内的员工才能获得学生明细。
## 分配级范围与班级设置
`GET /permissions/catalog` 增加 meta(kind/risk/relation/allowedScopes)。模板 DTO 的 defaultScope/defaultScopeValue/defaultScopeLabel 及兼容 scope 字段都是建议范围;kind、inactiveKeys、warnings 展示模板类别和停用键。成员 DTO 的 assignments 是实际分配,groupIds 为派生值;ADMIN fixed=true。
`GET /permissions/users/:id/effective` 仅 ADMIN,返回 permissions、entries(每键 scopes/classes/classCount)、conflicts、headOnlyKeys。可选 query assignments 为 JSON 数组,预览尚未保存的整体分配;预览与实际授权共用求值器。
`PATCH /classes/:id/settings` 使用 class:settings + 本班班主任(ADMIN 例外)保存 termWeeks/termStartDate。管理员 `PATCH /classes/:id` 只接收 enrollmentYear/classIndex。`PATCH /classes/:id/class-account` 使用 class:account + 本班班主任。全局周次仅管理员,带 classId 的周次写走 class:settings + 班主任。时间表写入按存储 classId 分流,班级时间表需要 schedule:write + 班主任,全校时间表仅管理员。
成绩 overview/collective/exams/ranking 显式检查 grade:read,班级端由服务断言限本班并执行查询开关。设备 heartbeat/update-progress/commands/:id/ack 仅接受绑定本班的 CLASS_DEVICE,不接受教师个人设备。
`GET /permissions/me` 同时返回 classScopes、manageScopes、allScopeKeys,供 Web 按钮与班级选择器显示实际能力范围;manageScopes 对名单、日课表、导入和考试配置叠加班主任或显式组织范围规则。界面范围提示不替代服务端断言。
+10 -2
View File
@@ -27,9 +27,9 @@ description: 现行模型职责、迁移与快照边界。
| `TimeLayout` | 共享或班级自有时间表;旧 items JSON 保留迁移审计 |
| `Grade` | studentId、课程、考试、分数和等级 |
| `PermissionAudit` | 权限与人工升级审计,操作人和前后 JSON,不建账号外键 |
| `PermissionGroup` | 默认/自定义/岗位组及 scope/scopeValue |
| `PermissionGroup` | 岗位模板与建议 scope/scopeValue |
| `PermissionGroupItem` | 组内权限键 |
| `UserPermissionGroup` | 账号与组的多对多分配 |
| `UserPermissionGroup` | 账号与模板的分配及实际 scope/scopeValue |
| `AppSetting` | 周次、升级、AI 和系统设置 |
| `ClientDevice` | deviceKey、心跳、更新目标与状态 |
| `ClientCommand` | 白名单远控、有效期与执行回执 |
@@ -85,3 +85,11 @@ User.careMode 是预留列;当前 Web 偏好在 localStorage,客户端偏好
SNAPSHOT_TABLES 已加入 PermissionAudit,新增字段随模型导出。旧快照没有审计表可恢复为空,旧历史 mode 在恢复时转为 kind;
没有新增列的旧行使用 schema 默认值。执行记录与升级快照不自动清理。
升级计划存 AppSetting.term.promotionPlan,时间存 term.promotionAt,无需新增学校或学生账号模型。
## 授权分配迁移
`20261031000000_authz_assignments` 为 UserPermissionGroup 增加 scope(默认 TEACHING)与 scopeValue。先按旧模板范围回填分配,再将模板 SELF_CLASS/空值和 SELF_CLASS/HEAD 规范为 TEACHING/HEAD。教师默认模板建议范围固定 TEACHING;旧 ADMIN 默认模板与旧管理员专用键保留在库中,由运行时忽略。
同一成员可将同一模板分配到多个范围,唯一约束为 userId/groupId/scope/scopeValue。PermissionGroup 的范围列仅作建议,不参与已有分配求值。
快照表集合和版本 1 保持兼容。恢复旧模板时映射 SELF_CLASS;旧 UserPermissionGroup 没有范围列时按快照内对应模板推导,缺组回落 TEACHING。新快照按实际分配列恢复。真实库升级前先备份快照,隔离回归不替代真实库升级演练。
+4 -2
View File
@@ -18,7 +18,7 @@ description: 课表、周次、学年、岛、设备与课件的现行实现入
## 权限、成绩与年度升级
默认权限组、岗位范围、科目写权限分别由 permissions/permission-scope/access 维护,范围并集不授予任课权。
能力、分配范围、业务关系统一由 lib/authz 的 principal/scope/relations/assert 维护;access 只提供带显式键的窄封装。ADMIN 固定全集,TEACHER 过滤管理员专用键。
全校周数上限在 `term.maxWeeks`,默认 30、允许 1–60;开学日和逐周区间支持全校与班级设置。
个人成绩查询关闭后只返回服务端集体聚合,个人明细、原始全班成绩与排名拒绝读取;样本不足或稀疏分段抑制。
年度升级由 `term.promotionPlan`、`term.promotionAt` 和 `PromotionRun.year` 管理,先存快照;运行期到点执行,启动漏跑需管理员预览补跑。
@@ -44,9 +44,11 @@ JSON 快照含课件表,完整恢复还必须带文件实体目录。细节见
学生状态/班级写入口集中在 `lib/student-lifecycle.ts`,班级名单添加/移出、名单导入、转出和转入均复用它。
资料 PATCH 不承担状态或班级迁移。学生与历史在事务内提交,UUID 标识批次,事务后广播两端班级,历史读取不附带在读过滤。
权限、功能对应范围、业务关系依次断言;权限只在当前请求对象上复用,下一请求重新计算。
范围解析在 shared,默认组或分组均按当前功能键过滤,不能借另一功能的范围扩大当前操作。
范围解析在 shared,实际范围存分配记录,模板范围仅作新分配建议。各键独立合并,无键为空集;默认教师范围固定 TEACHING。
权限组或成员分配与 PermissionAudit 同事务,成员权限一次预取计算。
年度升级服务由计划推导所有轨道的下一学段。预览哈希覆盖配置、班级、学生和周次,执行事务内重新计算;
先保存全库快照并计算 SHA256,再以年份唯一键提交升级。毕业班所有状态挂届别,非毕业班仅升级及清班级开学日覆盖。
周次是否清空由计划决定,预览公开影响。服务保留 runPromotion(year, dryRun) 内部兼容签名;HTTP 执行必须提供哈希、确认并检查年份。
实时班级事件按 core/homework/notification/grade/courseware 分房间,事件总线按 shared 映射选择通道,教师仅订阅对应读键范围。HTTP 与 Socket 握手共用数据库身份加载;敏感权限与账号变更使旧订阅失效。事务路径传递同一数据库连接给授权断言,保留原/目标资源的校验。
+52 -65
View File
@@ -1,89 +1,76 @@
---
title: 角色与权限
description: 功能权限、岗位范围与班级科目边界。
description: 能力目录、岗位模板、分配范围与业务关系的授权契约。
---
# 角色与权限
授权分为功能权限、数据范围和业务关系三层,任何一层拒绝都不能操作。
shared 的 `permissions.ts` 定义键、标签、默认模板与班级关系规则;服务端 `lib/permissions.ts`、
`lib/permission-scope.ts`、`lib/access.ts` 分别计算生效键、查询范围和具体业务断言。
授权依次检查能力、该能力的数据范围、数据行上的业务关系。服务端统一入口为 `lib/authz/`,路由预检不能替代服务断言。共享目录 `PERMISSION_META` 提供分类、风险、关系提示、合法范围、标签和默认角色;完整键目录读取源码,不在文档维护固定数量。
## 身份与权限组
## 身份与模板
账号角色为 ADMIN、TEACHER;班级登录形成 CLASS_DEVICE 会话。学生是 Student 名单,不是账号。
HEAD/SUBJECT 是班级关系,级长/部长是岗位组,均不新增 User.role。
| 主体 | 生效能力 |
| -------------------- | -------------------------------------------------- |
| ADMIN | 固定拥有全部目录键及全校范围,忽略权限组 |
| TEACHER | 显式分配模板的可分配能力并集;管理员专用键永久无效 |
| 未显式分配的 TEACHER | 数据库教师默认模板与固定 TEACHING 范围 |
| CLASS_DEVICE | 空集;接口明确允许班级端并校验本班关系 |
| 情况 | 有效功能权限 |
| ------------ | ----------------------------------------------------- |
| 已分配权限组 | 所有分组键的并集,可以比角色默认更少 |
| 没有分组 | 数据库对应角色的可编辑默认组 |
| ADMIN | 同上,始终额外保留 permission:manage 与 system:manage |
| CLASS_DEVICE | 空集;通过明确允许班级会话的接口与业务断言操作 |
学生是 Student 名单记录,没有账号。HEAD/SUBJECT 是班主任和科任关系,级长/部长是岗位模板,不增加 User.role。权限管理只允许 ADMIN;旧 ADMIN 默认模板保留存储但隐藏、忽略。
每请求读取生效权限,编辑后无需等待令牌过期。内置组可改不可删,默认组不是写死的全权限快捷方式。
路由使用 requirePermission(AND)、requireAnyPermission(OR),员工与班级端共享读取入口按 requireStaffPermission 等规则检查。
具体 API 的额外角色限制以模块路由和服务断言为准,不能把“默认只分配给管理员”写成所有接口硬编码 ADMIN。
`PermissionGroup` 保存能力和新分配的建议范围,`UserPermissionGroup` 保存实际 `scope/scopeValue`。模板改建议范围不会改变已有分配;模板改能力会影响其成员。同一模板可分配不同范围,多条分配按键取并集。空分配恢复默认模板,界面不会将未分组成员自动转换成显式默认组。
## 数据范围与科目
## 能力目录与范围
| 岗位/身份 | 范围 |
| ----------- | --------------------------------------------- |
| 管理员 | 全部班级;仍须功能键 |
| 班主任/教师 | 自己担任班主任或任课的班级,按分组 scope 收敛 |
| 级长 | 配置年级 |
| 部长 | 配置的一组年级,scopeValue 用逗号分隔 |
| 班级端 | 登录绑定班级 |
能力分为管理员专用、班级级、组织只读。管理员专用能力包括组织与账号维护、权限/系统/数据库、全局周次、全校时间表、升级归档、安装包管理等;具体清单以 `PERMISSION_META.kind` 为准。`teacher:read` 和 `update:check` 为组织只读,忽略班级范围。其余班级级能力按分配范围求值。
空年级/部配置不放行。范围按当前功能键独立合并,不能借其他只读组扩大名单、班级或设备写权限。
科目编辑再由 `assertCanManageSubjectContent` 检查:管理员可管理,教师仅自己任课科目,无科目内容归班主任。
班主任不任教某科就不能改该科的作业或成绩;班级端只允许作业写入,不能写成绩。
| 范围 | 语义 |
| -------- | -------------------------- |
| TEACHING | 本人担任班主任或任课的班级 |
| HEAD | 仅本人担任班主任的班级 |
| GRADE | 指定一个年级 |
| DIVISION | 指定多个年级 |
| ALL | 全校 |
| 业务 | 主要功能键与额外边界 |
| ---------------- | ----------------------------------------------------------------------------------------------------- |
| 名单读取/管理 | roster:read / roster:manage;管理需名单键与目标班范围;关联班级仍须班主任关系,全校名单范围可跨班维护 |
| 教师列表/维护 | teacher:read / teacher:manage;导入教师另检查 import:teachers 与 teacher:manage |
| 日课表写入 | schedule:write + 管理员/本班班主任 |
| 全校时间表写入 | timetable:manage;默认给 ADMIN |
| 作业/成绩 | homework:write / grade:write 或 grade:import,再按班级科目收口 |
| 通知/叫人 | notification:publish / call:create,再检查班级范围 |
| 课件 | courseware:read / courseware:write,上传和推送自己的文件还受本人授课班级约束 |
| 客户端维护/远控 | client:read、client:manage、remote:control,写操作按对应键范围 |
| 版本与安装包写入 | client:update;旧 client:update-push 不再作为写入口要求 |
| 学年升级 | promotion:manage;计划、预览与执行独立授权,全校范围,默认给 ADMIN |
| 数据库/系统/权限 | database:manage / system:manage / permission:manage |
`parsePermissionScope` 统一校验范围。GRADE/DIVISION 必须选择已有年级或升级计划中的年级,范围值按逗号分隔、去重排序;其余范围值必须为空串。旧 SELF_CLASS 仅在迁移和旧快照恢复中映射为 TEACHING/HEAD,不是运行时枚举。
完整键目录读取 `PERMISSION_KEYS`,不在文档维护固定键数。
`loadAuthPrincipal` 在请求主体上缓存只读能力和分配快照,没有可变的“当前权限键”。`classIdsFor(user, key)` 和 `classWhere(user, key)` 强制传键,无键或无授权返回空集合;不会合并其他能力的范围。`classWhere` 过滤 Class 表主键,下游子表使用明确的 classId 条件。
## 前端门控与成绩隐私
## 关系与写入
Web 菜单属性来自路由,`meta.roles` 粗筛、`meta.permission` 细筛;先 ensurePermissions 再按生效集合判断。
服务端仍检查功能、范围与业务关系,直接构造请求不能绕过。
岗位范围可以扩大读范围,不能自动扩大科目写权限,也不能替代课件的本人任课关系。
班级成绩查询开关仅员工按业务权限修改;关闭后班级端仍看集体聚合,个人明细、原始全班成绩和排名返回拒绝。
稀疏分段与补集抑制,服务端不向客户端下发个人原始成绩再聚合。
| 操作 | 功能与关系 |
| --------------------------------------------- | -------------------------------------------------------------------------------- |
| 建班/删班/届别班号、班主任/任课分配、教师账号 | 管理员专用,记录审计;最后一个管理员不可降级或删除 |
| 班级账号 | class:account + 本班班主任,管理员例外;班级码只给班主任或管理员 |
| 班级学期设置/班级周次 | class:settings + 本班班主任,管理员例外 |
| 名单、日课表与课程写入 | 对应写键;TEACHING/HEAD 要求班主任,显式年级/部/全校范围允许范围内维护 |
| 学生/课表导入 | 导入键与相应写键同时满足;先校验全部班级再写,学生及任课导入使用事务 |
| 班级时间表写入 | schedule:write + 本班班主任;按存储 classId 校验,所有引用班级也要校验 |
| 全校时间表、全局周次 | 管理员;班级设置使用独立接口 |
| 作业/成绩写入 | assertContentWrite;有科目要求该科任教,无科目要求班主任;原科目与目标科目都检查 |
| 作业提交/未交名单 | homework:write + 同样的科目关系;班级端明确允许本班作业 |
| 通知/叫人 | 发布能力和范围,再检查班主任/任课关系;班级端限本班 |
| 通知删除 | notification:delete + 发布者/班主任/管理员 |
| 课件 | 本人文件、对应功能范围与本人授课班级共同约束,不赋予班主任同事文件访问权 |
## 设备维护
ALL 也不能替代班主任、任课、发布者关系,只有 ADMIN 无条件绕过关系。班级端可代录作业,不能写成绩。跨班调班检查源班与目标班。事务中的断言使用同一事务连接,不能因事务路径省略授权。
`/api/clients/heartbeat` 和命令回执供已登录设备使用,不依赖班级端的空权限集合;服务端校验 deviceKey 与班级归属。
远控从管理端确认下发,客户端执行后回传真实结果。更新文件由服务端缓存,教室机验 SHA256,
按立即、放学后、空闲策略安装;关闭自动更新不会强制安装。详见[更新与升级](../management/upgrade.md)。
成绩列表、考试、排名和集体分析都要求 grade:read 与对应班级范围。学生明细另要求 student:grade-query。关闭班级自助查询后,本班设备仍可获得集体聚合,个人明细、原始成绩、考试列表和排名拒绝。实时载荷同样受隐私开关保护,服务端执行稀疏样本和补集抑制。
## 范围配置与权限治理
## 实时与设备
范围由 shared 的 `parsePermissionScope` 统一解析。关联班级只接受空值(班主任与任课班)或 HEAD(仅班主任班);
GRADE 必选一个年级,DIVISION 必选多个年级组成的集合;全角逗号、顿号和空白统一分隔、去重排序。
ALL 存空范围值。年级选项来自数据库已有年级和升级计划,空范围或未知年级返回 400;旧库未配置的岗位组不放行。
`GET /permissions/scopes` 提供选择器选项,组 DTO 的 scopeLabel 供页面展示。
班级房间为 `class:{id}:{channel}`,事件映射见 `SOCKET_EVENT_CHANNELS`。班级端加入本班全部通道;教师按各域读键及范围加入,core 由 class:read/schedule:read 决定。显式订阅再次校验通道,握手与 HTTP 共用数据库身份加载;权限/账号变更断开旧订阅。
默认组只能分配给同角色;非 ADMIN 的权限管理员只能管理关联班级范围的自定义组,
组原有键和新键均不得超过自己当前权限。角色默认组只有 ADMIN 可改;删除组可能触发默认回落,仅 ADMIN 可执行。
分配空数组恢复数据库角色默认组,分配任何组则仅取所选组并集;需要保留默认时显式同时选择同角色默认组。
心跳、进度和命令回执只接受 CLASS_DEVICE,deviceKey 必须属于会话班级。远控只排队允许的系统指令,自动化始终假执行。安装包登记/删除使用管理员 client:update;班级推送使用 client:update-push。下载要求本班班级端或有范围的 client:read。版本与文件名在请求校验和路径构造两处拒绝不安全路径段。
权限页保存前展示成员与权限差异,支持复制组、查看生效成员和分页审计。
PermissionAudit 在组或分配事务中保存操作人、目标、前后 JSON;`GET /permissions/audit` 受 permission:manage 控制。
成员权限由一次预取组和账号后计算,不逐账号重复加载。
## 权限页与审计
新增迁移为旧库已有 ADMIN 默认组补 promotion:manage;新库由默认模板创建。
自定义组和已分配其他组的管理员不会自动获得该键,需要在权限页显式授予。
班级端权限仍为空,本班名单、课表、通知、叫人和作业代录走显式员工守卫例外;班级端发布内容归属本班班主任。
权限页默认展示成员与范围,每条分配独立选择范围,保存前预览新增/移除能力、每键合并范围、班级列表、混合范围提示、班主任专属提示和风险标记。模板页仅提供可分配能力,旧管理员键显示为停用。组织只读能力标注“不限班级”。路由用 `meta.permission` 检查读取能力;维护页面额外用 `meta.menuActions` 声明可执行动作,动作须在至少一个可读班级生效才显示侧栏、搜索及页签,有部分功能可操作就保留入口。`permissions/me.actionScopes` 结合范围和班主任、任课、发布者关系提供动作班级;班级管理动作另用 `manageScopes`。纯查看页面按读取能力展示,服务端始终做最终判定。
学期周次维护入口要求全校周次写能力或至少一个可维护的本班设置范围。非全校维护者进入后默认选择可编辑班级,仍可切换查看授权范围内的其他班级。
目录、模板、成员、分配预览和审计的公开契约见[API](api.md)。模板/分配和账号敏感操作写入 PermissionAudit;审计不保存密码明文或哈希。迁移与快照兼容见[数据模型](data-model.md)。
## 验证
`pnpm check:authz` 扫描旧授权符号和缺少显式键的查询,并接入 lint。`pnpm verify:authz` 使用临时 SQLite 验证接管班级、管理员提权、跨键借范围、科目改挂、跨班时间表/周次/导入、成绩隐私、通道订阅、设备冒用和旧快照等攻击场景。其他专项回归保留生命周期、课表、课件、设备与升级覆盖。验证不触及真实班级、不访问外网、不执行系统指令。
+14 -11
View File
@@ -5,22 +5,21 @@ description: 实际事件与房间、断线恢复和客户端提醒分工。
# 实时事件(Socket.IO)
连接使用 `io(url, { auth: { token } })`,服务端在握手校验 JWT。
连接使用 `io(url, { auth: { token } })`,服务端握手校验 JWT 并与 HTTP 共用数据库身份加载。
事件与房间定义在 shared 的 `constants.ts`,载荷类型在 shared 的 `types.ts`,以这些契约和发送点为准。
## 房间与定向推送
| 房间 | 作用 |
| --------------- | -------------------------------------------------- |
| class:{classId} | 某班级的业务广播 |
| session:{id} | 会话定向名;员工 id 为账号 id,班级端 id 为班级 id |
| role:{role} | 角色房间名,定义在共享常量 |
| teacher:{id} | 教师房间名,定义在共享常量 |
| students | 班级端连接的广播房间,代表全班,不是个人学生账号 |
| teachers | 教师和管理员连接的广播房间 |
| 房间 | 作用 |
| ------------------------- | -------------------------------------------------- |
| class:{classId}:{channel} | 某班级对应读域的业务广播 |
| session:{id} | 会话定向名;员工 id 为账号 id,班级端 id 为班级 id |
| role:{role} | 角色房间名,定义在共享常量 |
| teacher:{id} | 教师房间名,定义在共享常量 |
| students | 班级端连接的广播房间,代表全班,不是个人学生账号 |
| teachers | 教师和管理员连接的广播房间 |
`realtime/socket.ts` 当前显式 join 可访问的 class 房间与 students/teachers 广播房间;
同时在 connected 载荷列出 session/role 名。不能把共享常量或该列表当成所有房间均已成功 join 的证明,实际订阅以 join 调用为准。
`realtime/socket.ts` 按域加入授权班级房间,并加入 session/role 与 students/teachers 房间;connected 载荷列出实际已加入房间。
业务经 `realtime/bus.ts` 的 emitToClass/emitToSession/emitToRooms 推送。
叫人记录属于班级通知,并发送班级广播与班级会话定向事件;学生本身没有连接和个人房间。
@@ -47,3 +46,7 @@ description: 实际事件与房间、断线恢复和客户端提醒分工。
同步修改共享事件名与载荷、服务端发送点、Web `stores/realtime.ts`、客户端 `stores/realtime.ts` 及相关缓存/岛逻辑。
不要手写事件名;只改服务端会导致前端静默不刷新。涉及成绩时另检查隐私载荷和班级范围。
## 读域订阅
现行班级房间为 `class:{id}:{channel}`。channel 为 core/homework/notification/grade/courseware,事件映射来自 shared 的 SOCKET_EVENT_CHANNELS。旧 `class:{id}` 仅保留兼容名称,生产不订阅。班级端加入本班全部域;教师按对应读键和分配范围加入。`class:join` 接受旧 classId 字符串或 `{classId, channels}`,后者逐通道校验,有任一无权则拒绝整次请求。握手回查账号与角色,权限变更断开旧订阅。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 101 KiB