mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 13:39:44 +02:00
fix(API): Apply projectId filter to executions for member API keys (#40887)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4fcb628fca
commit
3075b60576
|
|
@ -62,11 +62,12 @@ describe('WorkflowSharingService', () => {
|
|||
scopes: ['workflow:read'],
|
||||
});
|
||||
|
||||
expect(sharedWorkflowRepository.findWorkflowIdsAccessibleToUser).toHaveBeenCalledWith(
|
||||
member.id,
|
||||
['workflow:owner'],
|
||||
['project:viewer'],
|
||||
);
|
||||
expect(sharedWorkflowRepository.findWorkflowIdsAccessibleToUser).toHaveBeenCalledWith({
|
||||
userId: member.id,
|
||||
workflowRoleSlugs: ['workflow:owner'],
|
||||
projectRoleSlugs: ['project:viewer'],
|
||||
projectId: undefined,
|
||||
});
|
||||
expect(result).toEqual(['workflow-1']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -51,11 +51,12 @@ export class WorkflowSharingService {
|
|||
? await this.roleService.rolesWithScope('workflow', options.scopes)
|
||||
: options.workflowRoles;
|
||||
|
||||
return await this.sharedWorkflowRepository.findWorkflowIdsAccessibleToUser(
|
||||
user.id,
|
||||
workflowRoles,
|
||||
projectRoles,
|
||||
);
|
||||
return await this.sharedWorkflowRepository.findWorkflowIdsAccessibleToUser({
|
||||
userId: user.id,
|
||||
workflowRoleSlugs: workflowRoles,
|
||||
projectRoleSlugs: projectRoles,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -66,11 +66,11 @@ describe('SharedWorkflowRepository', () => {
|
|||
it('finds workflow IDs accessible through project and workflow roles', async () => {
|
||||
entityManager.find.mockResolvedValue(rows);
|
||||
|
||||
const result = await sharedWorkflowRepository.findWorkflowIdsAccessibleToUser(
|
||||
'user-1',
|
||||
['workflow:owner'],
|
||||
['project:viewer'],
|
||||
);
|
||||
const result = await sharedWorkflowRepository.findWorkflowIdsAccessibleToUser({
|
||||
userId: 'user-1',
|
||||
workflowRoleSlugs: ['workflow:owner'],
|
||||
projectRoleSlugs: ['project:viewer'],
|
||||
});
|
||||
|
||||
expect(entityManager.find).toHaveBeenCalledWith(SharedWorkflow, {
|
||||
select: ['workflowId'],
|
||||
|
|
@ -87,6 +87,32 @@ describe('SharedWorkflowRepository', () => {
|
|||
expect(result).toEqual(['workflow-1', 'workflow-2']);
|
||||
});
|
||||
|
||||
it('limits workflow IDs accessible to a user to a project', async () => {
|
||||
entityManager.find.mockResolvedValue(rows);
|
||||
|
||||
const result = await sharedWorkflowRepository.findWorkflowIdsAccessibleToUser({
|
||||
userId: 'user-1',
|
||||
workflowRoleSlugs: ['workflow:owner'],
|
||||
projectRoleSlugs: ['project:viewer'],
|
||||
projectId: 'project-1',
|
||||
});
|
||||
|
||||
expect(entityManager.find).toHaveBeenCalledWith(SharedWorkflow, {
|
||||
select: ['workflowId'],
|
||||
where: {
|
||||
role: In(['workflow:owner']),
|
||||
project: {
|
||||
id: 'project-1',
|
||||
projectRelations: {
|
||||
userId: 'user-1',
|
||||
role: { slug: In(['project:viewer']) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result).toEqual(['workflow-1', 'workflow-2']);
|
||||
});
|
||||
|
||||
it('finds workflow IDs shared with a user', async () => {
|
||||
entityManager.find.mockResolvedValue(rows);
|
||||
|
||||
|
|
|
|||
|
|
@ -41,15 +41,22 @@ export class SharedWorkflowRepository extends BaseRepository<SharedWorkflow> {
|
|||
return rows.map(({ workflowId }) => workflowId);
|
||||
}
|
||||
|
||||
async findWorkflowIdsAccessibleToUser(
|
||||
userId: string,
|
||||
workflowRoleSlugs: string[],
|
||||
projectRoleSlugs: string[],
|
||||
): Promise<string[]> {
|
||||
async findWorkflowIdsAccessibleToUser({
|
||||
userId,
|
||||
workflowRoleSlugs,
|
||||
projectRoleSlugs,
|
||||
projectId,
|
||||
}: {
|
||||
userId: string;
|
||||
workflowRoleSlugs: string[];
|
||||
projectRoleSlugs: string[];
|
||||
projectId?: string;
|
||||
}): Promise<string[]> {
|
||||
const rows = await this.find({
|
||||
where: {
|
||||
role: In(workflowRoleSlugs),
|
||||
project: {
|
||||
id: projectId ?? undefined,
|
||||
projectRelations: {
|
||||
userId,
|
||||
role: { slug: In(projectRoleSlugs) },
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
shareWorkflowWithUsers,
|
||||
testDb,
|
||||
} from '@n8n/backend-test-utils';
|
||||
import type { ExecutionEntity, IExecutionResponse, User } from '@n8n/db';
|
||||
import { type ExecutionEntity, type IExecutionResponse, type User } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
import { type ExecutionStatus } from 'n8n-workflow';
|
||||
|
|
@ -998,41 +998,61 @@ describe('GET /executions', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('should return executions filtered by project ID', async () => {
|
||||
/**
|
||||
* Arrange
|
||||
*/
|
||||
const [firstProject, secondProject] = await Promise.all([
|
||||
createTeamProject(),
|
||||
createTeamProject(),
|
||||
]);
|
||||
const [firstWorkflow, secondWorkflow] = await Promise.all([
|
||||
createWorkflow({}, firstProject),
|
||||
createWorkflow({}, secondProject),
|
||||
]);
|
||||
const [firstExecution, secondExecution, _] = await Promise.all([
|
||||
createExecution({}, firstWorkflow),
|
||||
createExecution({}, firstWorkflow),
|
||||
createExecution({}, secondWorkflow),
|
||||
]);
|
||||
test.each<{
|
||||
data: () => User;
|
||||
type: 'owner' | 'member';
|
||||
}>([
|
||||
{ type: 'owner', data: () => owner },
|
||||
{ type: 'member', data: () => user1 },
|
||||
])(
|
||||
'should return executions filtered by project ID for $type',
|
||||
async ({ data: getUser, type }) => {
|
||||
testServer.license.enable('feat:sharing');
|
||||
const user = getUser();
|
||||
const agent = testServer.publicApiAgentFor(user);
|
||||
|
||||
/**
|
||||
* Act
|
||||
*/
|
||||
const response = await authOwnerAgent.get('/executions').query({
|
||||
projectId: firstProject.id,
|
||||
});
|
||||
/**
|
||||
* Arrange
|
||||
*/
|
||||
const [firstProject, secondProject] = await Promise.all([
|
||||
createTeamProject('first project', owner),
|
||||
createTeamProject('second project', owner),
|
||||
]);
|
||||
if (type === 'member') {
|
||||
await Promise.all([
|
||||
linkUserToProject(user, firstProject, 'project:editor'),
|
||||
linkUserToProject(user, secondProject, 'project:editor'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert
|
||||
*/
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.data.length).toBe(2);
|
||||
expect(response.body.nextCursor).toBeNull();
|
||||
expect(response.body.data.map((execution: ExecutionEntity) => execution.id)).toEqual(
|
||||
expect.arrayContaining([firstExecution.id, secondExecution.id]),
|
||||
);
|
||||
});
|
||||
const [firstWorkflow, secondWorkflow] = await Promise.all([
|
||||
createWorkflow({}, firstProject),
|
||||
createWorkflow({}, secondProject),
|
||||
]);
|
||||
const [firstExecution, secondExecution, _] = await Promise.all([
|
||||
createExecution({}, firstWorkflow),
|
||||
createExecution({}, firstWorkflow),
|
||||
createExecution({}, secondWorkflow),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Act
|
||||
*/
|
||||
const response = await agent.get('/executions').query({
|
||||
projectId: firstProject.id,
|
||||
});
|
||||
|
||||
/**
|
||||
* Assert
|
||||
*/
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.data.length).toBe(2);
|
||||
expect(response.body.nextCursor).toBeNull();
|
||||
expect(response.body.data.map((execution: ExecutionEntity) => execution.id)).toEqual(
|
||||
expect.arrayContaining([firstExecution.id, secondExecution.id]),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
describe('with startedAfter and startedBefore filters', () => {
|
||||
test('should retrieve executions started after a given time', async () => {
|
||||
|
|
|
|||
|
|
@ -115,6 +115,33 @@ describe('WorkflowSharingService', () => {
|
|||
expect(sharedWorkflowIds).toContain(workflow1.id);
|
||||
expect(sharedWorkflowIds).not.toContain(workflow2.id);
|
||||
});
|
||||
|
||||
it('should only show workflows of the given project to users', async () => {
|
||||
const projectA = await projectService.createTeamProject(member, { name: 'Project A' });
|
||||
const projectB = await projectService.createTeamProject(member, { name: 'Project B' });
|
||||
|
||||
await Promise.all([
|
||||
projectService.addUser(projectA.id, {
|
||||
userId: anotherMember.id,
|
||||
role: 'project:admin',
|
||||
}),
|
||||
projectService.addUser(projectB.id, {
|
||||
userId: anotherMember.id,
|
||||
role: 'project:admin',
|
||||
}),
|
||||
]);
|
||||
|
||||
const workflowA = await createWorkflow(undefined, projectA);
|
||||
const workflowB = await createWorkflow(undefined, projectB);
|
||||
|
||||
const sharedWorkflowIds = await workflowSharingService.getSharedWorkflowIds(anotherMember, {
|
||||
scopes: ['workflow:read'],
|
||||
projectId: projectA.id,
|
||||
});
|
||||
|
||||
expect(sharedWorkflowIds).toEqual([workflowA.id]);
|
||||
expect(sharedWorkflowIds).not.toContain(workflowB.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserIdsWithAccessToWorkflow', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user