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:
Csaba Tuncsik
2025-02-21 11:05:37 +01:00
committed by GitHub
parent 29ae2396c9
commit 7bd83d7d33
22 changed files with 1078 additions and 136 deletions

View File

@@ -330,7 +330,12 @@ export class CredentialsController {
credentialsId: credentialId,
projectId: In(toUnshare),
});
await this.enterpriseCredentialsService.shareWithProjects(req.user, credential, toShare, trx);
await this.enterpriseCredentialsService.shareWithProjects(
req.user,
credential.id,
toShare,
trx,
);
if (deleteResult.affected) {
amountRemoved = deleteResult.affected;

View File

@@ -28,13 +28,13 @@ export class EnterpriseCredentialsService {
async shareWithProjects(
user: User,
credential: CredentialsEntity,
credentialId: string,
shareWithIds: string[],
entityManager?: EntityManager,
) {
const em = entityManager ?? this.sharedCredentialsRepository.manager;
const projects = await em.find(Project, {
let projects = await em.find(Project, {
where: [
{
id: In(shareWithIds),
@@ -55,11 +55,19 @@ export class EnterpriseCredentialsService {
type: 'personal',
},
],
relations: { sharedCredentials: true },
});
// filter out all projects that already own the credential
projects = projects.filter(
(p) =>
!p.sharedCredentials.some(
(psc) => psc.credentialsId === credentialId && psc.role === 'credential:owner',
),
);
const newSharedCredentials = projects.map((project) =>
this.sharedCredentialsRepository.create({
credentialsId: credential.id,
credentialsId: credentialId,
role: 'credential:user',
projectId: project.id,
}),

View File

@@ -56,6 +56,39 @@ export class SharedCredentialsRepository extends Repository<SharedCredentials> {
return sharedCredential.credentials;
}
/** Get all credentials shared to a user */
async findAllCredentialsForUser(user: User, scopes: Scope[], trx?: EntityManager) {
trx = trx ?? this.manager;
let where: FindOptionsWhere<SharedCredentials> = {};
if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) {
const projectRoles = this.roleService.rolesWithScope('project', scopes);
const credentialRoles = this.roleService.rolesWithScope('credential', scopes);
where = {
role: In(credentialRoles),
project: {
projectRelations: {
role: In(projectRoles),
userId: user.id,
},
},
};
}
const sharedCredential = await trx.find(SharedCredentials, {
where,
// TODO: write a small relations merger and use that one here
relations: {
credentials: {
shared: { project: { projectRelations: { user: true } } },
},
},
});
return sharedCredential.map((sc) => ({ ...sc.credentials, projectId: sc.projectId }));
}
async findByCredentialIds(credentialIds: string[], role: CredentialSharingRole) {
return await this.find({
relations: { credentials: true, project: { projectRelations: { user: true } } },
@@ -97,7 +130,10 @@ export class SharedCredentialsRepository extends Repository<SharedCredentials> {
options:
| { scopes: Scope[] }
| { projectRoles: ProjectRole[]; credentialRoles: CredentialSharingRole[] },
trx?: EntityManager,
) {
trx = trx ?? this.manager;
const projectRoles =
'scopes' in options
? this.roleService.rolesWithScope('project', options.scopes)
@@ -107,7 +143,7 @@ export class SharedCredentialsRepository extends Repository<SharedCredentials> {
? this.roleService.rolesWithScope('credential', options.scopes)
: options.credentialRoles;
const sharings = await this.find({
const sharings = await trx.find(SharedCredentials, {
where: {
role: In(credentialRoles),
project: {
@@ -118,6 +154,7 @@ export class SharedCredentialsRepository extends Repository<SharedCredentials> {
},
},
});
return sharings.map((s) => s.credentialsId);
}

View File

@@ -58,10 +58,4 @@ export declare namespace WorkflowRequest {
type ManualRun = AuthenticatedRequest<{ workflowId: string }, {}, ManualRunPayload, {}>;
type Share = AuthenticatedRequest<{ workflowId: string }, {}, { shareWithIds: string[] }>;
type Transfer = AuthenticatedRequest<
{ workflowId: string },
{},
{ destinationProjectId: string }
>;
}

View File

@@ -8,11 +8,13 @@ import { ApplicationError, NodeOperationError, WorkflowActivationError } from 'n
import { ActiveWorkflowManager } from '@/active-workflow-manager';
import { CredentialsService } from '@/credentials/credentials.service';
import { EnterpriseCredentialsService } from '@/credentials/credentials.service.ee';
import type { CredentialsEntity } from '@/databases/entities/credentials-entity';
import { Project } from '@/databases/entities/project';
import { SharedWorkflow } from '@/databases/entities/shared-workflow';
import type { User } from '@/databases/entities/user';
import { CredentialsRepository } from '@/databases/repositories/credentials.repository';
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@@ -37,6 +39,8 @@ export class EnterpriseWorkflowService {
private readonly ownershipService: OwnershipService,
private readonly projectService: ProjectService,
private readonly activeWorkflowManager: ActiveWorkflowManager,
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
private readonly enterpriseCredentialsService: EnterpriseCredentialsService,
) {}
async shareWithProjects(
@@ -46,9 +50,17 @@ export class EnterpriseWorkflowService {
) {
const em = entityManager ?? this.sharedWorkflowRepository.manager;
const projects = await em.find(Project, {
let projects = await em.find(Project, {
where: { id: In(shareWithIds), type: 'personal' },
relations: { sharedWorkflows: true },
});
// filter out all projects that already own the workflow
projects = projects.filter(
(p) =>
!p.sharedWorkflows.some(
(swf) => swf.workflowId === workflowId && swf.role === 'workflow:owner',
),
);
const newSharedWorkflows = projects
// We filter by role === 'project:personalOwner' above and there should
@@ -248,7 +260,12 @@ export class EnterpriseWorkflowService {
});
}
async transferOne(user: User, workflowId: string, destinationProjectId: string) {
async transferOne(
user: User,
workflowId: string,
destinationProjectId: string,
shareCredentials: string[] = [],
) {
// 1. get workflow
const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [
'workflow:move',
@@ -307,7 +324,28 @@ export class EnterpriseWorkflowService {
);
});
// 8. try to activate it again if it was active
// 8. share credentials into the destination project
await this.workflowRepository.manager.transaction(async (trx) => {
const allCredentials = await this.sharedCredentialsRepository.findAllCredentialsForUser(
user,
['credential:share'],
trx,
);
const credentialsAllowedToShare = allCredentials.filter((c) =>
shareCredentials.includes(c.id),
);
for (const credential of credentialsAllowedToShare) {
await this.enterpriseCredentialsService.shareWithProjects(
user,
credential.id,
[destinationProject.id],
trx,
);
}
});
// 9. try to activate it again if it was active
if (wasActive) {
try {
await this.activeWorkflowManager.add(workflowId, 'update');

View File

@@ -1,4 +1,8 @@
import { ImportWorkflowFromUrlDto, ManualRunQueryDto } from '@n8n/api-types';
import {
ImportWorkflowFromUrlDto,
ManualRunQueryDto,
TransferWorkflowBodyDto,
} from '@n8n/api-types';
import { GlobalConfig } from '@n8n/config';
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import { In, type FindOptionsRelations } from '@n8n/typeorm';
@@ -7,7 +11,6 @@ import express from 'express';
import { Logger } from 'n8n-core';
import { ApplicationError } from 'n8n-workflow';
import { v4 as uuid } from 'uuid';
import { z } from 'zod';
import type { Project } from '@/databases/entities/project';
import { SharedWorkflow } from '@/databases/entities/shared-workflow';
@@ -18,7 +21,19 @@ import { SharedWorkflowRepository } from '@/databases/repositories/shared-workfl
import { TagRepository } from '@/databases/repositories/tag.repository';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import * as Db from '@/db';
import { Delete, Get, Patch, Post, ProjectScope, Put, Query, RestController } from '@/decorators';
import {
Body,
Delete,
Get,
Licensed,
Param,
Patch,
Post,
ProjectScope,
Put,
Query,
RestController,
} from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
@@ -402,11 +417,10 @@ export class WorkflowsController {
);
}
@Licensed('feat:sharing')
@Put('/:workflowId/share')
@ProjectScope('workflow:share')
async share(req: WorkflowRequest.Share) {
if (!this.license.isSharingEnabled()) throw new NotFoundError('Route not found');
const { workflowId } = req.params;
const { shareWithIds } = req.body;
@@ -472,13 +486,17 @@ export class WorkflowsController {
@Put('/:workflowId/transfer')
@ProjectScope('workflow:move')
async transfer(req: WorkflowRequest.Transfer) {
const body = z.object({ destinationProjectId: z.string() }).parse(req.body);
async transfer(
req: AuthenticatedRequest,
_res: unknown,
@Param('workflowId') workflowId: string,
@Body body: TransferWorkflowBodyDto,
) {
return await this.enterpriseWorkflowService.transferOne(
req.user,
req.params.workflowId,
workflowId,
body.destinationProjectId,
body.shareCredentials,
);
}
}

View File

@@ -951,6 +951,57 @@ describe('PUT /credentials/:id/share', () => {
expect(mailer.notifyCredentialsShared).toHaveBeenCalledTimes(1);
});
test('should ignore sharing with owner project', async () => {
// ARRANGE
const project = await projectService.createTeamProject(owner, { name: 'Team Project' });
const credential = await saveCredential(randomCredentialPayload(), { project });
// ACT
const response = await authOwnerAgent
.put(`/credentials/${credential.id}/share`)
.send({ shareWithIds: [project.id] });
const sharedCredentials = await Container.get(SharedCredentialsRepository).find({
where: { credentialsId: credential.id },
});
// ASSERT
expect(response.statusCode).toBe(200);
expect(sharedCredentials).toHaveLength(1);
expect(sharedCredentials[0].projectId).toBe(project.id);
expect(sharedCredentials[0].role).toBe('credential:owner');
});
test('should ignore sharing with project that already has it shared', async () => {
// ARRANGE
const project = await projectService.createTeamProject(owner, { name: 'Team Project' });
const credential = await saveCredential(randomCredentialPayload(), { project });
const project2 = await projectService.createTeamProject(owner, { name: 'Team Project 2' });
await shareCredentialWithProjects(credential, [project2]);
// ACT
const response = await authOwnerAgent
.put(`/credentials/${credential.id}/share`)
.send({ shareWithIds: [project2.id] });
const sharedCredentials = await Container.get(SharedCredentialsRepository).find({
where: { credentialsId: credential.id },
});
// ASSERT
expect(response.statusCode).toBe(200);
expect(sharedCredentials).toHaveLength(2);
expect(sharedCredentials).toEqual(
expect.arrayContaining([
expect.objectContaining({ projectId: project.id, role: 'credential:owner' }),
expect.objectContaining({ projectId: project2.id, role: 'credential:user' }),
]),
);
});
test('should respond 400 if invalid payload is provided', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });

View File

@@ -7,8 +7,8 @@ import { SharedWorkflowRepository } from '@/databases/repositories/shared-workfl
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { Telemetry } from '@/telemetry';
import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee';
import { mockInstance } from '@test/mocking';
import { mockInstance } from '../../shared/mocking';
import * as testDb from '../shared/test-db';
import {
FIRST_CREDENTIAL_ID,
@@ -33,6 +33,8 @@ describe('EnterpriseWorkflowService', () => {
mock(),
mock(),
mock(),
mock(),
mock(),
);
});

View File

@@ -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

View File

@@ -38,9 +38,13 @@ let anotherMember: User;
let authOwnerAgent: SuperAgentTest;
let authMemberAgent: SuperAgentTest;
jest.spyOn(License.prototype, 'isSharingEnabled').mockReturnValue(false);
const testServer = utils.setupTestServer({ endpointGroups: ['workflows'] });
const testServer = utils.setupTestServer({
endpointGroups: ['workflows'],
enabledFeatures: ['feat:sharing'],
quotas: {
'quota:maxTeamProjects': -1,
},
});
const license = testServer.license;
const { objectContaining, arrayContaining, any } = expect;
@@ -48,9 +52,19 @@ const { objectContaining, arrayContaining, any } = expect;
const activeWorkflowManagerLike = mockInstance(ActiveWorkflowManager);
let projectRepository: ProjectRepository;
let projectService: ProjectService;
beforeAll(async () => {
beforeEach(async () => {
await testDb.truncate([
'Workflow',
'SharedWorkflow',
'Tag',
'WorkflowHistory',
'Project',
'ProjectRelation',
]);
projectRepository = Container.get(ProjectRepository);
projectService = Container.get(ProjectService);
owner = await createOwner();
authOwnerAgent = testServer.authAgentFor(owner);
member = await createMember();
@@ -58,9 +72,8 @@ beforeAll(async () => {
anotherMember = await createMember();
});
beforeEach(async () => {
jest.resetAllMocks();
await testDb.truncate(['Workflow', 'SharedWorkflow', 'Tag', 'WorkflowHistory']);
afterEach(() => {
jest.clearAllMocks();
});
describe('POST /workflows', () => {
@@ -271,7 +284,7 @@ describe('POST /workflows', () => {
type: 'team',
}),
);
await Container.get(ProjectService).addUser(project.id, owner.id, 'project:admin');
await projectService.addUser(project.id, owner.id, 'project:admin');
//
// ACT
@@ -345,7 +358,7 @@ describe('POST /workflows', () => {
type: 'team',
}),
);
await Container.get(ProjectService).addUser(project.id, member.id, 'project:viewer');
await projectService.addUser(project.id, member.id, 'project:viewer');
//
// ACT