|
- import fs from 'node:fs';
- import path from 'node:path';
- import crypto from 'node:crypto';
- import assert from 'node:assert/strict';
- import { fileURLToPath } from 'node:url';
-
- export const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
- export const work = path.resolve(root, '../.codex-tmp/permissions-20260906');
- export const report = path.resolve(root, 'ute2e/reports/role-permissions-20260906');
- fs.mkdirSync(work, { recursive: true }); fs.mkdirSync(report, { recursive: true });
- export function credentials(account) {
- const lines = fs.readFileSync(path.join(root, '账号.MD'), 'utf8').replace(/^\uFEFF/, '').split(/\r?\n/).map(x => x.trim()).filter(Boolean);
- const pairs = [];
- for (let i = 0; i + 1 < lines.length; i += 2) pairs.push({ username: lines[i].replace(/^.*?[::]\s*/, '').trim(), password: lines[i + 1].replace(/^.*?[::]\s*/, '').trim() });
- const found = pairs.find(x => x.username === account);
- if (!found?.password) throw new Error('Requested local credential pair is unavailable');
- return found;
- }
- export async function call(endpoint, token, body, method = body === undefined ? 'GET' : 'POST') {
- const response = await fetch(`http://127.0.0.1:6180/api/${endpoint}`, {
- method, headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
- body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(30000),
- });
- const value = await response.json();
- return { status: response.status, code: value.code, data: value.data, message: value.message };
- }
- export async function login(account = 'admin') {
- const result = await call('auth/v1/auth/login', null, { ...credentials(account), roleCode: 'admin', rememberMe: false });
- assert.equal(result.status, 200, `Local ${account} login status`);
- assert.ok(result.data?.accessToken, 'Access token returned');
- return result.data;
- }
- const checks = [];
- const check = (name, condition) => { assert.ok(condition, name); checks.push({ name, result: 'PASS' }); };
- const records = value => value?.records ?? value ?? [];
- const fixtureFile = path.join(work, 'fixtures.json');
- const write = fixture => fs.writeFileSync(fixtureFile, JSON.stringify(fixture, null, 2));
-
- async function prepare() {
- const admin = await login(); const maintenance = await login('root');
- const token = admin.accessToken; const rootToken = maintenance.accessToken;
- try {
- const me = (await call('auth/v1/auth/me', token)).data;
- const rootMe = (await call('auth/v1/auth/me', rootToken)).data;
- const rootUserId = String(rootMe.id);
- check('root 使用管理员登录入口进入内部维护角色', rootMe.roles.some(r => r.code === 'root') && rootMe.maintenanceAccount === true);
- const roles = records((await call('auth/v1/roles/all', token)).data);
- const adminRole = roles.find(r => r.code === 'admin'), teacherRole = roles.find(r => r.code === 'teacher'), studentRole = roles.find(r => r.code === 'student');
- check('普通角色目录不显示内部维护角色', !roles.some(r => r.code === 'root'));
- check('管理员角色采用固定的受限授权', adminRole.permissionPolicy === 'FIXED' && adminRole.superAdmin === false);
- const grants = (await call(`auth/v1/permissions/roles/${adminRole.id}`, token)).data;
- check('管理员授权已持久化,不动态获得全权限', grants.dynamicAll === false && grants.permissionCodes.length > 0 && !grants.permissionCodes.includes('content.model.create'));
- for (const [name, actor] of [['admin', token], ['root', rootToken]]) {
- const forbidden = await call(`auth/v1/permissions/roles/${adminRole.id}`, actor, { permissionCodes: grants.permissionCodes, version: grants.version }, 'PUT');
- check(`${name} 无法通过接口修改固定管理员授权`, forbidden.status === 403 || forbidden.status === 400);
- }
- const normalAdmin = (await call(`auth/v1/users/${me.id}`, token)).data;
- check('admin 账号已取消 is_protected', normalAdmin.isProtected === false);
- const users = records((await call('auth/v1/users?size=100', token)).data);
- check('用户目录不显示 root 维护账号', !users.some(u => String(u.id) === rootUserId));
- for (const [name, body, method, tail] of [
- ['查询', undefined, 'GET', ''], ['停用', { enabled: false, version: 0 }, 'PUT', '/status'],
- ['重置密码', { password: `Test!${crypto.randomBytes(7).toString('hex')}`, version: 0 }, 'PUT', '/password'],
- ['删除', { version: 0 }, 'DELETE', ''],
- ]) {
- const forbidden = await call(`auth/v1/users/${rootUserId}${tail}`, token, body, method);
- check(`普通账号按 ID ${name} root 受保护`, [400, 403, 404].includes(forbidden.status));
- }
- const maintenanceDenied = await call('auth/v1/maintenance/audit-logs', token);
- check('管理员无法访问维护审计', maintenanceDenied.status === 403);
- const normalAudit = records((await call('auth/v1/audit-logs?size=100', token)).data);
- check('普通审计不混入 root 登录与操作记录', !normalAudit.some(x => String(x.userId) === rootUserId || x.username === 'root'));
- const internalAudit = (await call('auth/v1/maintenance/audit-logs?size=100', rootToken)).data;
- check('root 活动保留在可追溯的内部审计', records(internalAudit).some(x => x.username === 'root'));
- const catalog = (await call('auth/v1/permissions/catalog', token)).data;
- const permissionItems = catalog.flatMap(s => s.items);
- check('目录提供模型独立动作与查看依赖', ['create', 'update', 'delete', 'publish'].every(action => permissionItems.some(p => p.code === `content.model.${action}` && p.requires.includes('content.model'))));
- check('已移除旧的共用内容写权限', !permissionItems.some(p => ['content.create', 'content.update', 'content.publish'].includes(p.code)));
- const suffix = crypto.randomBytes(4).toString('hex');
- const fixture = { createdAt: new Date().toISOString(), roleIds: [], projectIds: [], user: null, adminRoleId: adminRole.id, teacherRoleId: teacherRole.id, studentRoleId: studentRole.id };
- if (fs.existsSync(fixtureFile) && !JSON.parse(fs.readFileSync(fixtureFile, 'utf8')).cleanedAt) {
- throw new Error('Active fixture file exists; cleanup or reuse it first');
- }
- write(fixture);
- for (const [kind, name, permissions] of [
- ['reader', '权限实测·模型只读', ['dashboard.view', 'content.model']],
- ['editor', '权限实测·模型编辑', ['dashboard.view', 'content.model', 'content.model.create', 'content.model.update', 'content.model.delete', 'content.model.publish']],
- ]) {
- const made = await call('auth/v1/roles', token, { code: `rv_${kind}_${suffix}`, name, shortName: '测', description: 'UTE2E-PERMISSIONS-20260906', dataScope: 'ALL', enabled: true, sortOrder: 90, departmentScopeIds: [] });
- check(`创建可配置的${kind}测试角色`, made.status === 200);
- fixture.roleIds.push(made.data.id); fixture[kind] = made.data; write(fixture);
- const saved = await call(`auth/v1/permissions/roles/${made.data.id}`, token, { permissionCodes: permissions, version: made.data.dataVersion }, 'PUT');
- check(`保存${kind}细分授权`, saved.status === 200);
- }
- const password = `Rv!${crypto.randomBytes(7).toString('hex')}`;
- const userCommand = { username: `rv_perm_${suffix}`, displayName: '权限与角色切换实测', departmentId: me.departmentId, roleIds: [teacherRole.id, studentRole.id, ...fixture.roleIds], defaultRoleId: teacherRole.id, enabled: true, password, remark: 'UTE2E-PERMISSIONS-20260906' };
- const madeUser = await call('auth/v1/users', token, userCommand);
- check('建立隔离的多角色测试账号', madeUser.status === 200);
- fixture.user = { ...madeUser.data, password }; write(fixture);
- const session = await call('auth/v1/auth/login', null, { userId: fixture.user.id, password, roleCode: 'teacher', departmentId: me.departmentId, rememberMe: false });
- check('多角色测试账号正常登录', session.status === 200);
- let actor = session.data.accessToken;
- for (const kind of ['reader', 'editor']) {
- const switched = await call('auth/v1/auth/active-role', actor, { roleId: fixture[kind].id }, 'PUT');
- check(`接口切换到${kind}角色`, switched.status === 200);
- actor = switched.data?.accessToken || actor;
- const detail = await call('auth/v1/auth/me', actor);
- check(`${kind}当前身份独立生效`, String(detail.data.activeRoleId) === String(fixture[kind].id));
- const list = await call('tran/v1/content/projects?type=MODEL&size=10', actor);
- check(`${kind}拥有模型查看权限`, list.status === 200);
- const project = await call('tran/v1/content/projects', actor, { type: 'MODEL', code: `RV-PERM-${suffix}-${kind}`, name: '权限实测临时模型', categoryCode: 'EQUIPMENT', content: {}, dependencies: [], assets: [] });
- if (kind === 'reader') check('只有查看权限时,直接新建接口返回403', project.status === 403);
- else {
- check('自定义非教员角色获得新增权限即可创建模型', project.status === 200);
- assert.ok(project.data?.project?.id, 'Created project returns a stable project ID');
- fixture.projectIds.push(project.data.project.id); write(fixture);
- }
- }
- await call('auth/v1/auth/logout', actor, {});
- const adminWrite = await call('tran/v1/content/projects', token, { type: 'MODEL', name: '禁止生成的模型', content: {} });
- check('普通管理员模型查看与创建权限相互独立', adminWrite.status === 403);
- fs.writeFileSync(path.join(report, 'api-checks.json'), JSON.stringify({ time: new Date().toISOString(), checks, catalogCount: permissionItems.length, adminPermissionCount: grants.permissionCodes.length }, null, 2));
- console.log(JSON.stringify({ passed: checks.length, fixturesReady: true }));
- } finally {
- await call('auth/v1/auth/logout', token, {}).catch(() => {});
- await call('auth/v1/auth/logout', rootToken, {}).catch(() => {});
- }
- }
-
- async function cleanup() {
- const fixture = JSON.parse(fs.readFileSync(fixtureFile, 'utf8'));
- const session = await login('root'); const token = session.accessToken;
- const result = { time: new Date().toISOString(), projects: [], users: [], roles: [] };
- try {
- for (const id of fixture.projectIds) {
- const existing = await call(`tran/v1/content/projects/${id}`, token);
- if (existing.status === 404) continue;
- assert.equal(existing.status, 200, 'Read isolated project before cleanup');
- const project = existing.data.project;
- assert.equal(project.name, '权限实测临时模型', 'Only isolated fixture projects can be removed');
- assert.equal(String(project.ownerUserId), String(fixture.user.id), 'Fixture project must belong to this run’s temporary account');
- assert.ok(project.addTime >= Math.floor(Date.parse(fixture.createdAt) / 1000), 'Fixture project must have been created during this run');
- const removed = await call(`tran/v1/content/projects/${id}?version=${project.version}`, token, undefined, 'DELETE');
- assert.equal(removed.status, 200, 'Remove isolated project'); result.projects.push(String(id));
- }
- if (fixture.user?.id) {
- const existing = await call(`auth/v1/users/${fixture.user.id}`, token);
- if (existing.status !== 404) {
- assert.equal(existing.status, 200);
- assert.equal(existing.data.remark, 'UTE2E-PERMISSIONS-20260906');
- const removed = await call(`auth/v1/users/${fixture.user.id}`, token, { version: existing.data.version ?? existing.data.dataVersion }, 'DELETE');
- assert.equal(removed.status, 200, 'Remove isolated test account'); result.users.push(String(fixture.user.id));
- }
- }
- for (const id of fixture.roleIds) {
- const existing = await call(`auth/v1/roles/${id}`, token);
- if (existing.status === 404) continue;
- assert.equal(existing.status, 200);
- assert.equal(existing.data.description, 'UTE2E-PERMISSIONS-20260906');
- const removed = await call(`auth/v1/roles/${id}`, token, { version: existing.data.version ?? existing.data.dataVersion }, 'DELETE');
- assert.equal(removed.status, 200, 'Remove isolated test role'); result.roles.push(String(id));
- }
- fixture.cleanedAt = result.time; if (fixture.user) delete fixture.user.password; write(fixture);
- fs.writeFileSync(path.join(report, 'cleanup.json'), JSON.stringify({ ...result, status: 'PASS' }, null, 2));
- console.log(JSON.stringify({ cleanup: 'PASS', projects: result.projects.length, users: result.users.length, roles: result.roles.length }));
- } finally { await call('auth/v1/auth/logout', token, {}).catch(() => {}); }
- }
-
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
- const cleanupMode = process.argv.includes('--cleanup');
- (cleanupMode ? cleanup() : prepare()).catch(error => {
- fs.writeFileSync(path.join(report, cleanupMode ? 'cleanup.json' : 'api-checks.json'), JSON.stringify({ time: new Date().toISOString(), status: 'FAIL', ...(cleanupMode ? {} : { checks }), error: error.message }, null, 2));
- console.error(error.message); process.exitCode = 1;
- });
- }
|