import { v4 as uuid } from 'uuid'; import { Container } from 'typedi'; import type { INode, INodeTypeData } from 'n8n-workflow'; import { randomInt } from 'n8n-workflow'; import type { User } from '@/databases/entities/user'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { NodeTypes } from '@/node-types'; import { OwnershipService } from '@/services/ownership.service'; import { PermissionChecker } from '@/user-management/permission-checker'; import { mockInstance } from '../shared/mocking'; import { randomCredentialPayload as randomCred } from './shared/random'; import * as testDb from './shared/test-db'; import type { SaveCredentialFunction } from './shared/types'; import { affixRoleToSaveCredential } from './shared/db/credentials'; import { createOwner, createUser } from './shared/db/users'; import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; import { getPersonalProject } from './shared/db/projects'; import type { WorkflowEntity } from '@/databases/entities/workflow-entity'; import type { Project } from '@/databases/entities/project'; import { ProjectRepository } from '@/databases/repositories/project.repository'; const ownershipService = mockInstance(OwnershipService); function mockNodeTypesData( nodeNames: string[], options?: { addTrigger?: boolean; }, ) { return nodeNames.reduce((acc, nodeName) => { return ( (acc[`n8n-nodes-base.${nodeName}`] = { sourcePath: '', type: { description: { displayName: nodeName, name: nodeName, group: [], description: '', version: 1, defaults: {}, inputs: [], outputs: [], properties: [], }, trigger: options?.addTrigger ? async () => undefined : undefined, }, }), acc ); }, {}); } const createWorkflow = async (nodes: INode[], workflowOwner?: User): Promise => { const workflowDetails = { id: randomInt(1, 10).toString(), name: 'test', active: false, connections: {}, nodeTypes: mockNodeTypes, nodes, }; const workflowEntity = await Container.get(WorkflowRepository).save(workflowDetails); if (workflowOwner) { const project = await getPersonalProject(workflowOwner); await Container.get(SharedWorkflowRepository).save({ workflow: workflowEntity, user: workflowOwner, project, role: 'workflow:owner', }); } return workflowEntity; }; let saveCredential: SaveCredentialFunction; let owner: User; let member: User; let ownerPersonalProject: Project; let memberPersonalProject: Project; const mockNodeTypes = mockInstance(NodeTypes); mockInstance(LoadNodesAndCredentials, { loadedNodes: mockNodeTypesData(['start', 'actionNetwork']), }); let permissionChecker: PermissionChecker; beforeAll(async () => { await testDb.init(); saveCredential = affixRoleToSaveCredential('credential:owner'); permissionChecker = Container.get(PermissionChecker); [owner, member] = await Promise.all([createOwner(), createUser()]); ownerPersonalProject = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail( owner.id, ); memberPersonalProject = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail( member.id, ); }); describe('check()', () => { beforeEach(async () => { await testDb.truncate(['Workflow', 'Credentials']); }); afterAll(async () => { await testDb.terminate(); }); test('should allow if workflow has no creds', async () => { const nodes: INode[] = [ { id: uuid(), name: 'Start', type: 'n8n-nodes-base.start', typeVersion: 1, parameters: {}, position: [0, 0], }, ]; const workflow = await createWorkflow(nodes, member); ownershipService.getWorkflowProjectCached.mockResolvedValueOnce(memberPersonalProject); await expect(permissionChecker.check(workflow.id, nodes)).resolves.not.toThrow(); }); test('should allow if workflow creds are valid subset', async () => { const ownerCred = await saveCredential(randomCred(), { user: owner }); const memberCred = await saveCredential(randomCred(), { user: member }); await Container.get(SharedCredentialsRepository).save( Container.get(SharedCredentialsRepository).create({ projectId: (await getPersonalProject(member)).id, credentialsId: ownerCred.id, role: 'credential:user', }), ); const nodes: INode[] = [ { id: uuid(), name: 'Action Network', type: 'n8n-nodes-base.actionNetwork', parameters: {}, typeVersion: 1, position: [0, 0], credentials: { actionNetworkApi: { id: ownerCred.id, name: ownerCred.name, }, }, }, { id: uuid(), name: 'Action Network 2', type: 'n8n-nodes-base.actionNetwork', parameters: {}, typeVersion: 1, position: [0, 0], credentials: { actionNetworkApi: { id: memberCred.id, name: memberCred.name, }, }, }, ]; const workflowEntity = await createWorkflow(nodes, member); ownershipService.getWorkflowProjectCached.mockResolvedValueOnce(memberPersonalProject); await expect(permissionChecker.check(workflowEntity.id, nodes)).resolves.not.toThrow(); }); test('should deny if workflow creds are not valid subset', async () => { const memberCred = await saveCredential(randomCred(), { user: member }); const ownerCred = await saveCredential(randomCred(), { user: owner }); const nodes = [ { id: uuid(), name: 'Action Network', type: 'n8n-nodes-base.actionNetwork', parameters: {}, typeVersion: 1, position: [0, 0] as [number, number], credentials: { actionNetworkApi: { id: memberCred.id, name: memberCred.name, }, }, }, { id: uuid(), name: 'Action Network 2', type: 'n8n-nodes-base.actionNetwork', parameters: {}, typeVersion: 1, position: [0, 0] as [number, number], credentials: { actionNetworkApi: { id: ownerCred.id, name: ownerCred.name, }, }, }, ]; const workflowEntity = await createWorkflow(nodes, member); await expect( permissionChecker.check(workflowEntity.id, workflowEntity.nodes), ).rejects.toThrow(); }); test('should allow all credentials if current user is instance owner', async () => { const memberCred = await saveCredential(randomCred(), { user: member }); const ownerCred = await saveCredential(randomCred(), { user: owner }); const nodes = [ { id: uuid(), name: 'Action Network', type: 'n8n-nodes-base.actionNetwork', parameters: {}, typeVersion: 1, position: [0, 0] as [number, number], credentials: { actionNetworkApi: { id: memberCred.id, name: memberCred.name, }, }, }, { id: uuid(), name: 'Action Network 2', type: 'n8n-nodes-base.actionNetwork', parameters: {}, typeVersion: 1, position: [0, 0] as [number, number], credentials: { actionNetworkApi: { id: ownerCred.id, name: ownerCred.name, }, }, }, ]; const workflowEntity = await createWorkflow(nodes, owner); ownershipService.getWorkflowProjectCached.mockResolvedValueOnce(ownerPersonalProject); ownershipService.getPersonalProjectOwnerCached.mockResolvedValueOnce(owner); await expect( permissionChecker.check(workflowEntity.id, workflowEntity.nodes), ).resolves.not.toThrow(); }); });