120 lines
5.7 KiB
JavaScript
120 lines
5.7 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import http from 'node:http';
|
|
import test from 'node:test';
|
|
import { once } from 'node:events';
|
|
|
|
import { verifyKnowledgePlatform } from '../verify-knowledge-platform.mjs';
|
|
|
|
test('真实HTTP验证器覆盖正例、跨租户反例、伪造身份和限流', async (t) => {
|
|
const rateLimit = 3;
|
|
let rateProbes = 0;
|
|
const server = http.createServer(async (request, response) => {
|
|
const chunks = [];
|
|
for await (const chunk of request) chunks.push(chunk);
|
|
const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {};
|
|
const token = String(request.headers.authorization || '').replace(/^Bearer\s+/i, '');
|
|
let code = 200;
|
|
let data = null;
|
|
|
|
if (request.url === '/api/knowledge/admin/spaces') {
|
|
data = token === 'admin-token'
|
|
? ['yc_public_policy', 'yc_property_sop', 'yc_management_ops'].map((value) => ({ code: value }))
|
|
: null;
|
|
code = data ? 200 : 401;
|
|
} else if (token.endsWith('x') || !['employee-token', 'supervisor-token', 'meitu-token'].includes(token)) {
|
|
code = 401;
|
|
} else if (body.queryText === 'rate-limit-probe') {
|
|
rateProbes += 1;
|
|
code = rateProbes > rateLimit ? 429 : 403;
|
|
} else if (request.url === '/api/open/knowledge/query' && body.toolCode) {
|
|
code = 403;
|
|
} else if (body.toolCode === 'TEAM_PRACTICE_SUMMARY' && token === 'employee-token') {
|
|
code = 403;
|
|
} else if (body.toolCode) {
|
|
data = {
|
|
citations: [{ sourceType: 'DATA_TOOL', docId: body.toolCode }],
|
|
data: { scope: token === 'employee-token' ? 'SELF' : 'TEAM' }
|
|
};
|
|
} else {
|
|
const codeRequested = body.spaceCodes?.[0];
|
|
const isMeitu = token === 'meitu-token';
|
|
const allowed = isMeitu
|
|
? codeRequested === 'mt_customer_service'
|
|
: ['yc_public_policy', 'yc_property_sop', 'yc_management_ops'].includes(codeRequested);
|
|
const roleAllowed = codeRequested !== 'yc_management_ops' || token === 'supervisor-token';
|
|
if (!allowed || !roleAllowed) {
|
|
code = 403;
|
|
} else {
|
|
data = {
|
|
usedSpaceCodes: [codeRequested],
|
|
citations: [{ sourceType: 'DOCUMENT', spaceCode: codeRequested }]
|
|
};
|
|
}
|
|
}
|
|
|
|
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
response.end(JSON.stringify({ code, msg: code === 200 ? '操作成功' : 'rejected', data }));
|
|
});
|
|
server.listen(0, '127.0.0.1');
|
|
await once(server, 'listening');
|
|
t.after(() => server.close());
|
|
const address = server.address();
|
|
const logs = [];
|
|
const results = await verifyKnowledgePlatform({
|
|
AIHR_BASE_URL: `http://127.0.0.1:${address.port}`,
|
|
AIHR_SILVER_ADMIN_TOKEN: 'admin-token',
|
|
AIHR_SILVER_EMPLOYEE_TOKEN: 'employee-token',
|
|
AIHR_SILVER_SUPERVISOR_TOKEN: 'supervisor-token',
|
|
AIHR_MEITU_APP_TOKEN: 'meitu-token',
|
|
AIHR_SHARED_DOC_QUERY: 'shared-marker',
|
|
AIHR_MEITU_RATE_LIMIT: rateLimit,
|
|
AIHR_VERIFY_RATE_LIMIT: true,
|
|
AIHR_VERIFY_TIMEOUT_MS: 2_000
|
|
}, { log: (line) => logs.push(line) });
|
|
|
|
assert.equal(results.length, 15);
|
|
assert.equal(results.every((item) => item.status === 'PASS'), true);
|
|
assert.equal(logs.some((line) => /token/.test(line)), false);
|
|
assert.equal(rateProbes, rateLimit + 1);
|
|
});
|
|
|
|
test('服务端安全合同在RAG前拒绝空范围,且身份只取认证上下文', async () => {
|
|
const root = new URL('../../', import.meta.url);
|
|
const queryService = await readFile(new URL(
|
|
'backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/service/AihrKnowledgeQueryService.java', root), 'utf8');
|
|
const queryDto = await readFile(new URL(
|
|
'backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/domain/AihrKnowledgeQueryDto.java', root), 'utf8');
|
|
const accessService = await readFile(new URL(
|
|
'backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/service/AihrKnowledgeAccessService.java', root), 'utf8');
|
|
const appService = await readFile(new URL(
|
|
'backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/service/AihrKnowledgeAppService.java', root), 'utf8');
|
|
|
|
assert.doesNotMatch(queryDto, /tenantId|userId|extPartyId/);
|
|
assert.match(queryService, /principalResolver\.current\(\)/);
|
|
assert.match(queryService, /TenantHelper\.dynamic\(app\.tenantId\(\)/);
|
|
assert.match(queryService, /resolveInternalSpaceIds[\s\S]*queryDocuments/);
|
|
assert.match(accessService, /if \(effective\.isEmpty\(\)\)[\s\S]*throw forbidden/);
|
|
assert.match(accessService, /where s\.tenant_id = \? and s\.app_id = \?/);
|
|
assert.match(accessService, /where g\.tenant_id = \?/);
|
|
assert.match(appService, /expiresTime\(\) == null \|\| row\.expiresTime\(\)\.isAfter/);
|
|
assert.match(appService, /RateType\.OVERALL, rate, 60/);
|
|
});
|
|
|
|
test('验证器从内部JWT自动携带clientid且不要求额外密钥配置', async () => {
|
|
const source = await readFile(new URL('../verify-knowledge-platform.mjs', import.meta.url), 'utf8');
|
|
assert.match(source, /payload\.clientid/);
|
|
assert.match(source, /clientid: clientId/);
|
|
assert.doesNotMatch(source, /AIHR_.*CLIENT_ID/);
|
|
});
|
|
|
|
test('破坏性解绑验证默认关闭且本地运行器最终撤销临时令牌', async () => {
|
|
const verifier = await readFile(new URL('../verify-knowledge-platform.mjs', import.meta.url), 'utf8');
|
|
const localRunner = await readFile(new URL('../verify-knowledge-platform-local.mjs', import.meta.url), 'utf8');
|
|
assert.match(verifier, /AIHR_VERIFY_UNBIND: env\.AIHR_VERIFY_UNBIND === 'true'/);
|
|
assert.match(verifier, /removed\.data\?\.ossDeleted !== false/);
|
|
assert.match(verifier, /解绑后其他空间检索/);
|
|
assert.match(localRunner, /finally \{[\s\S]*disableTemporaryMeituToken\(\)/);
|
|
assert.doesNotMatch(localRunner, /console\.log\([^\n]*externalToken/);
|
|
});
|