mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 04:47:29 -08:00
feat: Return scopes on executions (no-changelog) (#10310)
This commit is contained in:
parent
6d8323fade
commit
fa17391dbd
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ describe('ExecutionService', () => {
|
||||||
mock(),
|
mock(),
|
||||||
concurrencyControl,
|
concurrencyControl,
|
||||||
mock(),
|
mock(),
|
||||||
|
mock(),
|
||||||
);
|
);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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] ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ describe('ExecutionService', () => {
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
|
mock(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
Loading…
Reference in a new issue