2024-02-02 03:21:53 -08:00
|
|
|
import { v4 as uuid } from 'uuid';
|
|
|
|
import { Container } from 'typedi';
|
2024-08-05 02:12:25 -07:00
|
|
|
import type { INode, INodeTypeData } from 'n8n-workflow';
|
2024-07-24 03:51:01 -07:00
|
|
|
import { randomInt } from 'n8n-workflow';
|
2024-05-17 01:53:15 -07:00
|
|
|
import type { User } from '@db/entities/User';
|
2024-02-02 03:21:53 -08:00
|
|
|
import { WorkflowRepository } from '@db/repositories/workflow.repository';
|
|
|
|
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
|
2024-08-22 02:10:37 -07:00
|
|
|
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
|
|
|
import { NodeTypes } from '@/node-types';
|
2024-02-02 03:21:53 -08:00
|
|
|
import { OwnershipService } from '@/services/ownership.service';
|
2024-08-22 02:10:37 -07:00
|
|
|
import { PermissionChecker } from '@/user-management/permission-checker';
|
2024-02-02 03:21:53 -08:00
|
|
|
|
|
|
|
import { mockInstance } from '../shared/mocking';
|
2024-08-22 02:10:37 -07:00
|
|
|
import { randomCredentialPayload as randomCred } from './shared/random';
|
|
|
|
import * as testDb from './shared/testDb';
|
|
|
|
import type { SaveCredentialFunction } from './shared/types';
|
|
|
|
import { affixRoleToSaveCredential } from './shared/db/credentials';
|
|
|
|
import { createOwner, createUser } from './shared/db/users';
|
2024-03-05 10:18:34 -08:00
|
|
|
import { SharedCredentialsRepository } from '@/databases/repositories/sharedCredentials.repository';
|
2024-05-17 01:53:15 -07:00
|
|
|
import { getPersonalProject } from './shared/db/projects';
|
2024-03-05 10:18:34 -08:00
|
|
|
import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity';
|
2024-07-24 03:51:01 -07:00
|
|
|
import type { Project } from '@/databases/entities/Project';
|
2024-05-17 01:53:15 -07:00
|
|
|
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
2024-02-02 03:21:53 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
const ownershipService = mockInstance(OwnershipService);
|
|
|
|
|
2024-08-05 02:12:25 -07:00
|
|
|
function mockNodeTypesData(
|
|
|
|
nodeNames: string[],
|
|
|
|
options?: {
|
|
|
|
addTrigger?: boolean;
|
|
|
|
},
|
|
|
|
) {
|
|
|
|
return nodeNames.reduce<INodeTypeData>((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
|
|
|
|
);
|
|
|
|
}, {});
|
|
|
|
}
|
|
|
|
|
2024-03-05 10:18:34 -08:00
|
|
|
const createWorkflow = async (nodes: INode[], workflowOwner?: User): Promise<WorkflowEntity> => {
|
|
|
|
const workflowDetails = {
|
2024-06-19 04:33:57 -07:00
|
|
|
id: randomInt(1, 10).toString(),
|
2024-03-05 10:18:34 -08:00
|
|
|
name: 'test',
|
|
|
|
active: false,
|
|
|
|
connections: {},
|
|
|
|
nodeTypes: mockNodeTypes,
|
|
|
|
nodes,
|
|
|
|
};
|
|
|
|
|
|
|
|
const workflowEntity = await Container.get(WorkflowRepository).save(workflowDetails);
|
|
|
|
if (workflowOwner) {
|
2024-05-17 01:53:15 -07:00
|
|
|
const project = await getPersonalProject(workflowOwner);
|
|
|
|
|
2024-03-05 10:18:34 -08:00
|
|
|
await Container.get(SharedWorkflowRepository).save({
|
|
|
|
workflow: workflowEntity,
|
|
|
|
user: workflowOwner,
|
2024-05-17 01:53:15 -07:00
|
|
|
project,
|
2024-03-05 10:18:34 -08:00
|
|
|
role: 'workflow:owner',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return workflowEntity;
|
|
|
|
};
|
|
|
|
|
2024-02-02 03:21:53 -08:00
|
|
|
let saveCredential: SaveCredentialFunction;
|
|
|
|
|
2024-03-05 10:18:34 -08:00
|
|
|
let owner: User;
|
|
|
|
let member: User;
|
2024-05-17 01:53:15 -07:00
|
|
|
let ownerPersonalProject: Project;
|
|
|
|
let memberPersonalProject: Project;
|
2024-03-05 10:18:34 -08:00
|
|
|
|
2024-02-02 03:21:53 -08:00
|
|
|
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);
|
2024-03-05 10:18:34 -08:00
|
|
|
|
|
|
|
[owner, member] = await Promise.all([createOwner(), createUser()]);
|
2024-05-17 01:53:15 -07:00
|
|
|
ownerPersonalProject = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(
|
|
|
|
owner.id,
|
|
|
|
);
|
|
|
|
memberPersonalProject = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(
|
|
|
|
member.id,
|
|
|
|
);
|
2024-03-05 10:18:34 -08:00
|
|
|
});
|
2024-02-21 05:47:02 -08:00
|
|
|
|
2024-03-05 10:18:34 -08:00
|
|
|
describe('check()', () => {
|
2024-02-02 03:21:53 -08:00
|
|
|
beforeEach(async () => {
|
|
|
|
await testDb.truncate(['Workflow', 'Credentials']);
|
|
|
|
});
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
await testDb.terminate();
|
|
|
|
});
|
|
|
|
|
|
|
|
test('should allow if workflow has no creds', async () => {
|
2024-02-21 05:47:02 -08:00
|
|
|
const nodes: INode[] = [
|
|
|
|
{
|
|
|
|
id: uuid(),
|
|
|
|
name: 'Start',
|
|
|
|
type: 'n8n-nodes-base.start',
|
|
|
|
typeVersion: 1,
|
|
|
|
parameters: {},
|
|
|
|
position: [0, 0],
|
|
|
|
},
|
|
|
|
];
|
2024-02-02 03:21:53 -08:00
|
|
|
|
2024-03-05 10:18:34 -08:00
|
|
|
const workflow = await createWorkflow(nodes, member);
|
2024-05-17 01:53:15 -07:00
|
|
|
ownershipService.getWorkflowProjectCached.mockResolvedValueOnce(memberPersonalProject);
|
2024-03-05 10:18:34 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
await expect(permissionChecker.check(workflow.id, nodes)).resolves.not.toThrow();
|
2024-03-05 10:18:34 -08:00
|
|
|
});
|
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
test('should allow if workflow creds are valid subset', async () => {
|
2024-03-05 10:18:34 -08:00
|
|
|
const ownerCred = await saveCredential(randomCred(), { user: owner });
|
|
|
|
const memberCred = await saveCredential(randomCred(), { user: member });
|
|
|
|
|
|
|
|
await Container.get(SharedCredentialsRepository).save(
|
|
|
|
Container.get(SharedCredentialsRepository).create({
|
2024-05-17 01:53:15 -07:00
|
|
|
projectId: (await getPersonalProject(member)).id,
|
2024-03-05 10:18:34 -08:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
const workflowEntity = await createWorkflow(nodes, member);
|
2024-03-05 10:18:34 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
ownershipService.getWorkflowProjectCached.mockResolvedValueOnce(memberPersonalProject);
|
|
|
|
|
|
|
|
await expect(permissionChecker.check(workflowEntity.id, nodes)).resolves.not.toThrow();
|
2024-02-02 03:21:53 -08:00
|
|
|
});
|
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
test('should deny if workflow creds are not valid subset', async () => {
|
2024-03-05 10:18:34 -08:00
|
|
|
const memberCred = await saveCredential(randomCred(), { user: member });
|
2024-05-17 01:53:15 -07:00
|
|
|
const ownerCred = await saveCredential(randomCred(), { user: owner });
|
2024-03-05 10:18:34 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
const nodes = [
|
2024-03-05 10:18:34 -08:00
|
|
|
{
|
|
|
|
id: uuid(),
|
|
|
|
name: 'Action Network',
|
|
|
|
type: 'n8n-nodes-base.actionNetwork',
|
|
|
|
parameters: {},
|
|
|
|
typeVersion: 1,
|
2024-05-17 01:53:15 -07:00
|
|
|
position: [0, 0] as [number, number],
|
2024-03-05 10:18:34 -08:00
|
|
|
credentials: {
|
|
|
|
actionNetworkApi: {
|
|
|
|
id: memberCred.id,
|
|
|
|
name: memberCred.name,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2024-02-21 05:47:02 -08:00
|
|
|
{
|
|
|
|
id: uuid(),
|
2024-05-17 01:53:15 -07:00
|
|
|
name: 'Action Network 2',
|
2024-02-21 05:47:02 -08:00
|
|
|
type: 'n8n-nodes-base.actionNetwork',
|
|
|
|
parameters: {},
|
|
|
|
typeVersion: 1,
|
2024-05-17 01:53:15 -07:00
|
|
|
position: [0, 0] as [number, number],
|
2024-02-21 05:47:02 -08:00
|
|
|
credentials: {
|
|
|
|
actionNetworkApi: {
|
|
|
|
id: ownerCred.id,
|
|
|
|
name: ownerCred.name,
|
2024-02-02 03:21:53 -08:00
|
|
|
},
|
|
|
|
},
|
2024-02-21 05:47:02 -08:00
|
|
|
},
|
|
|
|
];
|
2024-02-02 03:21:53 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
const workflowEntity = await createWorkflow(nodes, member);
|
2024-03-05 10:18:34 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
await expect(
|
|
|
|
permissionChecker.check(workflowEntity.id, workflowEntity.nodes),
|
|
|
|
).rejects.toThrow();
|
2024-02-02 03:21:53 -08:00
|
|
|
});
|
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
test('should allow all credentials if current user is instance owner', async () => {
|
2024-02-02 03:21:53 -08:00
|
|
|
const memberCred = await saveCredential(randomCred(), { user: member });
|
2024-05-17 01:53:15 -07:00
|
|
|
const ownerCred = await saveCredential(randomCred(), { user: owner });
|
2024-02-02 03:21:53 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
const nodes = [
|
2024-03-05 10:18:34 -08:00
|
|
|
{
|
|
|
|
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,
|
2024-02-02 03:21:53 -08:00
|
|
|
},
|
|
|
|
},
|
2024-03-05 10:18:34 -08:00
|
|
|
},
|
|
|
|
{
|
|
|
|
id: uuid(),
|
|
|
|
name: 'Action Network 2',
|
|
|
|
type: 'n8n-nodes-base.actionNetwork',
|
|
|
|
parameters: {},
|
|
|
|
typeVersion: 1,
|
|
|
|
position: [0, 0] as [number, number],
|
|
|
|
credentials: {
|
|
|
|
actionNetworkApi: {
|
2024-05-17 01:53:15 -07:00
|
|
|
id: ownerCred.id,
|
|
|
|
name: ownerCred.name,
|
2024-02-02 03:21:53 -08:00
|
|
|
},
|
|
|
|
},
|
2024-03-05 10:18:34 -08:00
|
|
|
},
|
|
|
|
];
|
2024-02-02 03:21:53 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
const workflowEntity = await createWorkflow(nodes, owner);
|
|
|
|
ownershipService.getWorkflowProjectCached.mockResolvedValueOnce(ownerPersonalProject);
|
|
|
|
ownershipService.getProjectOwnerCached.mockResolvedValueOnce(owner);
|
2024-02-02 03:21:53 -08:00
|
|
|
|
2024-05-17 01:53:15 -07:00
|
|
|
await expect(
|
|
|
|
permissionChecker.check(workflowEntity.id, workflowEntity.nodes),
|
|
|
|
).resolves.not.toThrow();
|
2024-02-02 03:21:53 -08:00
|
|
|
});
|
|
|
|
});
|