fix(core): 执行记录查询补越权校验,待确认中的执行也能按 executionId 查到

This commit is contained in:
zh 2026-09-29 11:01:50 +08:00
parent 498e7e60b4
commit ee42650f9c
4 changed files with 20 additions and 6 deletions

View File

@ -257,6 +257,12 @@ test('A4.4 确认卡:写操作等确认、确认后执行、事件齐全、重
assert.ok(await stream.waitFor((event) => event.type === 'approval.requested'), '要推确认卡给界面'); assert.ok(await stream.waitFor((event) => event.type === 'approval.requested'), '要推确认卡给界面');
assert.equal(aps.stats.calls['aps.write'], undefined, '没确认前不能动业务系统'); assert.equal(aps.stats.calls['aps.write'], undefined, '没确认前不能动业务系统');
// 待确认中的执行也要能按 executionId 查到,且只有本人能查
const pendingView = await fetch(`${server.url}/v1/executions/${receipt.executionId}`, { headers: headersFor(subject) });
assert.equal((await pendingView.json()).status, 'pending-confirmation');
const stolenView = await fetch(`${server.url}/v1/executions/${receipt.executionId}`, { headers: headersFor(otherSubject) });
assert.equal(stolenView.status, 403, '执行记录只有本人能查');
const decision = await fetch(`${server.url}/v1/approvals/${receipt.approvalId}`, { const decision = await fetch(`${server.url}/v1/approvals/${receipt.approvalId}`, {
method: 'POST', method: 'POST',
headers: headersFor(subject), headers: headersFor(subject),

View File

@ -227,13 +227,13 @@ test('HTTP:注册 Provider 后能力清单非空,执行记录端点可查回
params: { text: '通过 HTTP 查回执' }, params: { text: '通过 HTTP 查回执' },
}); });
const response = await fetch(`${server.url}/v1/executions/${receipt.executionId}`); const response = await fetch(`${server.url}/v1/executions/${receipt.executionId}`, { headers: { 'x-aps-user': subject.userId, 'x-aps-tenant': subject.tenantId } });
assert.equal(response.status, 200, '执行记录端点应返回 200'); assert.equal(response.status, 200, '执行记录端点应返回 200');
const body = await response.json(); const body = await response.json();
assert.equal(body.executionId, receipt.executionId); assert.equal(body.executionId, receipt.executionId);
assert.equal(body.status, 'succeeded'); assert.equal(body.status, 'succeeded');
const missing = await fetch(`${server.url}/v1/executions/exec-none`); const missing = await fetch(`${server.url}/v1/executions/exec-none`, { headers: { 'x-aps-user': subject.userId } });
assert.equal(missing.status, 404, '未知执行记录应 404'); assert.equal(missing.status, 404, '未知执行记录应 404');
const missingBody = await missing.json(); const missingBody = await missing.json();
assert.equal(missingBody.error.code, 'not_found'); assert.equal(missingBody.error.code, 'not_found');

View File

@ -114,9 +114,17 @@ export class ApplicationService {
return this.contextRepository?.get(sessionId); return this.contextRepository?.get(sessionId);
} }
/** 执行记录查询:GET /v1/executions/{id} */ /** 执行记录查询:GET /v1/executions/{id};待确认中的执行也能查到(回执 status = pending-confirmation) */
execution(executionId: string) { execution(subject: Subject, executionId: string) {
return this.core.executions.get(executionId)?.receipt; const record = this.core.executions.get(executionId);
if (record) {
if (record.subjectUserId !== subject.userId) throw new AppError('forbidden', '这是别人的执行记录', { status: 403 });
return record.receipt;
}
const action = this.core.approvals?.byExecution(executionId);
if (!action) return undefined;
if (action.ownerUserId !== subject.userId) throw new AppError('forbidden', '这是别人的执行记录', { status: 403 });
return action.receipt;
} }
/** 执行追溯(A4.5):按 executionId 追到人、参数摘要、审批与回执;只有本人能看 */ /** 执行追溯(A4.5):按 executionId 追到人、参数摘要、审批与回执;只有本人能看 */

View File

@ -100,7 +100,7 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse, service:
return sendJson(res, 200, service.capabilities(subject, sessionFilter)); return sendJson(res, 200, service.capabilities(subject, sessionFilter));
case 'GET /v1/executions/{executionId}': { case 'GET /v1/executions/{executionId}': {
const executionId = params.get('executionId') ?? ''; const executionId = params.get('executionId') ?? '';
const receipt = service.execution(executionId); const receipt = service.execution(subject, executionId);
if (!receipt) throw new AppError('not_found', `执行记录 不存在`, { status: 404 }); if (!receipt) throw new AppError('not_found', `执行记录 不存在`, { status: 404 });
return sendJson(res, 200, receipt); return sendJson(res, 200, receipt);
} }