fix(tess): enforce command authorization approvals (#718)
This commit was merged in pull request #718.
This commit is contained in:
74
apps/gateway/src/chat/chat.gateway-command-approval.spec.ts
Normal file
74
apps/gateway/src/chat/chat.gateway-command-approval.spec.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { SlashCommandPayload } from '@mosaicstack/types';
|
||||
import { ChatGateway } from './chat.gateway.js';
|
||||
|
||||
const payload: SlashCommandPayload = {
|
||||
command: 'gc',
|
||||
conversationId: 'conversation-1',
|
||||
approvalId: 'approval-1',
|
||||
};
|
||||
|
||||
function buildGateway(commandExecutor: {
|
||||
execute: ReturnType<typeof vi.fn>;
|
||||
createApproval: ReturnType<typeof vi.fn>;
|
||||
}): ChatGateway {
|
||||
return new ChatGateway(
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
commandExecutor as never,
|
||||
{} as never,
|
||||
);
|
||||
}
|
||||
|
||||
describe('ChatGateway command approval ingress', () => {
|
||||
it('passes the client approval ID through to command execution while deriving the actor server-side', async (): Promise<void> => {
|
||||
const commandExecutor = {
|
||||
execute: vi.fn().mockResolvedValue({ ...payload, success: true }),
|
||||
createApproval: vi.fn(),
|
||||
};
|
||||
const gateway = buildGateway(commandExecutor);
|
||||
const client = { data: { user: { id: 'admin-1' } }, emit: vi.fn() };
|
||||
|
||||
await gateway.handleCommandExecute(client as never, payload);
|
||||
|
||||
expect(commandExecutor.execute).toHaveBeenCalledWith(payload, {
|
||||
userId: 'admin-1',
|
||||
tenantId: 'admin-1',
|
||||
});
|
||||
expect(client.emit).toHaveBeenCalledWith(
|
||||
'command:result',
|
||||
expect.objectContaining({ success: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it('issues a durable approval only for the authenticated actor', async (): Promise<void> => {
|
||||
const commandExecutor = {
|
||||
execute: vi.fn(),
|
||||
createApproval: vi.fn().mockResolvedValue({
|
||||
approvalId: 'approval-1',
|
||||
expiresAt: '2026-07-12T00:05:00.000Z',
|
||||
}),
|
||||
};
|
||||
const gateway = buildGateway(commandExecutor);
|
||||
const client = { data: { user: { id: 'admin-1' } }, emit: vi.fn() };
|
||||
|
||||
await gateway.handleCommandApproval(client as never, {
|
||||
command: 'gc',
|
||||
conversationId: 'conversation-1',
|
||||
});
|
||||
|
||||
expect(commandExecutor.createApproval).toHaveBeenCalledWith(
|
||||
{ command: 'gc', conversationId: 'conversation-1' },
|
||||
{ userId: 'admin-1', tenantId: 'admin-1' },
|
||||
);
|
||||
expect(client.emit).toHaveBeenCalledWith('command:approval', {
|
||||
command: 'gc',
|
||||
conversationId: 'conversation-1',
|
||||
success: true,
|
||||
approvalId: 'approval-1',
|
||||
expiresAt: '2026-07-12T00:05:00.000Z',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,7 @@ import type { Auth } from '@mosaicstack/auth';
|
||||
import type { Brain } from '@mosaicstack/brain';
|
||||
import type {
|
||||
SetThinkingPayload,
|
||||
SlashCommandApprovalResultPayload,
|
||||
SlashCommandPayload,
|
||||
SystemReloadPayload,
|
||||
RoutingDecisionInfo,
|
||||
@@ -429,6 +430,30 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
||||
client.emit('command:result', result);
|
||||
}
|
||||
|
||||
@SubscribeMessage('command:approve')
|
||||
async handleCommandApproval(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@MessageBody() payload: SlashCommandPayload,
|
||||
): Promise<void> {
|
||||
const scope = this.getClientScope(client);
|
||||
const approval = scope ? await this.commandExecutor.createApproval(payload, scope) : null;
|
||||
const result: SlashCommandApprovalResultPayload = approval
|
||||
? {
|
||||
command: payload.command,
|
||||
conversationId: payload.conversationId,
|
||||
success: true,
|
||||
approvalId: approval.approvalId,
|
||||
expiresAt: approval.expiresAt,
|
||||
}
|
||||
: {
|
||||
command: payload.command,
|
||||
conversationId: payload.conversationId,
|
||||
success: false,
|
||||
message: 'Not authorized to approve this command.',
|
||||
};
|
||||
client.emit('command:approval', result);
|
||||
}
|
||||
|
||||
broadcastReload(payload: SystemReloadPayload): void {
|
||||
this.server.emit('system:reload', payload);
|
||||
this.logger.log('Broadcasted system:reload to all connected clients');
|
||||
|
||||
Reference in New Issue
Block a user