diff --git a/packages/cli/src/databases/repositories/projectRelation.repository.ts b/packages/cli/src/databases/repositories/projectRelation.repository.ts index 00fc4de34a..1f875d011f 100644 --- a/packages/cli/src/databases/repositories/projectRelation.repository.ts +++ b/packages/cli/src/databases/repositories/projectRelation.repository.ts @@ -61,4 +61,12 @@ export class ProjectRelationRepository extends Repository { return [...new Set(rows.map((r) => r.userId))]; } + + async findAllByUser(userId: string) { + return await this.find({ + where: { + userId, + }, + }); + } } diff --git a/packages/cli/src/databases/repositories/sharedWorkflow.repository.ts b/packages/cli/src/databases/repositories/sharedWorkflow.repository.ts index 4dc54935cb..d8e224fee2 100644 --- a/packages/cli/src/databases/repositories/sharedWorkflow.repository.ts +++ b/packages/cli/src/databases/repositories/sharedWorkflow.repository.ts @@ -200,4 +200,13 @@ export class SharedWorkflowRepository extends Repository { }) )?.project; } + + async getRelationsByWorkflowIdsAndProjectIds(workflowIds: string[], projectIds: string[]) { + return await this.find({ + where: { + workflowId: In(workflowIds), + projectId: In(projectIds), + }, + }); + } } diff --git a/packages/cli/src/executions/__tests__/execution.service.test.ts b/packages/cli/src/executions/__tests__/execution.service.test.ts index 567e0c9758..04c266a4d0 100644 --- a/packages/cli/src/executions/__tests__/execution.service.test.ts +++ b/packages/cli/src/executions/__tests__/execution.service.test.ts @@ -31,6 +31,7 @@ describe('ExecutionService', () => { mock(), concurrencyControl, mock(), + mock(), ); beforeEach(() => { diff --git a/packages/cli/src/executions/__tests__/executions.controller.test.ts b/packages/cli/src/executions/__tests__/executions.controller.test.ts index 2a4c733c5a..decb88d598 100644 --- a/packages/cli/src/executions/__tests__/executions.controller.test.ts +++ b/packages/cli/src/executions/__tests__/executions.controller.test.ts @@ -74,6 +74,8 @@ describe('ExecutionsController', () => { }, ]; + executionService.findRangeWithCount.mockResolvedValue(NO_EXECUTIONS); + describe('if either status or range provided', () => { test.each(QUERIES_WITH_EITHER_STATUS_OR_RANGE)( 'should fetch executions per query', diff --git a/packages/cli/src/executions/execution.service.ts b/packages/cli/src/executions/execution.service.ts index bb8650e99f..0823bde36f 100644 --- a/packages/cli/src/executions/execution.service.ts +++ b/packages/cli/src/executions/execution.service.ts @@ -40,6 +40,8 @@ import { QueuedExecutionRetryError } from '@/errors/queued-execution-retry.error import { ConcurrencyControlService } from '@/concurrency/concurrency-control.service'; import { AbortedExecutionRetryError } from '@/errors/aborted-execution-retry.error'; import { License } from '@/License'; +import type { User } from '@/databases/entities/User'; +import { WorkflowSharingService } from '@/workflows/workflowSharing.service'; export const schemaGetExecutionsQueryFilter = { $id: '/IGetExecutionsQueryFilter', @@ -92,6 +94,7 @@ export class ExecutionService { private readonly workflowRunner: WorkflowRunner, private readonly concurrencyControl: ConcurrencyControlService, private readonly license: License, + private readonly workflowSharingService: WorkflowSharingService, ) {} async findOne( @@ -478,4 +481,16 @@ export class ExecutionService { return await this.executionRepository.stopDuringRun(execution); } + + async addScopes(user: User, summaries: ExecutionSummaries.ExecutionSummaryWithScopes[]) { + const workflowIds = [...new Set(summaries.map((s) => s.workflowId))]; + + const scopes = Object.fromEntries( + await this.workflowSharingService.getSharedWorkflowScopes(workflowIds, user), + ); + + for (const s of summaries) { + s.scopes = scopes[s.workflowId] ?? []; + } + } } diff --git a/packages/cli/src/executions/execution.types.ts b/packages/cli/src/executions/execution.types.ts index 7e8872bf1b..fd5024adbd 100644 --- a/packages/cli/src/executions/execution.types.ts +++ b/packages/cli/src/executions/execution.types.ts @@ -1,6 +1,12 @@ import type { ExecutionEntity } from '@/databases/entities/ExecutionEntity'; import type { AuthenticatedRequest } from '@/requests'; -import type { ExecutionStatus, IDataObject, WorkflowExecuteMode } from 'n8n-workflow'; +import type { Scope } from '@n8n/permissions'; +import type { + ExecutionStatus, + ExecutionSummary, + IDataObject, + WorkflowExecuteMode, +} from 'n8n-workflow'; export declare namespace ExecutionRequest { namespace QueryParams { @@ -83,6 +89,8 @@ export namespace ExecutionSummaries { stoppedAt?: 'DESC'; }; }; + + export type ExecutionSummaryWithScopes = ExecutionSummary & { scopes: Scope[] }; } export type QueueRecoverySettings = { diff --git a/packages/cli/src/executions/executions.controller.ts b/packages/cli/src/executions/executions.controller.ts index 64b6a54274..c68c8cb7d5 100644 --- a/packages/cli/src/executions/executions.controller.ts +++ b/packages/cli/src/executions/executions.controller.ts @@ -1,4 +1,4 @@ -import { ExecutionRequest } from './execution.types'; +import { ExecutionRequest, type ExecutionSummaries } from './execution.types'; import { ExecutionService } from './execution.service'; import { Get, Post, RestController } from '@/decorators'; import { EnterpriseExecutionsService } from './execution.service.ee'; @@ -53,10 +53,20 @@ export class ExecutionsController { const noRange = !query.range.lastId || !query.range.firstId; if (noStatus && noRange) { - return await this.executionService.findLatestCurrentAndCompleted(query); + const executions = await this.executionService.findLatestCurrentAndCompleted(query); + await this.executionService.addScopes( + req.user, + executions.results as ExecutionSummaries.ExecutionSummaryWithScopes[], + ); + return executions; } - return await this.executionService.findRangeWithCount(query); + const executions = await this.executionService.findRangeWithCount(query); + await this.executionService.addScopes( + req.user, + executions.results as ExecutionSummaries.ExecutionSummaryWithScopes[], + ); + return executions; } @Get('/:id') diff --git a/packages/cli/src/workflows/workflowSharing.service.ts b/packages/cli/src/workflows/workflowSharing.service.ts index add5bb31b8..111e50b581 100644 --- a/packages/cli/src/workflows/workflowSharing.service.ts +++ b/packages/cli/src/workflows/workflowSharing.service.ts @@ -8,12 +8,14 @@ import { RoleService } from '@/services/role.service'; import type { Scope } from '@n8n/permissions'; import type { ProjectRole } from '@/databases/entities/ProjectRelation'; import type { WorkflowSharingRole } from '@/databases/entities/SharedWorkflow'; +import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository'; @Service() export class WorkflowSharingService { constructor( private readonly sharedWorkflowRepository: SharedWorkflowRepository, private readonly roleService: RoleService, + private readonly projectRelationRepository: ProjectRelationRepository, ) {} /** @@ -64,4 +66,28 @@ export class WorkflowSharingService { return sharedWorkflows.map(({ workflowId }) => workflowId); } + + async getSharedWorkflowScopes( + workflowIds: string[], + user: User, + ): Promise> { + const projectRelations = await this.projectRelationRepository.findAllByUser(user.id); + const sharedWorkflows = + await this.sharedWorkflowRepository.getRelationsByWorkflowIdsAndProjectIds( + workflowIds, + projectRelations.map((p) => p.projectId), + ); + + return workflowIds.map((workflowId) => { + return [ + workflowId, + this.roleService.combineResourceScopes( + 'workflow', + user, + sharedWorkflows.filter((s) => s.workflowId === workflowId), + projectRelations, + ), + ]; + }); + } } diff --git a/packages/cli/test/integration/execution.service.integration.test.ts b/packages/cli/test/integration/execution.service.integration.test.ts index e30d55602a..6f29950bf4 100644 --- a/packages/cli/test/integration/execution.service.integration.test.ts +++ b/packages/cli/test/integration/execution.service.integration.test.ts @@ -30,6 +30,7 @@ describe('ExecutionService', () => { mock(), mock(), mock(), + mock(), ); }); diff --git a/packages/cli/test/integration/executions.controller.test.ts b/packages/cli/test/integration/executions.controller.test.ts index 1e6d65a4f8..94faa32351 100644 --- a/packages/cli/test/integration/executions.controller.test.ts +++ b/packages/cli/test/integration/executions.controller.test.ts @@ -48,6 +48,16 @@ describe('GET /executions', () => { const response2 = await testServer.authAgentFor(member).get('/executions').expect(200); expect(response2.body.data.count).toBe(1); }); + + test('should return a scopes array for each execution', async () => { + testServer.license.enable('feat:sharing'); + const workflow = await createWorkflow({}, owner); + await shareWorkflowWithUsers(workflow, [member]); + await createSuccessfulExecution(workflow); + + const response = await testServer.authAgentFor(member).get('/executions').expect(200); + expect(response.body.data.results[0].scopes).toContain('workflow:execute'); + }); }); describe('GET /executions/:id', () => {