mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
refactor(core): Use DI in PermissionChecker (no-changelog) (#8344)
This commit is contained in:
parent
420b4271a9
commit
64ceb16af6
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue