拆分能力、分配范围和业务关系,固定管理员能力并收紧组织级写入口。 补齐内容、课表、设备会话和实时通道授权,兼容旧库分配及历史快照。 重写权限分配预览,按实际可操作范围展示维护入口,合并学生入口并独立学年升级页。 增加授权攻击矩阵及静态门禁,更新隔离回归与开发契约。 验证:授权矩阵 23 段通过,Node 24 下 Web 63/63,类型检查、lint 与文档严格构建通过。
87 lines
3.0 KiB
JavaScript
87 lines
3.0 KiB
JavaScript
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('授权静态门禁通过:无旧入口、无省略权限键的范围查询');
|