|
- import fs from 'node:fs';
- import path from 'node:path';
- import crypto from 'node:crypto';
- import assert from 'node:assert/strict';
- import { spawnSync } from 'node:child_process';
- import { fileURLToPath } from 'node:url';
- import { login, call, root, work, report } from './role-permissions-live.mjs';
-
- // Run only after migration, service restart and the separate prepare fixture have completed.
- // Credentials remain inside the existing login helper; no response token, password or user payload is reported.
- const checks = [];
- const sessions = [];
- const withoutFixtures = process.argv.includes('--without-fixtures');
- const outcome = { startedAt: new Date().toISOString(), mode: 'LIVE_API', checks, limitations: [] };
- let activeCheck = 'initialization';
- const rows = value => value?.records ?? value ?? [];
- const sameId = (left, right) => String(left) === String(right);
- const ids = values => values.map(String).sort();
- function check(name, condition, evidence = {}) {
- activeCheck = name;
- checks.push({ name, result: condition ? 'PASS' : 'FAIL', ...evidence });
- assert.ok(condition, name);
- }
- async function read(endpoint, token) {
- activeCheck = `读取 ${endpoint.split('?')[0]}`;
- const result = await call(endpoint, token);
- assert.equal(result.status, 200, activeCheck);
- return result.data;
- }
- async function failedLogin(username, requestId) {
- const response = await fetch('http://127.0.0.1:6180/api/auth/v1/auth/login', {
- method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Request-Id': requestId },
- body: JSON.stringify({ username, password: `Bad!${crypto.randomBytes(7).toString('hex')}`, roleCode: 'admin', rememberMe: false }),
- signal: AbortSignal.timeout(30000),
- });
- const value = await response.json();
- return { status: response.status, requestId: value.requestId || response.headers.get('X-Request-Id') };
- }
- function databaseSnapshot(requestIds = []) {
- const source = String.raw`
- import importlib.util,json,sys
- from pathlib import Path
- p=Path(sys.argv[1])/'ute2e'/'tools'/'review-db.py'
- spec=importlib.util.spec_from_file_location('review_db_private',p)
- module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
- connection=module.connect()
- try:
- connection.execute('BEGIN READ ONLY')
- result={}
- with connection.cursor() as cursor:
- cursor.execute("SELECT COUNT(*) AS total, COUNT(*) FILTER(WHERE result_code=1) AS success, COUNT(*) FILTER(WHERE result_code=0) AS failed FROM ut_sys_audit_log WHERE is_delete=0 AND visibility_scope='BUSINESS'")
- result['businessAudit']=cursor.fetchone()
- cursor.execute("SELECT COUNT(*) AS total, COUNT(*) FILTER(WHERE result_code=1) AS success, COUNT(*) FILTER(WHERE result_code=0) AS failed FROM ut_sys_audit_log WHERE is_delete=0 AND visibility_scope='INTERNAL'")
- result['internalAudit']=cursor.fetchone()
- cursor.execute("SELECT COUNT(*) AS total, COUNT(*) FILTER(WHERE status=1) AS enabled FROM ut_sys_user WHERE is_delete=0 AND is_protected=0")
- result['ordinaryUsers']=cursor.fetchone()
- cursor.execute("SELECT COUNT(*) AS total FROM ut_sys_role WHERE is_delete=0 AND role_code<>'root'")
- result['ordinaryRoles']=cursor.fetchone()
- cursor.execute("SELECT id, request_id, visibility_scope, result_code, actor_user_id, target_type, target_id FROM ut_sys_audit_log WHERE request_id=ANY(%s) ORDER BY id",(json.loads(sys.argv[2]),))
- result['events']=cursor.fetchall()
- connection.rollback()
- print(json.dumps(result,default=str))
- finally:
- connection.close()
- `;
- const result = spawnSync('python', ['-c', source, root, JSON.stringify(requestIds)], { encoding: 'utf8', windowsHide: true });
- // Neither stderr nor the private source configuration is copied into the report.
- assert.equal(result.status, 0, '只读数据库证据查询成功');
- return JSON.parse(result.stdout.trim());
- }
- function exportedContains(data, needle) {
- const source = String.raw`
- import base64,io,json,sys,zipfile
- payload=json.load(sys.stdin)
- with zipfile.ZipFile(io.BytesIO(base64.b64decode(payload['contentBase64']))) as archive:
- contents=b'\n'.join(archive.read(name) for name in archive.namelist() if name.startswith('xl/') and name.endswith('.xml'))
- print(json.dumps({'contains':payload['needle'].encode('utf-8') in contents}))
- `;
- const result = spawnSync('python', ['-c', source], {
- input: JSON.stringify({ contentBase64: data.contentBase64, needle }), encoding: 'utf8', windowsHide: true,
- });
- assert.equal(result.status, 0, '导出文件内容校验成功');
- return JSON.parse(result.stdout).contains;
- }
- function roleCommand(role, overrides = {}) {
- return { code: role.code, name: role.name, shortName: role.shortName, description: role.description,
- dataScope: role.dataScope, enabled: role.enabled, sortOrder: role.sortOrder,
- departmentScopeIds: role.departmentScopeIds, version: role.dataVersion, ...overrides };
- }
- function userCommand(user, overrides = {}) {
- return { username: user.username, displayName: user.displayName, departmentId: user.departmentId,
- roleIds: user.roleIds, defaultRoleId: user.defaultRoleId, enabled: user.enabled, remark: user.remark,
- version: user.dataVersion, ...overrides };
- }
-
- async function main() {
- const fixture = withoutFixtures ? {} : JSON.parse(fs.readFileSync(path.join(work, 'fixtures.json'), 'utf8'));
- if (!withoutFixtures) assert.ok(fixture.user?.id && fixture.adminRoleId, '隔离测试账号已由 prepare 创建');
- else outcome.limitations.push('首阶段不依赖隔离账号,内部角色赋值拒绝及测试账号版本保持由完整运行补测。');
- const admin = await login('admin'); sessions.push(admin.accessToken);
- const maintenance = await login('root'); sessions.push(maintenance.accessToken);
- const token = admin.accessToken, rootToken = maintenance.accessToken;
- const me = await read('auth/v1/auth/me', token), rootMe = await read('auth/v1/auth/me', rootToken);
- const rootUserId = rootMe.id, internalRole = rootMe.roles.find(role => role.code === 'root');
- check('维护账号使用已分配的内部角色,普通管理员不带维护标记',
- !!internalRole && rootMe.maintenanceAccount === true && me.maintenanceAccount === false);
-
- for (const [actor, actorToken] of [['admin', token], ['root', rootToken]]) {
- const all = rows(await read('auth/v1/roles/all', actorToken));
- const page = rows(await read('auth/v1/roles?size=200', actorToken));
- check(`${actor} 的普通角色目录和分页都隐藏内部角色`, [...all, ...page].every(role => role.code !== 'root'));
- for (const suffix of ['', '/users', '/permissions']) {
- const result = await call(`auth/v1/roles/${internalRole.id}${suffix}`, actorToken);
- check(`${actor} 不能按已知内部角色 ID 读取${suffix || '/详情'}`, result.status === 404, { status: result.status });
- }
- }
- const ordinaryRoles = rows(await read('auth/v1/roles/all', token));
- for (const role of ordinaryRoles) {
- const linked = rows(await read(`auth/v1/roles/${role.id}/users`, token));
- check(`角色 ${role.code} 的关联用户排除维护账号`, linked.every(user => !sameId(user.id, rootUserId)));
- }
- const lookup = rows(await read(`auth/v1/directory/teaching-members?keyword=${encodeURIComponent(rootMe.username)}&limit=100`, token));
- check('教学人员搜索不暴露维护账号', lookup.every(user => !sameId(user.id, rootUserId)));
- const resolved = await call('auth/v1/directory/teaching-members/resolve', token, { ids: [rootUserId] });
- check('教学人员按 ID 解析拒绝维护账号', resolved.status === 400, { status: resolved.status });
- const candidates = rows(await read(`auth/v1/auth/login-identities?roleCode=teacher&departmentId=${rootMe.departmentId}`));
- check('公开登录候选列表不暴露维护账号', candidates.every(user => !sameId(user.id, rootUserId)));
-
- if (fixture.user?.id) {
- const originalUser = await read(`auth/v1/users/${fixture.user.id}`, token);
- for (const [actor, actorToken] of [['admin', token], ['root', rootToken]]) {
- const attempt = await call(`auth/v1/users/${fixture.user.id}`, actorToken,
- userCommand(originalUser, { roleIds: [...originalUser.roleIds, internalRole.id] }), 'PUT');
- if (attempt.status === 200) {
- // The only mutable target is the isolated fixture. Restore before reporting an unexpected authorization failure.
- const current = await read(`auth/v1/users/${fixture.user.id}`, rootToken);
- await call(`auth/v1/users/${fixture.user.id}`, rootToken, userCommand(originalUser, { version: current.dataVersion }), 'PUT');
- }
- check(`${actor} 无法通过普通用户接口给测试账号授予内部角色`, [400, 403].includes(attempt.status), { status: attempt.status });
- }
- const afterUser = await read(`auth/v1/users/${fixture.user.id}`, token);
- check('拒绝内部角色分配后测试账号原角色和版本保持不变',
- JSON.stringify(ids(afterUser.roleIds)) === JSON.stringify(ids(originalUser.roleIds)) && afterUser.dataVersion === originalUser.dataVersion);
- }
-
- const adminRoleId = fixture.adminRoleId ?? ordinaryRoles.find(role => role.code === 'admin')?.id;
- const fixed = await read(`auth/v1/roles/${adminRoleId}`, token);
- for (const [actor, actorToken] of [['admin', token], ['root', rootToken]]) {
- const scope = await call(`auth/v1/roles/${fixed.id}`, actorToken, roleCommand(fixed, { dataScope: 'SELF' }), 'PUT');
- if (scope.status === 200) {
- const current = await read(`auth/v1/roles/${fixed.id}`, rootToken);
- await call(`auth/v1/roles/${fixed.id}`, rootToken, roleCommand(fixed, { version: current.dataVersion }), 'PUT');
- }
- check(`${actor} 无法通过普通编辑更改固定管理员数据范围`, scope.status === 403, { status: scope.status });
- const status = await call(`auth/v1/roles/${fixed.id}/status`, actorToken, { enabled: false, version: fixed.dataVersion }, 'PUT');
- if (status.status === 200) {
- const current = await read(`auth/v1/roles/${fixed.id}`, rootToken);
- await call(`auth/v1/roles/${fixed.id}/status`, rootToken, { enabled: true, version: current.dataVersion }, 'PUT');
- }
- check(`${actor} 无法停用固定管理员角色`, status.status === 403, { status: status.status });
- }
- const afterRole = await read(`auth/v1/roles/${fixed.id}`, token);
- check('固定管理员角色状态、范围、版本未被边界请求改变',
- afterRole.enabled === fixed.enabled && afterRole.dataScope === fixed.dataScope && afterRole.dataVersion === fixed.dataVersion);
-
- for (const suffix of ['', '/stats', '/export']) {
- const denied = await call(`auth/v1/maintenance/audit-logs${suffix}`, token);
- check(`普通管理员不能读取维护审计${suffix || '/列表'}`, denied.status === 403, { status: denied.status });
- }
- const sequence = crypto.randomBytes(6).toString('hex');
- const rootAttempt = await failedLogin(rootMe.username, `maint-root-failure-${sequence}`);
- const unknownAttempt = await failedLogin(`missing_${sequence}`, `maint-unknown-failure-${sequence}`);
- check('已知维护账号错误密码和未知账号均返回统一认证失败', rootAttempt.status === 401 && unknownAttempt.status === 401);
- const rootFilter = encodeURIComponent(rootAttempt.requestId), unknownFilter = encodeURIComponent(unknownAttempt.requestId);
- const ordinaryKnown = rows(await read(`auth/v1/audit-logs?keyword=${rootFilter}`, token));
- const internalKnown = rows(await read(`auth/v1/maintenance/audit-logs?keyword=${rootFilter}`, rootToken));
- const ordinaryUnknown = rows(await read(`auth/v1/audit-logs?keyword=${unknownFilter}`, token));
- check('维护登录失败在普通列表不可见且内部审计保留真实 USER 目标', ordinaryKnown.length === 0
- && internalKnown.some(event => event.targetType === 'USER' && sameId(event.targetId, rootUserId) && event.success === false));
- check('未知身份失败仍可在普通安全审计中查到', ordinaryUnknown.some(event => event.requestId === unknownAttempt.requestId && event.success === false));
- const ordinaryExport = await read(`auth/v1/audit-logs/export?keyword=${rootFilter}`, token);
- const internalExport = await read(`auth/v1/maintenance/audit-logs/export?keyword=${rootFilter}`, rootToken);
- check('普通审计导出不包含维护记录,维护导出保留该记录', ordinaryExport.recordCount === 0
- && !exportedContains(ordinaryExport, rootAttempt.requestId) && internalExport.recordCount >= 1
- && exportedContains(internalExport, rootAttempt.requestId));
-
- const before = databaseSnapshot([rootAttempt.requestId, unknownAttempt.requestId]);
- const auditStats = await read('auth/v1/audit-logs/stats', token);
- const internalStats = await read('auth/v1/maintenance/audit-logs/stats', rootToken);
- const userStats = await read('auth/v1/users/stats', token);
- const roleStats = await read('auth/v1/roles/stats', token);
- const dashboard = await read('auth/v1/dashboard/platform', token);
- const after = databaseSnapshot([rootAttempt.requestId, unknownAttempt.requestId]);
- const between = (value, first, last) => value >= first && value <= last;
- check('普通审计统计只计算 BUSINESS,维护统计只计算 INTERNAL',
- ['total', 'success', 'failed'].every(key => between(auditStats[key], before.businessAudit[key], after.businessAudit[key])
- && between(internalStats[key], before.internalAudit[key], after.internalAudit[key])));
- check('普通用户和角色统计排除内部账号及内部角色',
- between(userStats.total, before.ordinaryUsers.total, after.ordinaryUsers.total)
- && between(roleStats.total, before.ordinaryRoles.total, after.ordinaryRoles.total));
- check('首页人数不计维护账号,最近动态不显示维护失败日志',
- between(dashboard.metrics.userTotal, before.ordinaryUsers.total, after.ordinaryUsers.total)
- && !dashboard.activities.some(activity => internalKnown.some(event => sameId(activity.id, event.id))));
- const knownDb = after.events.find(event => event.request_id === rootAttempt.requestId);
- const unknownDb = after.events.find(event => event.request_id === unknownAttempt.requestId);
- check('数据库只读核验:已知维护失败 INTERNAL,未知失败 BUSINESS',
- knownDb?.visibility_scope === 'INTERNAL' && sameId(knownDb.target_id, rootUserId)
- && unknownDb?.visibility_scope === 'BUSINESS' && !unknownDb.target_id && !unknownDb.actor_user_id);
- outcome.databaseEvidence = {
- ordinaryUserCount: after.ordinaryUsers.total, ordinaryRoleCount: after.ordinaryRoles.total,
- businessAuditCount: after.businessAudit.total, internalAuditCount: after.internalAudit.total,
- knownFailureScope: knownDb?.visibility_scope, unknownFailureScope: unknownDb?.visibility_scope,
- knownFailurePreservesTarget: sameId(knownDb?.target_id, rootUserId),
- };
-
- // Restore the failed-login counter by a normal successful authentication; no direct account or credential writes.
- const verified = await login('root'); sessions.push(verified.accessToken);
- const normalAdmin = await read(`auth/v1/users/${me.id}`, token);
- check('维护再次登录后普通 admin 仍未被重新保护', normalAdmin.isProtected === false);
- outcome.bootstrapReview = { result: 'PASS', note: '初始化无密码配置时立即返回;已有凭据不覆盖;成功时只将旧普通管理员保护置0,没有将其恢复为1的路径。主代理已移除一次性环境变量并重启Auth,本脚本在该服务上核验admin保护仍为false。' };
- outcome.status = withoutFixtures ? 'PASS_WITH_LIMITATIONS' : 'PASS';
- }
-
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
- try { await main(); }
- catch (error) {
- outcome.status = 'FAIL'; outcome.failedStep = activeCheck;
- outcome.error = error?.name === 'AssertionError' ? '验证断言失败,参见最后一项或 failedStep' : '请求或验证未完成,未输出原始异常以避免泄漏认证信息';
- process.exitCode = 1;
- } finally {
- for (const token of sessions) await call('auth/v1/auth/logout', token, {}).catch(() => {});
- outcome.finishedAt = new Date().toISOString();
- fs.writeFileSync(path.join(report, withoutFixtures ? 'maintenance-boundaries-initial.json' : 'maintenance-boundaries.json'), JSON.stringify(outcome, null, 2));
- console.log(JSON.stringify({ status: outcome.status, passed: checks.filter(item => item.result === 'PASS').length,
- failed: checks.filter(item => item.result === 'FAIL').length, failedStep: outcome.failedStep ?? null }));
- }
- }
|