Narrow authorize-target to pending approvals
This commit is contained in:
1 parent
3d6dc62957
commit
6fef62aa8e
2 files changed
+47
-25
No files matched your search
@@ -50,4 +50,17 @@ describe('local API authorization hardening invariants', () => {
|
|||||||
expect(serverSource).toMatch(/Wildcard host approval requires explicit opt-in/);
|
expect(serverSource).toMatch(/Wildcard host approval requires explicit opt-in/);
|
||||||
expect(serverSource).toMatch(/body\.allowWildcard !== true/);
|
expect(serverSource).toMatch(/body\.allowWildcard !== true/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('authorize-target only approves existing pending receipts and cannot mint broad active-action approvals', () => {
|
||||||
|
const route = routeBlock("app.post('/api/approvals/authorize-target'", "app.post('/api/approvals/:id/reject'");
|
||||||
|
|
||||||
|
expect(route).toMatch(/approvalId/);
|
||||||
|
expect(route).toMatch(/approvalIds/);
|
||||||
|
expect(route).toMatch(/approvalRequests\.get\(id\)/);
|
||||||
|
expect(route).toMatch(/approval\.status !== 'pending'/);
|
||||||
|
expect(route).toMatch(/Math\.min\(30/);
|
||||||
|
expect(route).not.toMatch(/createApprovalRequest/);
|
||||||
|
expect(route).not.toMatch(/allowedActions/);
|
||||||
|
expect(route).not.toMatch(/command_execution['"],\s*['"]autonomous_execution/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
+34
-25
@@ -4874,8 +4874,15 @@ app.post('/api/approvals/:id/approve', (req: Request, res: Response) => {
|
|||||||
app.post('/api/approvals/authorize-target', (req: Request, res: Response) => {
|
app.post('/api/approvals/authorize-target', (req: Request, res: Response) => {
|
||||||
const body = req.body as Record<string, unknown>;
|
const body = req.body as Record<string, unknown>;
|
||||||
const target = normalizeTargetValue(body.target);
|
const target = normalizeTargetValue(body.target);
|
||||||
if (!target) {
|
const approvalIds = [
|
||||||
res.status(400).json({ error: 'target required' });
|
typeof body.approvalId === 'string' ? body.approvalId : '',
|
||||||
|
...(Array.isArray(body.approvalIds) ? body.approvalIds.filter((id): id is string => typeof id === 'string') : []),
|
||||||
|
].filter(Boolean);
|
||||||
|
if (!approvalIds.length) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'approvalId or approvalIds required',
|
||||||
|
next: 'Request a pending receipt first, then approve that exact receipt id.',
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (target === '*') {
|
if (target === '*') {
|
||||||
@@ -4893,35 +4900,37 @@ app.post('/api/approvals/authorize-target', (req: Request, res: Response) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestedActions = Array.isArray(body.actions) ? body.actions.map(String) : [];
|
const ttlMinutes = Math.max(1, Math.min(30, Number(body.ttlMinutes || 30)));
|
||||||
const allowedActions: GuardAction[] = ['mission_execution', 'network_request', 'command_execution', 'autonomous_execution'];
|
const expiresAt = new Date(Date.now() + ttlMinutes * 60_000).toISOString();
|
||||||
const actions = (requestedActions.length ? requestedActions : allowedActions)
|
const approvals: ApprovalRequest[] = [];
|
||||||
.filter((action): action is GuardAction => allowedActions.includes(action as GuardAction));
|
for (const id of [...new Set(approvalIds)]) {
|
||||||
if (!actions.length) {
|
const approval = approvalRequests.get(id);
|
||||||
res.status(400).json({ error: 'No supported approval actions requested' });
|
if (!approval) {
|
||||||
return;
|
res.status(404).json({ error: 'Approval request not found', approvalId: id });
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
const ttlMinutes = Number(body.ttlMinutes || process.env.T3MP3ST_APPROVAL_TTL_MINUTES || 240);
|
if (approval.status !== 'pending') {
|
||||||
const expiresAt = new Date(Date.now() + Math.max(1, ttlMinutes) * 60_000).toISOString();
|
res.status(409).json({ error: 'Approval request is not pending', approvalId: id, status: approval.status });
|
||||||
const approvals = actions.map((action) => {
|
return;
|
||||||
const approval = createApprovalRequest(
|
}
|
||||||
action,
|
if (target && !approvalMatches(approval, approval.action, target)) {
|
||||||
target,
|
res.status(400).json({
|
||||||
typeof body.reason === 'string' && body.reason.trim()
|
error: 'Approval target mismatch',
|
||||||
? body.reason.trim()
|
approvalId: id,
|
||||||
: `Authorize ${action} against ${target}`,
|
approvalTarget: approval.target,
|
||||||
body
|
target,
|
||||||
);
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
approval.status = 'approved';
|
approval.status = 'approved';
|
||||||
approval.approvedBy = typeof body.approvedBy === 'string' ? body.approvedBy : 'local-operator';
|
approval.approvedBy = typeof body.approvedBy === 'string' ? body.approvedBy : 'local-operator';
|
||||||
approval.expiresAt = expiresAt;
|
approval.expiresAt = expiresAt;
|
||||||
approval.updatedAt = nowIso();
|
approval.updatedAt = nowIso();
|
||||||
emitContractEvent('approval.approved', { approvalId: approval.id, action: approval.action, target: approval.target });
|
emitContractEvent('approval.approved', { approvalId: approval.id, action: approval.action, target: approval.target });
|
||||||
return approval;
|
approvals.push(approval);
|
||||||
});
|
}
|
||||||
|
|
||||||
res.status(201).json({ target, expiresAt, approvals });
|
res.json({ target: target || null, expiresAt, approvals });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/approvals/:id/reject', (req: Request, res: Response) => {
|
app.post('/api/approvals/:id/reject', (req: Request, res: Response) => {
|
||||||
|
|||||||
Reference in new issue
Block a user