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

+44 -1
View File
@@ -50,7 +50,12 @@ try {
const script = `const {ensureSchema}=await import('./packages/server/src/lib/db-bootstrap.ts');
const result=await ensureSchema(); if(result.applied!==${expected})throw new Error(JSON.stringify(result));
const {ensureDefaultGroups}=await import('./packages/server/src/lib/permissions.ts');
await ensureDefaultGroups();process.exit(0);`;
await ensureDefaultGroups();
const {loadAuthPrincipal}=await import('./packages/server/src/lib/authz/index.ts');
const {PERMISSION_KEYS}=await import('./packages/shared/dist/index.js');
const admin=await loadAuthPrincipal({sub:'legacy-admin-user',role:'ADMIN',name:'测试',username:'测试',classId:null});
if(admin.permissions.size!==PERMISSION_KEYS.length||admin.fromGroups)throw new Error('管理员不应受默认组限制');
process.exit(0);`;
const result = spawnSync(
process.execPath,
['--import', './packages/server/node_modules/tsx/dist/loader.mjs', '--input-type=module', '-e', script],
@@ -125,6 +130,18 @@ try {
sql: "INSERT INTO \"StudentClassTransfer\" (id,studentId,studentNo,studentName,mode) VALUES (?,'legacy-student','LEGACY','旧学生',?)",
args: [id, mode],
});
await legacy.execute(
`INSERT INTO "User" (id,username,passwordHash,name,role,updatedAt) VALUES ('legacy-teacher','legacy-teacher','unused','旧教师','TEACHER',CURRENT_TIMESTAMP),('legacy-admin-user','legacy-admin-user','unused','旧管理员','ADMIN',CURRENT_TIMESTAMP)`,
);
await legacy.execute(
`INSERT INTO "PermissionGroup" (id,name,scope,scopeValue,updatedAt) VALUES ('old-self','旧任教模板','SELF_CLASS','',CURRENT_TIMESTAMP),('old-head','旧班主任模板','SELF_CLASS','HEAD',CURRENT_TIMESTAMP)`,
);
await legacy.execute(
`INSERT INTO "PermissionGroup" (id,name,defaultRole,scope,scopeValue,updatedAt) VALUES ('old-teacher-default','旧教师默认模板','TEACHER','ALL','',CURRENT_TIMESTAMP)`,
);
await legacy.execute(
`INSERT INTO "UserPermissionGroup" (id,userId,groupId) VALUES ('old-self-link','legacy-teacher','old-self'),('old-head-link','legacy-teacher','old-head'),('old-admin-link','legacy-admin-user','legacy-admin')`,
);
legacy.close();
run(names.length - split, legacyUrl);
const upgraded = createClient({ url: legacyUrl });
@@ -133,6 +150,32 @@ try {
await upgraded.execute(`SELECT key FROM "PermissionGroupItem" WHERE groupId='legacy-admin'`)
).rows.map((row) => row.key);
assert(keys.includes('roster:read') && keys.includes('promotion:manage'));
const assignments = (
await upgraded.execute(
`SELECT scope,scopeValue FROM "UserPermissionGroup" WHERE userId='legacy-teacher' ORDER BY scope`,
)
).rows;
assert.deepEqual(
assignments.map((a) => [a.scope, a.scopeValue]),
[
['HEAD', ''],
['TEACHING', ''],
],
);
assert.equal(
(await upgraded.execute(`SELECT scope FROM "PermissionGroup" WHERE defaultRole='TEACHER'`)).rows[0]
.scope,
'TEACHING',
);
assert.equal(
(await upgraded.execute(`SELECT scope FROM "PermissionGroup" WHERE id='old-head'`)).rows[0].scope,
'HEAD',
);
assert.equal(
(await upgraded.execute(`SELECT count(*) AS n FROM "PermissionGroup" WHERE defaultRole='ADMIN'`))
.rows[0].n,
1,
);
const kinds = (await upgraded.execute('SELECT id,kind,batchId FROM "StudentClassTransfer" ORDER BY id'))
.rows;
assert.deepEqual(