feat: Return scopes on executions (no-changelog) (#10310)

This commit is contained in:
Val 2024-08-07 10:19:09 +01:00 committed by GitHub
parent 6d8323fade
commit fa17391dbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 94 additions and 4 deletions

View file

@ -61,4 +61,12 @@ export class ProjectRelationRepository extends Repository<ProjectRelation> {
return [...new Set(rows.map((r) => r.userId))]; return [...new Set(rows.map((r) => r.userId))];
} }
async findAllByUser(userId: string) {
return await this.find({
where: {
userId,
},
});
}
} }

View file

@ -200,4 +200,13 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
}) })
)?.project; )?.project;
} }
async getRelationsByWorkflowIdsAndProjectIds(workflowIds: string[], projectIds: string[]) {
return await this.find({
where: {
workflowId: In(workflowIds),
projectId: In(projectIds),
},
});
}
} }

View file

@ -31,6 +31,7 @@ describe('ExecutionService', () => {
mock(), mock(),
concurrencyControl, concurrencyControl,
mock(), mock(),
mock(),
); );
beforeEach(() => { beforeEach(() => {

View file

@ -74,6 +74,8 @@ describe('ExecutionsController', () => {
}, },
]; ];
executionService.findRangeWithCount.mockResolvedValue(NO_EXECUTIONS);
describe('if either status or range provided', () => { describe('if either status or range provided', () => {
test.each(QUERIES_WITH_EITHER_STATUS_OR_RANGE)( test.each(QUERIES_WITH_EITHER_STATUS_OR_RANGE)(
'should fetch executions per query', 'should fetch executions per query',

View file

@ -40,6 +40,8 @@ import { QueuedExecutionRetryError } from '@/errors/queued-execution-retry.error
import { ConcurrencyControlService } from '@/concurrency/concurrency-control.service'; import { ConcurrencyControlService } from '@/concurrency/concurrency-control.service';
import { AbortedExecutionRetryError } from '@/errors/aborted-execution-retry.error'; import { AbortedExecutionRetryError } from '@/errors/aborted-execution-retry.error';
import { License } from '@/License'; import { License } from '@/License';
import type { User } from '@/databases/entities/User';
import { WorkflowSharingService } from '@/workflows/workflowSharing.service';
export const schemaGetExecutionsQueryFilter = { export const schemaGetExecutionsQueryFilter = {
$id: '/IGetExecutionsQueryFilter', $id: '/IGetExecutionsQueryFilter',
@ -92,6 +94,7 @@ export class ExecutionService {
private readonly workflowRunner: WorkflowRunner, private readonly workflowRunner: WorkflowRunner,
private readonly concurrencyControl: ConcurrencyControlService, private readonly concurrencyControl: ConcurrencyControlService,
private readonly license: License, private readonly license: License,
private readonly workflowSharingService: WorkflowSharingService,
) {} ) {}
async findOne( async findOne(
@ -478,4 +481,16 @@ export class ExecutionService {
return await this.executionRepository.stopDuringRun(execution); 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] ?? [];
}
}
} }

View file

@ -1,6 +1,12 @@
import type { ExecutionEntity } from '@/databases/entities/ExecutionEntity'; import type { ExecutionEntity } from '@/databases/entities/ExecutionEntity';
import type { AuthenticatedRequest } from '@/requests'; 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 { export declare namespace ExecutionRequest {
namespace QueryParams { namespace QueryParams {
@ -83,6 +89,8 @@ export namespace ExecutionSummaries {
stoppedAt?: 'DESC'; stoppedAt?: 'DESC';
}; };
}; };
export type ExecutionSummaryWithScopes = ExecutionSummary & { scopes: Scope[] };
} }
export type QueueRecoverySettings = { export type QueueRecoverySettings = {

View file

@ -1,4 +1,4 @@
import { ExecutionRequest } from './execution.types'; import { ExecutionRequest, type ExecutionSummaries } from './execution.types';
import { ExecutionService } from './execution.service'; import { ExecutionService } from './execution.service';
import { Get, Post, RestController } from '@/decorators'; import { Get, Post, RestController } from '@/decorators';
import { EnterpriseExecutionsService } from './execution.service.ee'; import { EnterpriseExecutionsService } from './execution.service.ee';
@ -53,10 +53,20 @@ export class ExecutionsController {
const noRange = !query.range.lastId || !query.range.firstId; const noRange = !query.range.lastId || !query.range.firstId;
if (noStatus && noRange) { 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') @Get('/:id')

View file

@ -8,12 +8,14 @@ import { RoleService } from '@/services/role.service';
import type { Scope } from '@n8n/permissions'; import type { Scope } from '@n8n/permissions';
import type { ProjectRole } from '@/databases/entities/ProjectRelation'; import type { ProjectRole } from '@/databases/entities/ProjectRelation';
import type { WorkflowSharingRole } from '@/databases/entities/SharedWorkflow'; import type { WorkflowSharingRole } from '@/databases/entities/SharedWorkflow';
import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository';
@Service() @Service()
export class WorkflowSharingService { export class WorkflowSharingService {
constructor( constructor(
private readonly sharedWorkflowRepository: SharedWorkflowRepository, private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly roleService: RoleService, private readonly roleService: RoleService,
private readonly projectRelationRepository: ProjectRelationRepository,
) {} ) {}
/** /**
@ -64,4 +66,28 @@ export class WorkflowSharingService {
return sharedWorkflows.map(({ workflowId }) => workflowId); return sharedWorkflows.map(({ workflowId }) => workflowId);
} }
async getSharedWorkflowScopes(
workflowIds: string[],
user: User,
): Promise<Array<[string, Scope[]]>> {
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,
),
];
});
}
} }

View file

@ -30,6 +30,7 @@ describe('ExecutionService', () => {
mock(), mock(),
mock(), mock(),
mock(), mock(),
mock(),
); );
}); });

View file

@ -48,6 +48,16 @@ describe('GET /executions', () => {
const response2 = await testServer.authAgentFor(member).get('/executions').expect(200); const response2 = await testServer.authAgentFor(member).get('/executions').expect(200);
expect(response2.body.data.count).toBe(1); 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', () => { describe('GET /executions/:id', () => {