refactor(core): Use DI in PermissionChecker (no-changelog) (#8344)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-01-16 14:15:29 +01:00 committed by GitHub
parent 420b4271a9
commit 64ceb16af6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 52 additions and 42 deletions

View file

@ -1,22 +1,32 @@
import { Service } from 'typedi';
import type { INode, Workflow } from 'n8n-workflow'; import type { INode, Workflow } from 'n8n-workflow';
import { NodeOperationError, WorkflowOperationError } from 'n8n-workflow'; import { NodeOperationError, WorkflowOperationError } from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
import { isSharingEnabled } from './UserManagementHelper'; import { isSharingEnabled } from './UserManagementHelper';
import { OwnershipService } from '@/services/ownership.service'; import { OwnershipService } from '@/services/ownership.service';
import Container from 'typedi';
import { RoleService } from '@/services/role.service'; import { RoleService } from '@/services/role.service';
import { UserRepository } from '@db/repositories/user.repository'; import { UserRepository } from '@db/repositories/user.repository';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
@Service()
export class PermissionChecker { export class PermissionChecker {
constructor(
private readonly userRepository: UserRepository,
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly roleService: RoleService,
private readonly ownershipService: OwnershipService,
) {}
/** /**
* Check if a user is permitted to execute a workflow. * Check if a user is permitted to execute a workflow.
*/ */
static async check(workflow: Workflow, userId: string) { async check(workflow: Workflow, userId: string) {
// allow if no nodes in this workflow use creds // allow if no nodes in this workflow use creds
const credIdsToNodes = PermissionChecker.mapCredIdsToNodes(workflow); const credIdsToNodes = this.mapCredIdsToNodes(workflow);
const workflowCredIds = Object.keys(credIdsToNodes); const workflowCredIds = Object.keys(credIdsToNodes);
@ -24,7 +34,7 @@ export class PermissionChecker {
// allow if requesting user is instance owner // allow if requesting user is instance owner
const user = await Container.get(UserRepository).findOneOrFail({ const user = await this.userRepository.findOneOrFail({
where: { id: userId }, where: { id: userId },
relations: ['globalRole'], relations: ['globalRole'],
}); });
@ -37,7 +47,7 @@ export class PermissionChecker {
let workflowUserIds = [userId]; let workflowUserIds = [userId];
if (workflow.id && isSharingEnabled()) { if (workflow.id && isSharingEnabled()) {
const workflowSharings = await Container.get(SharedWorkflowRepository).find({ const workflowSharings = await this.sharedWorkflowRepository.find({
relations: ['workflow'], relations: ['workflow'],
where: { workflowId: workflow.id }, where: { workflowId: workflow.id },
select: ['userId'], select: ['userId'],
@ -45,9 +55,9 @@ export class PermissionChecker {
workflowUserIds = workflowSharings.map((s) => s.userId); workflowUserIds = workflowSharings.map((s) => s.userId);
} }
const roleId = await Container.get(RoleService).findCredentialOwnerRoleId(); const roleId = await this.roleService.findCredentialOwnerRoleId();
const credentialSharings = await Container.get(SharedCredentialsRepository).findSharings( const credentialSharings = await this.sharedCredentialsRepository.findSharings(
workflowUserIds, workflowUserIds,
roleId, roleId,
); );
@ -68,7 +78,7 @@ export class PermissionChecker {
}); });
} }
static async checkSubworkflowExecutePolicy( async checkSubworkflowExecutePolicy(
subworkflow: Workflow, subworkflow: Workflow,
parentWorkflowId: string, parentWorkflowId: string,
node?: INode, node?: INode,
@ -94,11 +104,9 @@ export class PermissionChecker {
} }
const parentWorkflowOwner = const parentWorkflowOwner =
await Container.get(OwnershipService).getWorkflowOwnerCached(parentWorkflowId); await this.ownershipService.getWorkflowOwnerCached(parentWorkflowId);
const subworkflowOwner = await Container.get(OwnershipService).getWorkflowOwnerCached( const subworkflowOwner = await this.ownershipService.getWorkflowOwnerCached(subworkflow.id);
subworkflow.id,
);
const description = const description =
subworkflowOwner.id === parentWorkflowOwner.id subworkflowOwner.id === parentWorkflowOwner.id
@ -134,7 +142,7 @@ export class PermissionChecker {
} }
} }
private static mapCredIdsToNodes(workflow: Workflow) { private mapCredIdsToNodes(workflow: Workflow) {
return Object.values(workflow.nodes).reduce<{ [credentialId: string]: INode[] }>( return Object.values(workflow.nodes).reduce<{ [credentialId: string]: INode[] }>(
(map, node) => { (map, node) => {
if (node.disabled || !node.credentials) return map; if (node.disabled || !node.credentials) return map;

View file

@ -795,8 +795,8 @@ async function executeWorkflow(
let data; let data;
try { try {
await PermissionChecker.check(workflow, additionalData.userId); await Container.get(PermissionChecker).check(workflow, additionalData.userId);
await PermissionChecker.checkSubworkflowExecutePolicy( await Container.get(PermissionChecker).checkSubworkflowExecutePolicy(
workflow, workflow,
options.parentWorkflowId, options.parentWorkflowId,
options.node, options.node,

View file

@ -176,7 +176,7 @@ export async function executeErrorWorkflow(
const failedNode = workflowErrorData.execution?.lastNodeExecuted const failedNode = workflowErrorData.execution?.lastNodeExecuted
? workflowInstance.getNode(workflowErrorData.execution?.lastNodeExecuted) ? workflowInstance.getNode(workflowErrorData.execution?.lastNodeExecuted)
: undefined; : undefined;
await PermissionChecker.checkSubworkflowExecutePolicy( await Container.get(PermissionChecker).checkSubworkflowExecutePolicy(
workflowInstance, workflowInstance,
workflowErrorData.workflow.id!, workflowErrorData.workflow.id!,
failedNode ?? undefined, failedNode ?? undefined,

View file

@ -327,7 +327,7 @@ export class WorkflowRunner {
); );
try { try {
await PermissionChecker.check(workflow, data.userId); await Container.get(PermissionChecker).check(workflow, data.userId);
} catch (error) { } catch (error) {
ErrorReporter.error(error); ErrorReporter.error(error);
// Create a failed execution with the data for the node // Create a failed execution with the data for the node

View file

@ -142,7 +142,7 @@ class WorkflowRunnerProcess {
pinData: this.data.pinData, pinData: this.data.pinData,
}); });
try { try {
await PermissionChecker.check(this.workflow, userId); await Container.get(PermissionChecker).check(this.workflow, userId);
} catch (error) { } catch (error) {
const caughtError = error as NodeOperationError; const caughtError = error as NodeOperationError;
const failedExecutionData = generateFailedExecutionFromError( const failedExecutionData = generateFailedExecutionFromError(

View file

@ -185,7 +185,7 @@ export class Worker extends BaseCommand {
); );
try { try {
await PermissionChecker.check(workflow, workflowOwner.id); await Container.get(PermissionChecker).check(workflow, workflowOwner.id);
} catch (error) { } catch (error) {
if (error instanceof NodeOperationError) { if (error instanceof NodeOperationError) {
const failedExecution = generateFailedExecutionFromError( const failedExecution = generateFailedExecutionFromError(

View file

@ -1,15 +1,20 @@
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { Container } from 'typedi'; import { Container } from 'typedi';
import type { INodeTypes, WorkflowSettings } from 'n8n-workflow'; import type { WorkflowSettings } from 'n8n-workflow';
import { SubworkflowOperationError, Workflow } from 'n8n-workflow'; import { SubworkflowOperationError, Workflow } from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
import { User } from '@db/entities/User'; import { User } from '@db/entities/User';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { UserRepository } from '@/databases/repositories/user.repository';
import { generateNanoId } from '@/databases/utils/generators';
import { License } from '@/License';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
import { OwnershipService } from '@/services/ownership.service'; import { OwnershipService } from '@/services/ownership.service';
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
import { mockInstance } from '../shared/mocking'; import { mockInstance } from '../shared/mocking';
import { import {
@ -17,18 +22,13 @@ import {
randomName, randomName,
randomPositiveDigit, randomPositiveDigit,
} from '../integration/shared/random'; } from '../integration/shared/random';
import { LicenseMocker } from '../integration/shared/license';
import * as testDb from '../integration/shared/testDb'; import * as testDb from '../integration/shared/testDb';
import type { SaveCredentialFunction } from '../integration/shared/types'; import type { SaveCredentialFunction } from '../integration/shared/types';
import { mockNodeTypesData } from './Helpers'; import { mockNodeTypesData } from './Helpers';
import { affixRoleToSaveCredential } from '../integration/shared/db/credentials'; import { affixRoleToSaveCredential } from '../integration/shared/db/credentials';
import { getCredentialOwnerRole, getWorkflowOwnerRole } from '../integration/shared/db/roles'; import { getCredentialOwnerRole, getWorkflowOwnerRole } from '../integration/shared/db/roles';
import { createOwner, createUser } from '../integration/shared/db/users'; 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';
export const toTargetCallErrorMsg = (subworkflowId: string) => export const toTargetCallErrorMsg = (subworkflowId: string) =>
`Target workflow ID ${subworkflowId} may not be called`; `Target workflow ID ${subworkflowId} may not be called`;
@ -71,24 +71,26 @@ export function createSubworkflow({
}); });
} }
let mockNodeTypes: INodeTypes;
let credentialOwnerRole: Role; let credentialOwnerRole: Role;
let workflowOwnerRole: Role; let workflowOwnerRole: Role;
let saveCredential: SaveCredentialFunction; let saveCredential: SaveCredentialFunction;
const mockNodeTypes = mockInstance(NodeTypes);
mockInstance(LoadNodesAndCredentials, { mockInstance(LoadNodesAndCredentials, {
loadedNodes: mockNodeTypesData(['start', 'actionNetwork']), loadedNodes: mockNodeTypesData(['start', 'actionNetwork']),
}); });
let permissionChecker: PermissionChecker;
beforeAll(async () => { beforeAll(async () => {
await testDb.init(); await testDb.init();
mockNodeTypes = Container.get(NodeTypes);
credentialOwnerRole = await getCredentialOwnerRole(); credentialOwnerRole = await getCredentialOwnerRole();
workflowOwnerRole = await getWorkflowOwnerRole(); workflowOwnerRole = await getWorkflowOwnerRole();
saveCredential = affixRoleToSaveCredential(credentialOwnerRole); saveCredential = affixRoleToSaveCredential(credentialOwnerRole);
permissionChecker = Container.get(PermissionChecker);
}); });
describe('check()', () => { describe('check()', () => {
@ -121,7 +123,7 @@ describe('check()', () => {
], ],
}); });
expect(async () => PermissionChecker.check(workflow, userId)).not.toThrow(); expect(async () => permissionChecker.check(workflow, userId)).not.toThrow();
}); });
test('should allow if requesting user is instance owner', async () => { test('should allow if requesting user is instance owner', async () => {
@ -151,7 +153,7 @@ describe('check()', () => {
], ],
}); });
expect(async () => PermissionChecker.check(workflow, owner.id)).not.toThrow(); expect(async () => permissionChecker.check(workflow, owner.id)).not.toThrow();
}); });
test('should allow if workflow creds are valid subset', async () => { test('should allow if workflow creds are valid subset', async () => {
@ -198,7 +200,7 @@ describe('check()', () => {
], ],
}); });
expect(async () => PermissionChecker.check(workflow, owner.id)).not.toThrow(); expect(async () => permissionChecker.check(workflow, owner.id)).not.toThrow();
}); });
test('should deny if workflow creds are not valid subset', async () => { test('should deny if workflow creds are not valid subset', async () => {
@ -254,7 +256,7 @@ describe('check()', () => {
const workflow = new Workflow(workflowDetails); const workflow = new Workflow(workflowDetails);
await expect(PermissionChecker.check(workflow, member.id)).rejects.toThrow(); await expect(permissionChecker.check(workflow, member.id)).rejects.toThrow();
}); });
}); });
@ -278,7 +280,7 @@ describe('checkSubworkflowExecutePolicy()', () => {
ownershipService.getWorkflowOwnerCached.mockResolvedValue(new User()); ownershipService.getWorkflowOwnerCached.mockResolvedValue(new User());
const check = PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id); const check = permissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id);
await expect(check).rejects.toThrow(toTargetCallErrorMsg(subworkflow.id)); await expect(check).rejects.toThrow(toTargetCallErrorMsg(subworkflow.id));
@ -299,12 +301,12 @@ describe('checkSubworkflowExecutePolicy()', () => {
ownershipService.getWorkflowOwnerCached.mockResolvedValueOnce(firstUser); // parent workflow ownershipService.getWorkflowOwnerCached.mockResolvedValueOnce(firstUser); // parent workflow
ownershipService.getWorkflowOwnerCached.mockResolvedValueOnce(secondUser); // subworkflow ownershipService.getWorkflowOwnerCached.mockResolvedValueOnce(secondUser); // subworkflow
const check = PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id); const check = permissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id);
await expect(check).rejects.toThrow(toTargetCallErrorMsg(subworkflow.id)); await expect(check).rejects.toThrow(toTargetCallErrorMsg(subworkflow.id));
try { try {
await PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, uuid()); await permissionChecker.checkSubworkflowExecutePolicy(subworkflow, uuid());
} catch (error) { } catch (error) {
if (error instanceof SubworkflowOperationError) { if (error instanceof SubworkflowOperationError) {
expect(error.description).toBe( expect(error.description).toBe(
@ -326,7 +328,7 @@ describe('checkSubworkflowExecutePolicy()', () => {
callerIds: `123,456,bcdef, ${parentWorkflow.id}`, callerIds: `123,456,bcdef, ${parentWorkflow.id}`,
}); });
const check = PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id); const check = permissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id);
await expect(check).resolves.not.toThrow(); await expect(check).resolves.not.toThrow();
}); });
@ -339,7 +341,7 @@ describe('checkSubworkflowExecutePolicy()', () => {
callerIds: 'xyz', callerIds: 'xyz',
}); });
const check = PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id); const check = permissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id);
await expect(check).rejects.toThrow(); await expect(check).rejects.toThrow();
}); });
@ -351,7 +353,7 @@ describe('checkSubworkflowExecutePolicy()', () => {
const subworkflow = createSubworkflow({ policy: 'any' }); const subworkflow = createSubworkflow({ policy: 'any' });
ownershipService.getWorkflowOwnerCached.mockResolvedValue(new User()); ownershipService.getWorkflowOwnerCached.mockResolvedValue(new User());
const check = PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id); const check = permissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id);
await expect(check).resolves.not.toThrow(); await expect(check).resolves.not.toThrow();
}); });
@ -367,7 +369,7 @@ describe('checkSubworkflowExecutePolicy()', () => {
const subworkflow = createSubworkflow({ policy: 'workflowsFromSameOwner' }); const subworkflow = createSubworkflow({ policy: 'workflowsFromSameOwner' });
const check = PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, uuid()); const check = permissionChecker.checkSubworkflowExecutePolicy(subworkflow, uuid());
await expect(check).rejects.toThrow(toTargetCallErrorMsg(subworkflow.id)); await expect(check).rejects.toThrow(toTargetCallErrorMsg(subworkflow.id));
}); });
@ -382,7 +384,7 @@ describe('checkSubworkflowExecutePolicy()', () => {
const subworkflow = createSubworkflow({ policy: 'workflowsFromSameOwner' }); const subworkflow = createSubworkflow({ policy: 'workflowsFromSameOwner' });
const check = PermissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id); const check = permissionChecker.checkSubworkflowExecutePolicy(subworkflow, parentWorkflow.id);
await expect(check).resolves.not.toThrow(); await expect(check).resolves.not.toThrow();
}); });