feat(core): Move execution permission checks earlier in the lifecycle (#8677)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2024-02-21 14:47:02 +01:00
committed by GitHub
parent a573146135
commit 059d281fd1
10 changed files with 139 additions and 201 deletions

View File

@@ -1,6 +1,6 @@
import { v4 as uuid } from 'uuid';
import { Container } from 'typedi';
import type { WorkflowSettings } from 'n8n-workflow';
import type { INode, WorkflowSettings } from 'n8n-workflow';
import { SubworkflowOperationError, Workflow } from 'n8n-workflow';
import config from '@/config';
@@ -87,6 +87,8 @@ beforeAll(async () => {
});
describe('check()', () => {
const workflowId = randomPositiveDigit().toString();
beforeEach(async () => {
await testDb.truncate(['Workflow', 'Credentials']);
});
@@ -97,56 +99,40 @@ describe('check()', () => {
test('should allow if workflow has no creds', async () => {
const userId = uuid();
const nodes: INode[] = [
{
id: uuid(),
name: 'Start',
type: 'n8n-nodes-base.start',
typeVersion: 1,
parameters: {},
position: [0, 0],
},
];
const workflow = new Workflow({
id: randomPositiveDigit().toString(),
name: 'test',
active: false,
connections: {},
nodeTypes: mockNodeTypes,
nodes: [
{
id: uuid(),
name: 'Start',
type: 'n8n-nodes-base.start',
typeVersion: 1,
parameters: {},
position: [0, 0],
},
],
});
expect(async () => await permissionChecker.check(workflow, userId)).not.toThrow();
expect(async () => await permissionChecker.check(workflowId, userId, nodes)).not.toThrow();
});
test('should allow if requesting user is instance owner', async () => {
const owner = await createOwner();
const workflow = new Workflow({
id: randomPositiveDigit().toString(),
name: 'test',
active: false,
connections: {},
nodeTypes: mockNodeTypes,
nodes: [
{
id: uuid(),
name: 'Action Network',
type: 'n8n-nodes-base.actionNetwork',
parameters: {},
typeVersion: 1,
position: [0, 0],
credentials: {
actionNetworkApi: {
id: randomPositiveDigit().toString(),
name: 'Action Network Account',
},
const nodes: INode[] = [
{
id: uuid(),
name: 'Action Network',
type: 'n8n-nodes-base.actionNetwork',
parameters: {},
typeVersion: 1,
position: [0, 0],
credentials: {
actionNetworkApi: {
id: randomPositiveDigit().toString(),
name: 'Action Network Account',
},
},
],
});
},
];
expect(async () => await permissionChecker.check(workflow, owner.id)).not.toThrow();
expect(async () => await permissionChecker.check(workflowId, owner.id, nodes)).not.toThrow();
});
test('should allow if workflow creds are valid subset', async () => {
@@ -154,46 +140,38 @@ describe('check()', () => {
const ownerCred = await saveCredential(randomCred(), { user: owner });
const memberCred = await saveCredential(randomCred(), { user: member });
const workflow = new Workflow({
id: randomPositiveDigit().toString(),
name: 'test',
active: false,
connections: {},
nodeTypes: mockNodeTypes,
nodes: [
{
id: uuid(),
name: 'Action Network',
type: 'n8n-nodes-base.actionNetwork',
parameters: {},
typeVersion: 1,
position: [0, 0],
credentials: {
actionNetworkApi: {
id: ownerCred.id,
name: ownerCred.name,
},
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,
},
},
{
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,
},
},
],
});
},
];
expect(async () => await permissionChecker.check(workflow, owner.id)).not.toThrow();
expect(async () => await permissionChecker.check(workflowId, owner.id, nodes)).not.toThrow();
});
test('should deny if workflow creds are not valid subset', async () => {
@@ -247,9 +225,9 @@ describe('check()', () => {
role: 'workflow:owner',
});
const workflow = new Workflow(workflowDetails);
await expect(permissionChecker.check(workflow, member.id)).rejects.toThrow();
await expect(
permissionChecker.check(workflowDetails.id, member.id, workflowDetails.nodes),
).rejects.toThrow();
});
});

View File

@@ -1,4 +1,4 @@
import { type INodeTypes, Workflow } from 'n8n-workflow';
import type { INode } from 'n8n-workflow';
import { mock } from 'jest-mock-extended';
import type { User } from '@db/entities/User';
import type { UserRepository } from '@db/repositories/user.repository';
@@ -21,36 +21,30 @@ describe('PermissionChecker', () => {
license,
);
const workflow = new Workflow({
id: '1',
name: 'test',
active: false,
connections: {},
nodeTypes: mock<INodeTypes>(),
nodes: [
{
id: 'node-id',
name: 'HTTP Request',
type: 'n8n-nodes-base.httpRequest',
parameters: {},
typeVersion: 1,
position: [0, 0],
credentials: {
oAuth2Api: {
id: 'cred-id',
name: 'Custom oAuth2',
},
const workflowId = '1';
const nodes: INode[] = [
{
id: 'node-id',
name: 'HTTP Request',
type: 'n8n-nodes-base.httpRequest',
parameters: {},
typeVersion: 1,
position: [0, 0],
credentials: {
oAuth2Api: {
id: 'cred-id',
name: 'Custom oAuth2',
},
},
],
});
},
];
beforeEach(() => jest.clearAllMocks());
describe('check', () => {
it('should throw if no user is found', async () => {
userRepo.findOneOrFail.mockRejectedValue(new Error('Fail'));
await expect(permissionChecker.check(workflow, '123')).rejects.toThrow();
await expect(permissionChecker.check(workflowId, '123', nodes)).rejects.toThrow();
expect(license.isSharingEnabled).not.toHaveBeenCalled();
expect(sharedWorkflowRepo.getSharedUserIds).not.toBeCalled();
expect(sharedCredentialsRepo.getOwnedCredentialIds).not.toHaveBeenCalled();
@@ -60,7 +54,7 @@ describe('PermissionChecker', () => {
it('should allow a user if they have a global `workflow:execute` scope', async () => {
userRepo.findOneOrFail.mockResolvedValue(user);
user.hasGlobalScope.calledWith('workflow:execute').mockReturnValue(true);
await expect(permissionChecker.check(workflow, user.id)).resolves.not.toThrow();
await expect(permissionChecker.check(workflowId, user.id, nodes)).resolves.not.toThrow();
expect(license.isSharingEnabled).not.toHaveBeenCalled();
expect(sharedWorkflowRepo.getSharedUserIds).not.toBeCalled();
expect(sharedCredentialsRepo.getOwnedCredentialIds).not.toHaveBeenCalled();
@@ -77,7 +71,7 @@ describe('PermissionChecker', () => {
it('should validate credential access using only owned credentials', async () => {
sharedCredentialsRepo.getOwnedCredentialIds.mockResolvedValue(['cred-id']);
await expect(permissionChecker.check(workflow, user.id)).resolves.not.toThrow();
await expect(permissionChecker.check(workflowId, user.id, nodes)).resolves.not.toThrow();
expect(sharedWorkflowRepo.getSharedUserIds).not.toBeCalled();
expect(sharedCredentialsRepo.getOwnedCredentialIds).toBeCalledWith([user.id]);
@@ -87,7 +81,7 @@ describe('PermissionChecker', () => {
it('should throw when the user does not have access to the credential', async () => {
sharedCredentialsRepo.getOwnedCredentialIds.mockResolvedValue(['cred-id2']);
await expect(permissionChecker.check(workflow, user.id)).rejects.toThrow(
await expect(permissionChecker.check(workflowId, user.id, nodes)).rejects.toThrow(
'Node has no access to credential',
);
@@ -108,9 +102,9 @@ describe('PermissionChecker', () => {
it('should validate credential access using only owned credentials', async () => {
sharedCredentialsRepo.getAccessibleCredentialIds.mockResolvedValue(['cred-id']);
await expect(permissionChecker.check(workflow, user.id)).resolves.not.toThrow();
await expect(permissionChecker.check(workflowId, user.id, nodes)).resolves.not.toThrow();
expect(sharedWorkflowRepo.getSharedUserIds).toBeCalledWith(workflow.id);
expect(sharedWorkflowRepo.getSharedUserIds).toBeCalledWith(workflowId);
expect(sharedCredentialsRepo.getAccessibleCredentialIds).toBeCalledWith([
user.id,
'another-user',
@@ -121,7 +115,7 @@ describe('PermissionChecker', () => {
it('should throw when the user does not have access to the credential', async () => {
sharedCredentialsRepo.getAccessibleCredentialIds.mockResolvedValue(['cred-id2']);
await expect(permissionChecker.check(workflow, user.id)).rejects.toThrow(
await expect(permissionChecker.check(workflowId, user.id, nodes)).rejects.toThrow(
'Node has no access to credential',
);