fix(core): Fix user comparison in same-user subworkflow caller policy (#7913)
https://linear.app/n8n/issue/PAY-992 https://community.n8n.io/t/executing-workflow-using-owner-role-created-by-another-user-fails/33443 --------- Co-authored-by: Omar Ajoue <krynble@gmail.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Container } from 'typedi';
|
||||
import type { INodeTypes } from 'n8n-workflow';
|
||||
import type { INodeTypes, WorkflowSettings } from 'n8n-workflow';
|
||||
import { SubworkflowOperationError, Workflow } from 'n8n-workflow';
|
||||
|
||||
import config from '@/config';
|
||||
@@ -16,6 +16,7 @@ import { OwnershipService } from '@/services/ownership.service';
|
||||
import { mockInstance } from '../shared/mocking';
|
||||
import {
|
||||
randomCredentialPayload as randomCred,
|
||||
randomName,
|
||||
randomPositiveDigit,
|
||||
} from '../integration/shared/random';
|
||||
import * as testDb from '../integration/shared/testDb';
|
||||
@@ -26,6 +27,24 @@ import { getCredentialOwnerRole, getWorkflowOwnerRole } from '../integration/sha
|
||||
import { createOwner, createUser } from '../integration/shared/db/users';
|
||||
import { WorkflowRepository } from '@db/repositories/workflow.repository';
|
||||
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
|
||||
import { UserRepository } from '@/databases/repositories/user.repository';
|
||||
import { LicenseMocker } from '../integration/shared/license';
|
||||
import { License } from '@/License';
|
||||
import { generateNanoId } from '@/databases/utils/generators';
|
||||
import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity';
|
||||
|
||||
function createSubworkflow({ policy }: { policy: WorkflowSettings.CallerPolicy }) {
|
||||
return new Workflow({
|
||||
id: uuid(),
|
||||
nodes: [],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes: mockNodeTypes,
|
||||
settings: {
|
||||
callerPolicy: policy,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let mockNodeTypes: INodeTypes;
|
||||
let credentialOwnerRole: Role;
|
||||
@@ -230,7 +249,29 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
|
||||
const nonOwnerUser = new User();
|
||||
nonOwnerUser.id = uuid();
|
||||
|
||||
let parentWorkflow: WorkflowEntity;
|
||||
|
||||
beforeAll(() => {
|
||||
parentWorkflow = Container.get(WorkflowRepository).create({
|
||||
id: generateNanoId(),
|
||||
name: randomName(),
|
||||
active: false,
|
||||
connections: {},
|
||||
nodes: [
|
||||
{
|
||||
name: '',
|
||||
typeVersion: 1,
|
||||
type: 'n8n-nodes-base.executeWorkflow',
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('sets default policy from environment when subworkflow has none', async () => {
|
||||
await Container.get(WorkflowRepository).save(parentWorkflow);
|
||||
|
||||
config.set('workflows.callerPolicyDefaultOption', 'none');
|
||||
jest.spyOn(ownershipService, 'getWorkflowOwnerCached').mockResolvedValue(fakeUser);
|
||||
jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(true);
|
||||
@@ -243,12 +284,15 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
|
||||
id: '2',
|
||||
});
|
||||
await expect(
|
||||
PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, userId),
|
||||
PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id),
|
||||
).rejects.toThrow(`Target workflow ID ${subworkflow.id} may not be called`);
|
||||
});
|
||||
|
||||
test('if sharing is disabled, ensures that workflows are owned by same user and reject running workflows belonging to another user even if setting allows execution', async () => {
|
||||
jest.spyOn(ownershipService, 'getWorkflowOwnerCached').mockResolvedValue(nonOwnerUser);
|
||||
await Container.get(WorkflowRepository).save(parentWorkflow);
|
||||
|
||||
jest.spyOn(ownershipService, 'getWorkflowOwnerCached').mockResolvedValueOnce(fakeUser);
|
||||
jest.spyOn(ownershipService, 'getWorkflowOwnerCached').mockResolvedValueOnce(nonOwnerUser);
|
||||
jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(false);
|
||||
|
||||
const subworkflow = new Workflow({
|
||||
@@ -262,12 +306,12 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
|
||||
},
|
||||
});
|
||||
await expect(
|
||||
PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, userId),
|
||||
PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id),
|
||||
).rejects.toThrow(`Target workflow ID ${subworkflow.id} may not be called`);
|
||||
|
||||
// Check description
|
||||
try {
|
||||
await PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, '', 'abcde');
|
||||
await PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, 'abcde');
|
||||
} catch (error) {
|
||||
if (error instanceof SubworkflowOperationError) {
|
||||
expect(error.description).toBe(
|
||||
@@ -278,7 +322,8 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
|
||||
});
|
||||
|
||||
test('should throw if allowed list does not contain parent workflow id', async () => {
|
||||
const invalidParentWorkflowId = uuid();
|
||||
await Container.get(WorkflowRepository).save(parentWorkflow);
|
||||
|
||||
jest
|
||||
.spyOn(ownershipService, 'getWorkflowOwnerCached')
|
||||
.mockImplementation(async (workflowId) => fakeUser);
|
||||
@@ -296,11 +341,13 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
|
||||
},
|
||||
});
|
||||
await expect(
|
||||
PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, userId, invalidParentWorkflowId),
|
||||
PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id),
|
||||
).rejects.toThrow(`Target workflow ID ${subworkflow.id} may not be called`);
|
||||
});
|
||||
|
||||
test('sameOwner passes when both workflows are owned by the same user', async () => {
|
||||
await Container.get(WorkflowRepository).save(parentWorkflow);
|
||||
|
||||
jest.spyOn(ownershipService, 'getWorkflowOwnerCached').mockImplementation(async () => fakeUser);
|
||||
jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(false);
|
||||
|
||||
@@ -312,12 +359,13 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
|
||||
id: '2',
|
||||
});
|
||||
await expect(
|
||||
PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, userId, userId),
|
||||
PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id),
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
test('workflowsFromAList works when the list contains the parent id', async () => {
|
||||
const workflowId = uuid();
|
||||
await Container.get(WorkflowRepository).save(parentWorkflow);
|
||||
|
||||
jest
|
||||
.spyOn(ownershipService, 'getWorkflowOwnerCached')
|
||||
.mockImplementation(async (workflowId) => fakeUser);
|
||||
@@ -331,15 +379,17 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
|
||||
id: '2',
|
||||
settings: {
|
||||
callerPolicy: 'workflowsFromAList',
|
||||
callerIds: `123,456,bcdef, ${workflowId}`,
|
||||
callerIds: `123,456,bcdef, ${parentWorkflow.id}`,
|
||||
},
|
||||
});
|
||||
await expect(
|
||||
PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, userId, workflowId),
|
||||
PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id),
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
test('should not throw when workflow policy is set to any', async () => {
|
||||
await Container.get(WorkflowRepository).save(parentWorkflow);
|
||||
|
||||
jest
|
||||
.spyOn(ownershipService, 'getWorkflowOwnerCached')
|
||||
.mockImplementation(async (workflowId) => fakeUser);
|
||||
@@ -356,7 +406,44 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
|
||||
},
|
||||
});
|
||||
await expect(
|
||||
PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, userId),
|
||||
PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id),
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
describe('with workflows-from-same-owner caller policy', () => {
|
||||
beforeAll(() => {
|
||||
const license = new LicenseMocker();
|
||||
license.mock(Container.get(License));
|
||||
license.enable('feat:sharing');
|
||||
});
|
||||
|
||||
test('should deny if the two workflows are owned by different users', async () => {
|
||||
const parentWorkflowOwner = Container.get(UserRepository).create({ id: uuid() });
|
||||
const subworkflowOwner = Container.get(UserRepository).create({ id: uuid() });
|
||||
|
||||
ownershipService.getWorkflowOwnerCached.mockResolvedValueOnce(parentWorkflowOwner);
|
||||
ownershipService.getWorkflowOwnerCached.mockResolvedValueOnce(subworkflowOwner);
|
||||
|
||||
const subworkflow = createSubworkflow({ policy: 'workflowsFromSameOwner' });
|
||||
|
||||
const check = PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, uuid());
|
||||
|
||||
await expect(check).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('should allow if both workflows are owned by the same user', async () => {
|
||||
await Container.get(WorkflowRepository).save(parentWorkflow);
|
||||
|
||||
const bothWorkflowsOwner = Container.get(UserRepository).create({ id: uuid() });
|
||||
|
||||
ownershipService.getWorkflowOwnerCached.mockResolvedValueOnce(bothWorkflowsOwner); // parent workflow
|
||||
ownershipService.getWorkflowOwnerCached.mockResolvedValueOnce(bothWorkflowsOwner); // subworkflow
|
||||
|
||||
const subworkflow = createSubworkflow({ policy: 'workflowsFromSameOwner' });
|
||||
|
||||
const check = PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id);
|
||||
|
||||
await expect(check).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user