feat(authz): 重写权限模板分配与班级授权边界
拆分能力、分配范围和业务关系,固定管理员能力并收紧组织级写入口。 补齐内容、课表、设备会话和实时通道授权,兼容旧库分配及历史快照。 重写权限分配预览,按实际可操作范围展示维护入口,合并学生入口并独立学年升级页。 增加授权攻击矩阵及静态门禁,更新隔离回归与开发契约。 验证:授权矩阵 23 段通过,Node 24 下 Web 63/63,类型检查、lint 与文档严格构建通过。
This commit is contained in:
1 parent
682818dd4a
commit
66be3a63b1
115 files changed
+4463
-2747
No files matched your search
@@ -0,0 +1,86 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import ts from 'typescript';
|
||||
|
||||
const forbidden = new Set([
|
||||
'assertStaffPermission',
|
||||
'hasAllScope',
|
||||
'teacherScopeWhere',
|
||||
'currentRequestPermission',
|
||||
'bindRequestPermission',
|
||||
'bindRequestPermissions',
|
||||
]);
|
||||
const failures = [];
|
||||
function scan(dir) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.name === 'generated') continue;
|
||||
const file = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
scan(file);
|
||||
continue;
|
||||
}
|
||||
if (!file.endsWith('.ts')) continue;
|
||||
const src = fs.readFileSync(file, 'utf8'),
|
||||
ast = ts.createSourceFile(file, src, ts.ScriptTarget.Latest, true);
|
||||
function fail(node, message) {
|
||||
failures.push(`${file}:${ast.getLineAndCharacterOfPosition(node.getStart(ast)).line + 1} ${message}`);
|
||||
}
|
||||
function visit(node) {
|
||||
if (ts.isIdentifier(node) && forbidden.has(node.text)) fail(node, `禁止旧授权符号 ${node.text}`);
|
||||
if (ts.isCallExpression(node)) {
|
||||
const name = node.expression.getText(ast);
|
||||
const position = ['resolveClassScope', 'assertClassAccess', 'canAccessClass'].includes(name)
|
||||
? 2
|
||||
: ['classIdsFor', 'classWhere', 'getAccessibleClassIds'].includes(name)
|
||||
? 1
|
||||
: -1;
|
||||
if (
|
||||
position >= 0 &&
|
||||
(!node.arguments[position] || node.arguments[position].getText(ast) === 'undefined')
|
||||
)
|
||||
fail(node, `${name} 必须显式传权限键`);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
visit(ast);
|
||||
}
|
||||
}
|
||||
scan('packages/server/src');
|
||||
// 菜单页必须声明能力键,防止移除旧 roles 后出现无门控入口。
|
||||
const routerFile = 'packages/web-admin/src/router/index.ts';
|
||||
const routerAst = ts.createSourceFile(
|
||||
routerFile,
|
||||
fs.readFileSync(routerFile, 'utf8'),
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
);
|
||||
function property(object, name) {
|
||||
return object.properties.find((p) => ts.isPropertyAssignment(p) && p.name.getText(routerAst) === name)
|
||||
?.initializer;
|
||||
}
|
||||
function checkRoutes(node) {
|
||||
if (ts.isObjectLiteralExpression(node)) {
|
||||
const name = property(node, 'name'),
|
||||
component = property(node, 'component'),
|
||||
meta = property(node, 'meta');
|
||||
if (meta && ts.isObjectLiteralExpression(meta) && property(meta, 'roles'))
|
||||
failures.push(`${routerFile} 禁止 meta.roles,统一使用能力键`);
|
||||
if (
|
||||
name &&
|
||||
component &&
|
||||
name.getText(routerAst) !== "'no-access'" &&
|
||||
(!meta ||
|
||||
!ts.isObjectLiteralExpression(meta) ||
|
||||
(!property(meta, 'public') && !property(meta, 'permission')))
|
||||
)
|
||||
failures.push(`${routerFile} 页面 ${name.getText(routerAst)} 缺少 meta.permission`);
|
||||
}
|
||||
ts.forEachChild(node, checkRoutes);
|
||||
}
|
||||
checkRoutes(routerAst);
|
||||
if (fs.existsSync('packages/server/src/lib/permission-scope.ts'))
|
||||
failures.push('旧 permission-scope.ts 必须删除');
|
||||
if (failures.length) {
|
||||
console.error(failures.join('\n'));
|
||||
process.exitCode = 1;
|
||||
} else console.log('授权静态门禁通过:无旧入口、无省略权限键的范围查询');
|
||||
Reference in new issue
Block a user