mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(core): Move execution permission checks earlier in the lifecycle (#8677)
This commit is contained in:
committed by
GitHub
parent
a573146135
commit
059d281fd1
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user