mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +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:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
>;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user