mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(core): Prevent unauthorised workflow termination (#16405)
This commit is contained in:
@@ -70,12 +70,13 @@ describe('ExecutionService', () => {
|
|||||||
/**
|
/**
|
||||||
* Arrange
|
* Arrange
|
||||||
*/
|
*/
|
||||||
executionRepository.findSingleExecution.mockResolvedValue(undefined);
|
executionRepository.findWithUnflattenedData.mockResolvedValue(undefined);
|
||||||
|
const req = mock<ExecutionRequest.Stop>({ params: { id: '1234' } });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Act
|
* Act
|
||||||
*/
|
*/
|
||||||
const stop = executionService.stop('inexistent-123');
|
const stop = executionService.stop(req.params.id, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert
|
* Assert
|
||||||
@@ -88,12 +89,13 @@ describe('ExecutionService', () => {
|
|||||||
* Arrange
|
* Arrange
|
||||||
*/
|
*/
|
||||||
const execution = mock<IExecutionResponse>({ id: '123', status: 'success' });
|
const execution = mock<IExecutionResponse>({ id: '123', status: 'success' });
|
||||||
executionRepository.findSingleExecution.mockResolvedValue(execution);
|
executionRepository.findWithUnflattenedData.mockResolvedValue(execution);
|
||||||
|
const req = mock<ExecutionRequest.Stop>({ params: { id: execution.id } });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Act
|
* Act
|
||||||
*/
|
*/
|
||||||
const stop = executionService.stop(execution.id);
|
const stop = executionService.stop(req.params.id, [execution.id]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert
|
* Assert
|
||||||
@@ -107,16 +109,18 @@ describe('ExecutionService', () => {
|
|||||||
* Arrange
|
* Arrange
|
||||||
*/
|
*/
|
||||||
const execution = mock<IExecutionResponse>({ id: '123', status: 'running' });
|
const execution = mock<IExecutionResponse>({ id: '123', status: 'running' });
|
||||||
executionRepository.findSingleExecution.mockResolvedValue(execution);
|
executionRepository.findWithUnflattenedData.mockResolvedValue(execution);
|
||||||
concurrencyControl.has.mockReturnValue(false);
|
concurrencyControl.has.mockReturnValue(false);
|
||||||
activeExecutions.has.mockReturnValue(true);
|
activeExecutions.has.mockReturnValue(true);
|
||||||
waitTracker.has.mockReturnValue(false);
|
waitTracker.has.mockReturnValue(false);
|
||||||
executionRepository.stopDuringRun.mockResolvedValue(mock<IExecutionResponse>());
|
executionRepository.stopDuringRun.mockResolvedValue(mock<IExecutionResponse>());
|
||||||
|
|
||||||
|
const req = mock<ExecutionRequest.Stop>({ params: { id: execution.id } });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Act
|
* Act
|
||||||
*/
|
*/
|
||||||
await executionService.stop(execution.id);
|
await executionService.stop(req.params.id, [execution.id]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert
|
* Assert
|
||||||
@@ -132,16 +136,18 @@ describe('ExecutionService', () => {
|
|||||||
* Arrange
|
* Arrange
|
||||||
*/
|
*/
|
||||||
const execution = mock<IExecutionResponse>({ id: '123', status: 'waiting' });
|
const execution = mock<IExecutionResponse>({ id: '123', status: 'waiting' });
|
||||||
executionRepository.findSingleExecution.mockResolvedValue(execution);
|
executionRepository.findWithUnflattenedData.mockResolvedValue(execution);
|
||||||
concurrencyControl.has.mockReturnValue(false);
|
concurrencyControl.has.mockReturnValue(false);
|
||||||
activeExecutions.has.mockReturnValue(true);
|
activeExecutions.has.mockReturnValue(true);
|
||||||
waitTracker.has.mockReturnValue(true);
|
waitTracker.has.mockReturnValue(true);
|
||||||
executionRepository.stopDuringRun.mockResolvedValue(mock<IExecutionResponse>());
|
executionRepository.stopDuringRun.mockResolvedValue(mock<IExecutionResponse>());
|
||||||
|
|
||||||
|
const req = mock<ExecutionRequest.Stop>({ params: { id: execution.id } });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Act
|
* Act
|
||||||
*/
|
*/
|
||||||
await executionService.stop(execution.id);
|
await executionService.stop(req.params.id, [execution.id]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert
|
* Assert
|
||||||
@@ -157,16 +163,18 @@ describe('ExecutionService', () => {
|
|||||||
* Arrange
|
* Arrange
|
||||||
*/
|
*/
|
||||||
const execution = mock<IExecutionResponse>({ id: '123', status: 'new', mode: 'trigger' });
|
const execution = mock<IExecutionResponse>({ id: '123', status: 'new', mode: 'trigger' });
|
||||||
executionRepository.findSingleExecution.mockResolvedValue(execution);
|
executionRepository.findWithUnflattenedData.mockResolvedValue(execution);
|
||||||
concurrencyControl.has.mockReturnValue(true);
|
concurrencyControl.has.mockReturnValue(true);
|
||||||
activeExecutions.has.mockReturnValue(false);
|
activeExecutions.has.mockReturnValue(false);
|
||||||
waitTracker.has.mockReturnValue(false);
|
waitTracker.has.mockReturnValue(false);
|
||||||
executionRepository.stopBeforeRun.mockResolvedValue(mock<IExecutionResponse>());
|
executionRepository.stopBeforeRun.mockResolvedValue(mock<IExecutionResponse>());
|
||||||
|
|
||||||
|
const req = mock<ExecutionRequest.Stop>({ params: { id: execution.id } });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Act
|
* Act
|
||||||
*/
|
*/
|
||||||
await executionService.stop(execution.id);
|
await executionService.stop(req.params.id, [execution.id]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert
|
* Assert
|
||||||
@@ -193,11 +201,13 @@ describe('ExecutionService', () => {
|
|||||||
mode: 'manual',
|
mode: 'manual',
|
||||||
status: 'running',
|
status: 'running',
|
||||||
});
|
});
|
||||||
executionRepository.findSingleExecution.mockResolvedValue(execution);
|
executionRepository.findWithUnflattenedData.mockResolvedValue(execution);
|
||||||
concurrencyControl.has.mockReturnValue(false);
|
concurrencyControl.has.mockReturnValue(false);
|
||||||
activeExecutions.has.mockReturnValue(true);
|
activeExecutions.has.mockReturnValue(true);
|
||||||
waitTracker.has.mockReturnValue(false);
|
waitTracker.has.mockReturnValue(false);
|
||||||
const job = mock<Job>({ data: { executionId: '123' } });
|
|
||||||
|
const req = mock<ExecutionRequest.Stop>({ params: { id: execution.id } });
|
||||||
|
const job = mock<Job>({ data: { executionId: execution.id } });
|
||||||
scalingService.findJobsByStatus.mockResolvedValue([job]);
|
scalingService.findJobsByStatus.mockResolvedValue([job]);
|
||||||
executionRepository.stopDuringRun.mockResolvedValue(mock<IExecutionResponse>());
|
executionRepository.stopDuringRun.mockResolvedValue(mock<IExecutionResponse>());
|
||||||
// @ts-expect-error Private method
|
// @ts-expect-error Private method
|
||||||
@@ -206,7 +216,7 @@ describe('ExecutionService', () => {
|
|||||||
/**
|
/**
|
||||||
* Act
|
* Act
|
||||||
*/
|
*/
|
||||||
await executionService.stop(execution.id);
|
await executionService.stop(req.params.id, [execution.id]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert
|
* Assert
|
||||||
@@ -228,16 +238,18 @@ describe('ExecutionService', () => {
|
|||||||
*/
|
*/
|
||||||
config.set('executions.mode', 'queue');
|
config.set('executions.mode', 'queue');
|
||||||
const execution = mock<IExecutionResponse>({ id: '123', status: 'running' });
|
const execution = mock<IExecutionResponse>({ id: '123', status: 'running' });
|
||||||
executionRepository.findSingleExecution.mockResolvedValue(execution);
|
executionRepository.findWithUnflattenedData.mockResolvedValue(execution);
|
||||||
waitTracker.has.mockReturnValue(false);
|
waitTracker.has.mockReturnValue(false);
|
||||||
const job = mock<Job>({ data: { executionId: '123' } });
|
|
||||||
|
const req = mock<ExecutionRequest.Stop>({ params: { id: execution.id } });
|
||||||
|
const job = mock<Job>({ data: { executionId: execution.id } });
|
||||||
scalingService.findJobsByStatus.mockResolvedValue([job]);
|
scalingService.findJobsByStatus.mockResolvedValue([job]);
|
||||||
executionRepository.stopDuringRun.mockResolvedValue(mock<IExecutionResponse>());
|
executionRepository.stopDuringRun.mockResolvedValue(mock<IExecutionResponse>());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Act
|
* Act
|
||||||
*/
|
*/
|
||||||
await executionService.stop(execution.id);
|
await executionService.stop(req.params.id, [execution.id]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert
|
* Assert
|
||||||
@@ -255,16 +267,18 @@ describe('ExecutionService', () => {
|
|||||||
*/
|
*/
|
||||||
config.set('executions.mode', 'queue');
|
config.set('executions.mode', 'queue');
|
||||||
const execution = mock<IExecutionResponse>({ id: '123', status: 'waiting' });
|
const execution = mock<IExecutionResponse>({ id: '123', status: 'waiting' });
|
||||||
executionRepository.findSingleExecution.mockResolvedValue(execution);
|
executionRepository.findWithUnflattenedData.mockResolvedValue(execution);
|
||||||
waitTracker.has.mockReturnValue(true);
|
waitTracker.has.mockReturnValue(true);
|
||||||
const job = mock<Job>({ data: { executionId: '123' } });
|
|
||||||
|
const req = mock<ExecutionRequest.Stop>({ params: { id: execution.id } });
|
||||||
|
const job = mock<Job>({ data: { executionId: execution.id } });
|
||||||
scalingService.findJobsByStatus.mockResolvedValue([job]);
|
scalingService.findJobsByStatus.mockResolvedValue([job]);
|
||||||
executionRepository.stopDuringRun.mockResolvedValue(mock<IExecutionResponse>());
|
executionRepository.stopDuringRun.mockResolvedValue(mock<IExecutionResponse>());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Act
|
* Act
|
||||||
*/
|
*/
|
||||||
await executionService.stop(execution.id);
|
await executionService.stop(req.params.id, [execution.id]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert
|
* Assert
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ describe('ExecutionsController', () => {
|
|||||||
const executionId = '999';
|
const executionId = '999';
|
||||||
const req = mock<ExecutionRequest.Stop>({ params: { id: executionId } });
|
const req = mock<ExecutionRequest.Stop>({ params: { id: executionId } });
|
||||||
|
|
||||||
it('should 404 when execution is inaccessible for user', async () => {
|
it('should throw expected NotFoundError when all workflows are inaccessible for user', async () => {
|
||||||
workflowSharingService.getSharedWorkflowIds.mockResolvedValue([]);
|
workflowSharingService.getSharedWorkflowIds.mockResolvedValue([]);
|
||||||
|
|
||||||
const promise = executionsController.stop(req);
|
const promise = executionsController.stop(req);
|
||||||
@@ -147,12 +147,12 @@ describe('ExecutionsController', () => {
|
|||||||
expect(executionService.stop).not.toHaveBeenCalled();
|
expect(executionService.stop).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call ask for an execution to be stopped', async () => {
|
it('should call execution service with expected data when user has accessible workflows', async () => {
|
||||||
workflowSharingService.getSharedWorkflowIds.mockResolvedValue(['123']);
|
const mockAccessibleWorkflowIds = ['1234', '999'];
|
||||||
|
workflowSharingService.getSharedWorkflowIds.mockResolvedValue(mockAccessibleWorkflowIds);
|
||||||
|
|
||||||
await executionsController.stop(req);
|
await executionsController.stop(req);
|
||||||
|
expect(executionService.stop).toHaveBeenCalledWith(req.params.id, mockAccessibleWorkflowIds);
|
||||||
expect(executionService.stop).toHaveBeenCalledWith(executionId);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -420,13 +420,19 @@ export class ExecutionService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop(executionId: string): Promise<StopResult> {
|
async stop(executionId: string, sharedWorkflowIds: string[]): Promise<StopResult> {
|
||||||
const execution = await this.executionRepository.findSingleExecution(executionId, {
|
const execution = await this.executionRepository.findWithUnflattenedData(
|
||||||
includeData: true,
|
executionId,
|
||||||
unflattenData: true,
|
sharedWorkflowIds,
|
||||||
});
|
);
|
||||||
|
|
||||||
if (!execution) throw new MissingExecutionStopError(executionId);
|
if (!execution) {
|
||||||
|
this.logger.info(`Unable to stop execution "${executionId}" as it was not found`, {
|
||||||
|
executionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new MissingExecutionStopError(executionId);
|
||||||
|
}
|
||||||
|
|
||||||
this.assertStoppable(execution);
|
this.assertStoppable(execution);
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,9 @@ export class ExecutionsController {
|
|||||||
|
|
||||||
if (workflowIds.length === 0) throw new NotFoundError('Execution not found');
|
if (workflowIds.length === 0) throw new NotFoundError('Execution not found');
|
||||||
|
|
||||||
return await this.executionService.stop(req.params.id);
|
const executionId = req.params.id;
|
||||||
|
|
||||||
|
return await this.executionService.stop(executionId, workflowIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/:id/retry')
|
@Post('/:id/retry')
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import type { User } from '@n8n/db';
|
|||||||
import { ConcurrencyControlService } from '@/concurrency/concurrency-control.service';
|
import { ConcurrencyControlService } from '@/concurrency/concurrency-control.service';
|
||||||
import { WaitTracker } from '@/wait-tracker';
|
import { WaitTracker } from '@/wait-tracker';
|
||||||
|
|
||||||
import { createSuccessfulExecution, getAllExecutions } from './shared/db/executions';
|
import {
|
||||||
|
createSuccessfulExecution,
|
||||||
|
createWaitingExecution,
|
||||||
|
getAllExecutions,
|
||||||
|
} from './shared/db/executions';
|
||||||
import { createTeamProject, linkUserToProject } from './shared/db/projects';
|
import { createTeamProject, linkUserToProject } from './shared/db/projects';
|
||||||
import { createMember, createOwner } from './shared/db/users';
|
import { createMember, createOwner } from './shared/db/users';
|
||||||
import { createWorkflow, shareWorkflowWithUsers } from './shared/db/workflows';
|
import { createWorkflow, shareWorkflowWithUsers } from './shared/db/workflows';
|
||||||
@@ -27,6 +31,11 @@ const saveExecution = async ({ belongingTo }: { belongingTo: User }) => {
|
|||||||
return await createSuccessfulExecution(workflow);
|
return await createSuccessfulExecution(workflow);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveWaitingExecution = async ({ belongingTo }: { belongingTo: User }) => {
|
||||||
|
const workflow = await createWorkflow({}, belongingTo);
|
||||||
|
return await createWaitingExecution(workflow);
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await testDb.truncate(['ExecutionEntity', 'WorkflowEntity', 'SharedWorkflow']);
|
await testDb.truncate(['ExecutionEntity', 'WorkflowEntity', 'SharedWorkflow']);
|
||||||
testServer.license.reset();
|
testServer.license.reset();
|
||||||
@@ -117,3 +126,21 @@ describe('POST /executions/delete', () => {
|
|||||||
expect(executions).toHaveLength(0);
|
expect(executions).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('POST /executions/stop', () => {
|
||||||
|
test('should not stop an execution we do not have access to', async () => {
|
||||||
|
await saveExecution({ belongingTo: owner });
|
||||||
|
const incorrectExecutionId = '1234';
|
||||||
|
|
||||||
|
await testServer
|
||||||
|
.authAgentFor(owner)
|
||||||
|
.post(`/executions/${incorrectExecutionId}/stop`)
|
||||||
|
.expect(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should stop an execution we have access to', async () => {
|
||||||
|
const execution = await saveWaitingExecution({ belongingTo: owner });
|
||||||
|
|
||||||
|
await testServer.authAgentFor(owner).post(`/executions/${execution.id}/stop`).expect(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user