Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): Permission check for subworkflow properly checking for workflow settings #7576

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 2 additions & 15 deletions packages/cli/src/UserManagement/PermissionChecker.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import type { INode, Workflow } from 'n8n-workflow';
import {
NodeOperationError,
SubworkflowOperationError,
WorkflowOperationError,
} from 'n8n-workflow';
import { NodeOperationError, SubworkflowOperationError } from 'n8n-workflow';
import type { FindOptionsWhere } from 'typeorm';
import { In } from 'typeorm';
import * as Db from '@/Db';
import config from '@/config';
import type { SharedCredentials } from '@db/entities/SharedCredentials';
import { isSharingEnabled } from './UserManagementHelper';
import { WorkflowsService } from '@/workflows/workflows.services';
import { UserService } from '@/services/user.service';
import { OwnershipService } from '@/services/ownership.service';
import Container from 'typedi';
import { RoleService } from '@/services/role.service';
Expand Down Expand Up @@ -135,14 +129,7 @@ export class PermissionChecker {
}

if (policy === 'workflowsFromSameOwner') {
const user = await Container.get(UserService).findOne({ where: { id: userId } });
if (!user) {
throw new WorkflowOperationError(
'Fatal error: user not found. Please contact the system administrator.',
);
}
const sharing = await WorkflowsService.getSharing(user, subworkflow.id, ['role', 'user']);
if (!sharing || sharing.role.name !== 'owner') {
if (!subworkflowOwner || subworkflowOwner.id !== userId) {
krynble marked this conversation as resolved.
Show resolved Hide resolved
throw errorToThrow;
}
}
Expand Down
49 changes: 12 additions & 37 deletions packages/cli/test/unit/PermissionChecker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ import { User } from '@db/entities/User';
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { NodeTypes } from '@/NodeTypes';
import { UserService } from '@/services/user.service';
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
import * as UserManagementHelper from '@/UserManagement/UserManagementHelper';
import { WorkflowsService } from '@/workflows/workflows.services';
import { OwnershipService } from '@/services/ownership.service';

import { mockInstance } from '../integration/shared/utils/';
Expand Down Expand Up @@ -225,18 +223,14 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {

const nonOwnerMockRole = new Role();
nonOwnerMockRole.name = 'editor';
const sharedWorkflowNotOwner = new SharedWorkflow();
sharedWorkflowNotOwner.role = nonOwnerMockRole;

const userService = mockInstance(UserService);
const nonOwnerUser = new User();
nonOwnerUser.id = uuid();

test('sets default policy from environment when subworkflow has none', async () => {
config.set('workflows.callerPolicyDefaultOption', 'none');
jest
.spyOn(ownershipService, 'getWorkflowOwnerCached')
.mockImplementation(async (workflowId) => {
return fakeUser;
});
jest.spyOn(ownershipService, 'getWorkflowOwnerCached').mockImplementation(async () => {
return fakeUser;
});
krynble marked this conversation as resolved.
Show resolved Hide resolved
jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(true);

const subworkflow = new Workflow({
Expand All @@ -251,22 +245,21 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
).rejects.toThrow(`Target workflow ID ${subworkflow.id} may not be called`);
});

test('if sharing is disabled, ensures that workflows are owner by same user', async () => {
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')
.mockImplementation(async (workflowId) => fakeUser);
.mockImplementation(async () => nonOwnerUser);
krynble marked this conversation as resolved.
Show resolved Hide resolved
jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(false);
jest.spyOn(userService, 'findOne').mockImplementation(async () => fakeUser);
jest.spyOn(WorkflowsService, 'getSharing').mockImplementation(async () => {
return sharedWorkflowNotOwner;
});

const subworkflow = new Workflow({
nodes: [],
connections: {},
active: false,
nodeTypes: mockNodeTypes,
id: '2',
settings: {
callerPolicy: 'any',
},
});
await expect(
PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, userId),
Expand All @@ -284,16 +277,12 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
}
});

test('list of ids must include the parent workflow id', async () => {
test('should throw if allowed list does not contain parent workflow id', async () => {
const invalidParentWorkflowId = uuid();
jest
.spyOn(ownershipService, 'getWorkflowOwnerCached')
.mockImplementation(async (workflowId) => fakeUser);
jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(true);
jest.spyOn(userService, 'findOne').mockImplementation(async () => fakeUser);
jest.spyOn(WorkflowsService, 'getSharing').mockImplementation(async () => {
return sharedWorkflowNotOwner;
});

const subworkflow = new Workflow({
nodes: [],
Expand All @@ -312,14 +301,8 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
});

test('sameOwner passes when both workflows are owned by the same user', async () => {
jest
.spyOn(ownershipService, 'getWorkflowOwnerCached')
.mockImplementation(async (workflowId) => fakeUser);
jest.spyOn(ownershipService, 'getWorkflowOwnerCached').mockImplementation(async () => fakeUser);
jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(false);
jest.spyOn(userService, 'findOne').mockImplementation(async () => fakeUser);
jest.spyOn(WorkflowsService, 'getSharing').mockImplementation(async () => {
return sharedWorkflowOwner;
});

const subworkflow = new Workflow({
nodes: [],
Expand All @@ -339,10 +322,6 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
.spyOn(ownershipService, 'getWorkflowOwnerCached')
.mockImplementation(async (workflowId) => fakeUser);
jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(true);
jest.spyOn(userService, 'findOne').mockImplementation(async () => fakeUser);
jest.spyOn(WorkflowsService, 'getSharing').mockImplementation(async () => {
return sharedWorkflowNotOwner;
});

const subworkflow = new Workflow({
nodes: [],
Expand All @@ -365,10 +344,6 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
.spyOn(ownershipService, 'getWorkflowOwnerCached')
.mockImplementation(async (workflowId) => fakeUser);
jest.spyOn(UserManagementHelper, 'isSharingEnabled').mockReturnValue(true);
jest.spyOn(userService, 'findOne').mockImplementation(async () => fakeUser);
jest.spyOn(WorkflowsService, 'getSharing').mockImplementation(async () => {
return sharedWorkflowNotOwner;
});

const subworkflow = new Workflow({
nodes: [],
Expand Down
Loading