diff --git a/packages/cli/src/credentials/credentials.service.ee.ts b/packages/cli/src/credentials/credentials.service.ee.ts index c636bb308b..cf53cb74dd 100644 --- a/packages/cli/src/credentials/credentials.service.ee.ts +++ b/packages/cli/src/credentials/credentials.service.ee.ts @@ -1,7 +1,7 @@ /* eslint-disable import/no-cycle */ /* eslint-disable no-param-reassign */ -import { DeleteResult, EntityManager, In, Not } from 'typeorm'; -import { Db } from '..'; +import { DeleteResult, EntityManager, FindManyOptions, In, Not } from 'typeorm'; +import { Db, ICredentialsDb } from '..'; import { RoleService } from '../role/role.service'; import { CredentialsService } from './credentials.service'; diff --git a/packages/cli/src/credentials/credentials.service.ts b/packages/cli/src/credentials/credentials.service.ts index 5db9f98485..7345959a37 100644 --- a/packages/cli/src/credentials/credentials.service.ts +++ b/packages/cli/src/credentials/credentials.service.ts @@ -7,7 +7,7 @@ import { INodeCredentialTestResult, LoggerProxy, } from 'n8n-workflow'; -import { FindOneOptions, In } from 'typeorm'; +import { FindManyOptions, FindOneOptions, In } from 'typeorm'; import { createCredentialsFromCredentialsEntity, @@ -71,6 +71,10 @@ export class CredentialsService { }); } + static async getMany(filter: FindManyOptions): Promise { + return Db.collections.Credentials.find(filter); + } + /** * Retrieve the sharing that matches a user and a credential. */ diff --git a/packages/cli/src/workflows/workflows.controller.ee.ts b/packages/cli/src/workflows/workflows.controller.ee.ts index 5955bd20a6..4e9866a384 100644 --- a/packages/cli/src/workflows/workflows.controller.ee.ts +++ b/packages/cli/src/workflows/workflows.controller.ee.ts @@ -89,9 +89,11 @@ EEWorkflowController.get( if (!userSharing && req.user.globalRole.name !== 'owner') { throw new ResponseHelper.ResponseError(`Forbidden.`, undefined, 403); } - // @TODO: also return the credentials used by the workflow - return EEWorkflows.addOwnerAndSharings(workflow); + return EEWorkflows.addCredentialsToWorkflow( + EEWorkflows.addOwnerAndSharings(workflow), + req.user, + ); }), ); diff --git a/packages/cli/src/workflows/workflows.services.ee.ts b/packages/cli/src/workflows/workflows.services.ee.ts index 7bcd7c64cf..3189776c2a 100644 --- a/packages/cli/src/workflows/workflows.services.ee.ts +++ b/packages/cli/src/workflows/workflows.services.ee.ts @@ -6,7 +6,7 @@ import { WorkflowEntity } from '../databases/entities/WorkflowEntity'; import { RoleService } from '../role/role.service'; import { UserService } from '../user/user.service'; import { WorkflowsService } from './workflows.services'; -import { WorkflowWithSharings } from './workflows.types'; +import type { WorkflowWithSharingsAndCredentials } from './workflows.types'; import { EECredentialsService as EECredentials } from '../credentials/credentials.service.ee'; export class EEWorkflowsService extends WorkflowsService { @@ -74,10 +74,11 @@ export class EEWorkflowsService extends WorkflowsService { } static addOwnerAndSharings( - workflow: WorkflowEntity & WorkflowWithSharings, - ): WorkflowEntity & WorkflowWithSharings { + workflow: WorkflowWithSharingsAndCredentials, + ): WorkflowWithSharingsAndCredentials { workflow.ownedBy = null; workflow.sharedWith = []; + workflow.usedCredentials = []; workflow.shared?.forEach(({ user, role }) => { const { id, email, firstName, lastName } = user; @@ -90,12 +91,49 @@ export class EEWorkflowsService extends WorkflowsService { workflow.sharedWith?.push({ id, email, firstName, lastName }); }); - // @ts-ignore delete workflow.shared; return workflow; } + static async addCredentialsToWorkflow( + workflow: WorkflowWithSharingsAndCredentials, + currentUser: User, + ): Promise { + workflow.usedCredentials = []; + const userCredentials = await EECredentials.getAll(currentUser); + const credentialIdsUsedByWorkflow = new Set(); + workflow.nodes.forEach((node) => { + if (!node.credentials) { + return; + } + Object.keys(node.credentials).forEach((credentialType) => { + const credential = node.credentials?.[credentialType]; + if (!credential?.id) { + return; + } + const credentialId = parseInt(credential.id, 10); + credentialIdsUsedByWorkflow.add(credentialId); + }); + }); + const workflowCredentials = await EECredentials.getMany({ + where: { + id: In(Array.from(credentialIdsUsedByWorkflow)), + }, + }); + const userCredentialIds = userCredentials.map((credential) => credential.id.toString()); + workflowCredentials.forEach((credential) => { + const credentialId = credential.id.toString(); + workflow.usedCredentials?.push({ + id: credential.id.toString(), + name: credential.name, + currentUserHasAccess: userCredentialIds.includes(credentialId), + }); + }); + + return workflow; + } + static validateCredentialPermissionsToUser( workflow: WorkflowEntity, allowedCredentials: ICredentialsDb[], diff --git a/packages/cli/src/workflows/workflows.types.ts b/packages/cli/src/workflows/workflows.types.ts index cb9f51c248..947e4c6968 100644 --- a/packages/cli/src/workflows/workflows.types.ts +++ b/packages/cli/src/workflows/workflows.types.ts @@ -1,7 +1,16 @@ import type { IUser } from 'n8n-workflow'; +import { SharedWorkflow } from '../databases/entities/SharedWorkflow'; import { WorkflowEntity } from '../databases/entities/WorkflowEntity'; -export interface WorkflowWithSharings extends WorkflowEntity { +export interface WorkflowWithSharingsAndCredentials extends Omit { ownedBy?: IUser | null; sharedWith?: IUser[]; + usedCredentials?: CredentialUsedByWorkflow[]; + shared?: SharedWorkflow[]; +} + +export interface CredentialUsedByWorkflow { + id: string; + name: string; + currentUserHasAccess: boolean; } diff --git a/packages/cli/test/integration/workflows.controller.ee.test.ts b/packages/cli/test/integration/workflows.controller.ee.test.ts index e603544286..e6ada3aca0 100644 --- a/packages/cli/test/integration/workflows.controller.ee.test.ts +++ b/packages/cli/test/integration/workflows.controller.ee.test.ts @@ -213,6 +213,127 @@ describe('GET /workflows/:id', () => { expect(response.body.data.sharedWith).toHaveLength(2); }); + + test('GET should return workflow with credentials owned by user', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + + const workflowPayload = makeWorkflow({ + withPinData: false, + withCredential: { id: savedCredential.id.toString(), name: savedCredential.name }, + }); + const workflow = await createWorkflow(workflowPayload, owner); + + const response = await authAgent(owner).get(`/workflows/${workflow.id}`); + + expect(response.statusCode).toBe(200); + expect(response.body.data.usedCredentials).toMatchObject([ + { + id: savedCredential.id.toString(), + name: savedCredential.name, + currentUserHasAccess: true, + }, + ]); + + expect(response.body.data.sharedWith).toHaveLength(0); + }); + + test('GET should return workflow with credentials saying owner has access even when not shared', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const member = await testDb.createUser({ globalRole: globalMemberRole }); + const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); + + const workflowPayload = makeWorkflow({ + withPinData: false, + withCredential: { id: savedCredential.id.toString(), name: savedCredential.name }, + }); + const workflow = await createWorkflow(workflowPayload, owner); + + const response = await authAgent(owner).get(`/workflows/${workflow.id}`); + + expect(response.statusCode).toBe(200); + expect(response.body.data.usedCredentials).toMatchObject([ + { + id: savedCredential.id.toString(), + name: savedCredential.name, + currentUserHasAccess: true, // owner has access to any cred + }, + ]); + + expect(response.body.data.sharedWith).toHaveLength(0); + }); + + test('GET should return workflow with credentials for all users with or without access', async () => { + const member1 = await testDb.createUser({ globalRole: globalMemberRole }); + const member2 = await testDb.createUser({ globalRole: globalMemberRole }); + const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 }); + + const workflowPayload = makeWorkflow({ + withPinData: false, + withCredential: { id: savedCredential.id.toString(), name: savedCredential.name }, + }); + const workflow = await createWorkflow(workflowPayload, member1); + await testDb.shareWorkflowWithUsers(workflow, [member2]); + + const responseMember1 = await authAgent(member1).get(`/workflows/${workflow.id}`); + expect(responseMember1.statusCode).toBe(200); + expect(responseMember1.body.data.usedCredentials).toMatchObject([ + { + id: savedCredential.id.toString(), + name: savedCredential.name, + currentUserHasAccess: true, // one user has access + }, + ]); + expect(responseMember1.body.data.sharedWith).toHaveLength(1); + + const responseMember2 = await authAgent(member2).get(`/workflows/${workflow.id}`); + expect(responseMember2.statusCode).toBe(200); + expect(responseMember2.body.data.usedCredentials).toMatchObject([ + { + id: savedCredential.id.toString(), + name: savedCredential.name, + currentUserHasAccess: false, // the other one doesn't + }, + ]); + expect(responseMember2.body.data.sharedWith).toHaveLength(1); + }); + + test('GET should return workflow with credentials for all users with access', async () => { + const member1 = await testDb.createUser({ globalRole: globalMemberRole }); + const member2 = await testDb.createUser({ globalRole: globalMemberRole }); + const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 }); + // Both users have access to the credential (none is owner) + await testDb.shareCredentialWithUsers(savedCredential, [member2]); + + const workflowPayload = makeWorkflow({ + withPinData: false, + withCredential: { id: savedCredential.id.toString(), name: savedCredential.name }, + }); + const workflow = await createWorkflow(workflowPayload, member1); + await testDb.shareWorkflowWithUsers(workflow, [member2]); + + const responseMember1 = await authAgent(member1).get(`/workflows/${workflow.id}`); + expect(responseMember1.statusCode).toBe(200); + expect(responseMember1.body.data.usedCredentials).toMatchObject([ + { + id: savedCredential.id.toString(), + name: savedCredential.name, + currentUserHasAccess: true, + }, + ]); + expect(responseMember1.body.data.sharedWith).toHaveLength(1); + + const responseMember2 = await authAgent(member2).get(`/workflows/${workflow.id}`); + expect(responseMember2.statusCode).toBe(200); + expect(responseMember2.body.data.usedCredentials).toMatchObject([ + { + id: savedCredential.id.toString(), + name: savedCredential.name, + currentUserHasAccess: true, + }, + ]); + expect(responseMember2.body.data.sharedWith).toHaveLength(1); + }); }); describe('POST /workflows', () => {