mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
fix(editor): Optionally share credentials used by the workflow when moving the workflow between projects (#12524)
Co-authored-by: Danny Martini <danny@n8n.io>
This commit is contained in:
@@ -8,18 +8,28 @@ import config from '@/config';
|
||||
import type { Project } from '@/databases/entities/project';
|
||||
import type { User } from '@/databases/entities/user';
|
||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
|
||||
import { WorkflowHistoryRepository } from '@/databases/repositories/workflow-history.repository';
|
||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||
import { License } from '@/license';
|
||||
import { UserManagementMailer } from '@/user-management/email';
|
||||
import type { WorkflowWithSharingsMetaDataAndCredentials } from '@/workflows/workflows.types';
|
||||
import { mockInstance } from '@test/mocking';
|
||||
|
||||
import { mockInstance } from '../../shared/mocking';
|
||||
import { affixRoleToSaveCredential, shareCredentialWithUsers } from '../shared/db/credentials';
|
||||
import { createTeamProject, linkUserToProject } from '../shared/db/projects';
|
||||
import {
|
||||
affixRoleToSaveCredential,
|
||||
getCredentialSharings,
|
||||
shareCredentialWithProjects,
|
||||
shareCredentialWithUsers,
|
||||
} from '../shared/db/credentials';
|
||||
import { createTeamProject, getPersonalProject, linkUserToProject } from '../shared/db/projects';
|
||||
import { createTag } from '../shared/db/tags';
|
||||
import { createAdmin, createOwner, createUser, createUserShell } from '../shared/db/users';
|
||||
import { createWorkflow, getWorkflowSharing, shareWorkflowWithUsers } from '../shared/db/workflows';
|
||||
import {
|
||||
createWorkflow,
|
||||
getWorkflowSharing,
|
||||
shareWorkflowWithProjects,
|
||||
shareWorkflowWithUsers,
|
||||
} from '../shared/db/workflows';
|
||||
import { randomCredentialPayload } from '../shared/random';
|
||||
import * as testDb from '../shared/test-db';
|
||||
import type { SaveCredentialFunction } from '../shared/types';
|
||||
@@ -44,7 +54,6 @@ let workflowRepository: WorkflowRepository;
|
||||
|
||||
const activeWorkflowManager = mockInstance(ActiveWorkflowManager);
|
||||
|
||||
const sharingSpy = jest.spyOn(License.prototype, 'isSharingEnabled').mockReturnValue(true);
|
||||
const testServer = utils.setupTestServer({
|
||||
endpointGroups: ['workflows'],
|
||||
enabledFeatures: ['feat:sharing', 'feat:advancedPermissions'],
|
||||
@@ -95,15 +104,15 @@ describe('router should switch based on flag', () => {
|
||||
});
|
||||
|
||||
test('when sharing is disabled', async () => {
|
||||
sharingSpy.mockReturnValueOnce(false);
|
||||
|
||||
license.disable('feat:sharing');
|
||||
await authOwnerAgent
|
||||
.put(`/workflows/${savedWorkflowId}/share`)
|
||||
.send({ shareWithIds: [memberPersonalProject.id] })
|
||||
.expect(404);
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
test('when sharing is enabled', async () => {
|
||||
license.enable('feat:sharing');
|
||||
await authOwnerAgent
|
||||
.put(`/workflows/${savedWorkflowId}/share`)
|
||||
.send({ shareWithIds: [memberPersonalProject.id] })
|
||||
@@ -290,6 +299,52 @@ describe('PUT /workflows/:workflowId/share', () => {
|
||||
|
||||
config.set('userManagement.emails.mode', 'smtp');
|
||||
});
|
||||
|
||||
test('should ignore sharing with owner project', async () => {
|
||||
// ARRANGE
|
||||
const project = ownerPersonalProject;
|
||||
const workflow = await createWorkflow({ name: 'My workflow' }, project);
|
||||
|
||||
await authOwnerAgent
|
||||
.put(`/workflows/${workflow.id}/share`)
|
||||
.send({ shareWithIds: [project.id] })
|
||||
.expect(200);
|
||||
|
||||
const sharedWorkflows = await Container.get(SharedWorkflowRepository).findBy({
|
||||
workflowId: workflow.id,
|
||||
});
|
||||
|
||||
expect(sharedWorkflows).toHaveLength(1);
|
||||
expect(sharedWorkflows).toEqual([
|
||||
expect.objectContaining({ projectId: project.id, role: 'workflow:owner' }),
|
||||
]);
|
||||
});
|
||||
|
||||
test('should ignore sharing with project that already has it shared', async () => {
|
||||
// ARRANGE
|
||||
const project = ownerPersonalProject;
|
||||
const workflow = await createWorkflow({ name: 'My workflow' }, project);
|
||||
|
||||
const project2 = memberPersonalProject;
|
||||
await shareWorkflowWithProjects(workflow, [{ project: project2 }]);
|
||||
|
||||
await authOwnerAgent
|
||||
.put(`/workflows/${workflow.id}/share`)
|
||||
.send({ shareWithIds: [project2.id] })
|
||||
.expect(200);
|
||||
|
||||
const sharedWorkflows = await Container.get(SharedWorkflowRepository).findBy({
|
||||
workflowId: workflow.id,
|
||||
});
|
||||
|
||||
expect(sharedWorkflows).toHaveLength(2);
|
||||
expect(sharedWorkflows).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ projectId: project.id, role: 'workflow:owner' }),
|
||||
expect.objectContaining({ projectId: project2.id, role: 'workflow:editor' }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /workflows/new', () => {
|
||||
@@ -297,7 +352,7 @@ describe('GET /workflows/new', () => {
|
||||
test(`should return an auto-incremented name, even when sharing is ${
|
||||
sharingEnabled ? 'enabled' : 'disabled'
|
||||
}`, async () => {
|
||||
sharingSpy.mockReturnValueOnce(sharingEnabled);
|
||||
license.enable('feat:sharing');
|
||||
|
||||
await createWorkflow({ name: 'My workflow' }, owner);
|
||||
await createWorkflow({ name: 'My workflow 7' }, owner);
|
||||
@@ -1602,6 +1657,338 @@ describe('PUT /:workflowId/transfer', () => {
|
||||
expect(workflowFromDB).toMatchObject({ active: false });
|
||||
});
|
||||
|
||||
test('owner transfers workflow from project they are not part of, e.g. test global cred sharing scope', async () => {
|
||||
// ARRANGE
|
||||
const sourceProject = await createTeamProject('source project', admin);
|
||||
const destinationProject = await createTeamProject('destination project', member);
|
||||
const workflow = await createWorkflow({}, sourceProject);
|
||||
const credential = await saveCredential(randomCredentialPayload(), { project: sourceProject });
|
||||
|
||||
// ACT
|
||||
await testServer
|
||||
.authAgentFor(owner)
|
||||
.put(`/workflows/${workflow.id}/transfer`)
|
||||
.send({
|
||||
destinationProjectId: destinationProject.id,
|
||||
shareCredentials: [credential.id],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// ASSERT
|
||||
const allWorkflowSharings = await getWorkflowSharing(workflow);
|
||||
expect(allWorkflowSharings).toHaveLength(1);
|
||||
expect(allWorkflowSharings[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const allCredentialSharings = await getCredentialSharings(credential);
|
||||
expect(allCredentialSharings).toHaveLength(2);
|
||||
expect(allCredentialSharings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
projectId: sourceProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
projectId: destinationProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('admin transfers workflow from project they are not part of, e.g. test global cred sharing scope', async () => {
|
||||
// ARRANGE
|
||||
const sourceProject = await createTeamProject('source project', owner);
|
||||
const destinationProject = await createTeamProject('destination project', owner);
|
||||
const workflow = await createWorkflow({}, sourceProject);
|
||||
const credential = await saveCredential(randomCredentialPayload(), { project: sourceProject });
|
||||
|
||||
// ACT
|
||||
await testServer
|
||||
.authAgentFor(admin)
|
||||
.put(`/workflows/${workflow.id}/transfer`)
|
||||
.send({
|
||||
destinationProjectId: destinationProject.id,
|
||||
shareCredentials: [credential.id],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// ASSERT
|
||||
const allWorkflowSharings = await getWorkflowSharing(workflow);
|
||||
expect(allWorkflowSharings).toHaveLength(1);
|
||||
expect(allWorkflowSharings[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const allCredentialSharings = await getCredentialSharings(credential);
|
||||
expect(allCredentialSharings).toHaveLength(2);
|
||||
expect(allCredentialSharings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
projectId: sourceProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
projectId: destinationProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('member transfers workflow from personal project to team project and wf contains a credential that they can use but not share', async () => {
|
||||
// ARRANGE
|
||||
const sourceProject = memberPersonalProject;
|
||||
const destinationProject = await createTeamProject('destination project', member);
|
||||
const workflow = await createWorkflow({}, sourceProject);
|
||||
const credential = await saveCredential(randomCredentialPayload(), { user: owner });
|
||||
|
||||
await shareCredentialWithUsers(credential, [member]);
|
||||
|
||||
// ACT
|
||||
await testServer
|
||||
.authAgentFor(member)
|
||||
.put(`/workflows/${workflow.id}/transfer`)
|
||||
.send({
|
||||
destinationProjectId: destinationProject.id,
|
||||
shareCredentials: [credential.id],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// ASSERT
|
||||
const allWorkflowSharings = await getWorkflowSharing(workflow);
|
||||
expect(allWorkflowSharings).toHaveLength(1);
|
||||
expect(allWorkflowSharings[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const allCredentialSharings = await getCredentialSharings(credential);
|
||||
expect(allCredentialSharings).toHaveLength(2);
|
||||
expect(allCredentialSharings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
projectId: ownerPersonalProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
projectId: sourceProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('member transfers workflow from their personal project to another team project in which they have editor role', async () => {
|
||||
// ARRANGE
|
||||
const sourceProject = memberPersonalProject;
|
||||
const destinationProject = await createTeamProject('destination project');
|
||||
const workflow = await createWorkflow({}, sourceProject);
|
||||
const credential = await saveCredential(randomCredentialPayload(), { project: sourceProject });
|
||||
|
||||
await linkUserToProject(member, destinationProject, 'project:editor');
|
||||
|
||||
// ACT
|
||||
await testServer
|
||||
.authAgentFor(member)
|
||||
.put(`/workflows/${workflow.id}/transfer`)
|
||||
.send({
|
||||
destinationProjectId: destinationProject.id,
|
||||
shareCredentials: [credential.id],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// ASSERT
|
||||
const allWorkflowSharings = await getWorkflowSharing(workflow);
|
||||
expect(allWorkflowSharings).toHaveLength(1);
|
||||
expect(allWorkflowSharings[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const allCredentialSharings = await getCredentialSharings(credential);
|
||||
expect(allCredentialSharings).toHaveLength(2);
|
||||
expect(allCredentialSharings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
projectId: sourceProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
projectId: destinationProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('member transfers workflow from a team project as project admin to another team project in which they have editor role', async () => {
|
||||
// ARRANGE
|
||||
const sourceProject = await createTeamProject('source project', member);
|
||||
const destinationProject = await createTeamProject('destination project');
|
||||
const workflow = await createWorkflow({}, sourceProject);
|
||||
const credential = await saveCredential(randomCredentialPayload(), { project: sourceProject });
|
||||
|
||||
await linkUserToProject(member, destinationProject, 'project:editor');
|
||||
|
||||
// ACT
|
||||
await testServer
|
||||
.authAgentFor(member)
|
||||
.put(`/workflows/${workflow.id}/transfer`)
|
||||
.send({
|
||||
destinationProjectId: destinationProject.id,
|
||||
shareCredentials: [credential.id],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// ASSERT
|
||||
const allWorkflowSharings = await getWorkflowSharing(workflow);
|
||||
expect(allWorkflowSharings).toHaveLength(1);
|
||||
expect(allWorkflowSharings[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const allCredentialSharings = await getCredentialSharings(credential);
|
||||
expect(allCredentialSharings).toHaveLength(2);
|
||||
expect(allCredentialSharings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
projectId: sourceProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
projectId: destinationProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('member transfers workflow from a team project as project admin to another team project in which they have editor role but cannot share the credential that is only shared into the source project', async () => {
|
||||
// ARRANGE
|
||||
const sourceProject = await createTeamProject('source project', member);
|
||||
const destinationProject = await createTeamProject('destination project');
|
||||
const ownerProject = await getPersonalProject(owner);
|
||||
const workflow = await createWorkflow({}, sourceProject);
|
||||
const credential = await saveCredential(randomCredentialPayload(), { user: owner });
|
||||
|
||||
await linkUserToProject(member, destinationProject, 'project:editor');
|
||||
await shareCredentialWithProjects(credential, [sourceProject]);
|
||||
|
||||
// ACT
|
||||
await testServer
|
||||
.authAgentFor(member)
|
||||
.put(`/workflows/${workflow.id}/transfer`)
|
||||
.send({
|
||||
destinationProjectId: destinationProject.id,
|
||||
shareCredentials: [credential.id],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// ASSERT
|
||||
const allWorkflowSharings = await getWorkflowSharing(workflow);
|
||||
expect(allWorkflowSharings).toHaveLength(1);
|
||||
expect(allWorkflowSharings[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const allCredentialSharings = await getCredentialSharings(credential);
|
||||
expect(allCredentialSharings).toHaveLength(2);
|
||||
expect(allCredentialSharings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
projectId: ownerProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
projectId: sourceProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('member transfers workflow from a team project as project admin to another team project in which they have editor role but cannot share all the credentials', async () => {
|
||||
// ARRANGE
|
||||
const sourceProject = await createTeamProject('source project', member);
|
||||
const workflow = await createWorkflow({}, sourceProject);
|
||||
const credential = await saveCredential(randomCredentialPayload(), { project: sourceProject });
|
||||
|
||||
const ownersCredential = await saveCredential(randomCredentialPayload(), { user: owner });
|
||||
|
||||
const destinationProject = await createTeamProject('destination project');
|
||||
await linkUserToProject(member, destinationProject, 'project:editor');
|
||||
|
||||
// ACT
|
||||
await testServer
|
||||
.authAgentFor(member)
|
||||
.put(`/workflows/${workflow.id}/transfer`)
|
||||
.send({
|
||||
destinationProjectId: destinationProject.id,
|
||||
shareCredentials: [credential.id, ownersCredential.id],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// ASSERT
|
||||
const allWorkflowSharings = await getWorkflowSharing(workflow);
|
||||
expect(allWorkflowSharings).toHaveLength(1);
|
||||
expect(allWorkflowSharings[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const allCredentialSharings = await getCredentialSharings(credential);
|
||||
expect(allCredentialSharings).toHaveLength(2);
|
||||
expect(allCredentialSharings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
projectId: sourceProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
projectId: destinationProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
const ownerCredentialSharings = await getCredentialSharings(ownersCredential);
|
||||
expect(ownerCredentialSharings).toHaveLength(1);
|
||||
expect(ownerCredentialSharings).toEqual([
|
||||
expect.objectContaining({
|
||||
projectId: ownerPersonalProject.id,
|
||||
credentialsId: ownersCredential.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test('returns a 500 if the workflow cannot be activated due to an unknown error', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
|
||||
Reference in New Issue
Block a user