mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-30 15:07:28 -08:00
172 lines
5.4 KiB
TypeScript
172 lines
5.4 KiB
TypeScript
import type { INode, Workflow } from 'n8n-workflow';
|
|
import {
|
|
NodeOperationError,
|
|
SubworkflowOperationError,
|
|
WorkflowOperationError,
|
|
} 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';
|
|
|
|
export class PermissionChecker {
|
|
/**
|
|
* Check if a user is permitted to execute a workflow.
|
|
*/
|
|
static async check(workflow: Workflow, userId: string) {
|
|
// allow if no nodes in this workflow use creds
|
|
|
|
const credIdsToNodes = PermissionChecker.mapCredIdsToNodes(workflow);
|
|
|
|
const workflowCredIds = Object.keys(credIdsToNodes);
|
|
|
|
if (workflowCredIds.length === 0) return;
|
|
|
|
// allow if requesting user is instance owner
|
|
|
|
const user = await Db.collections.User.findOneOrFail({
|
|
where: { id: userId },
|
|
relations: ['globalRole'],
|
|
});
|
|
|
|
if (user.globalRole.name === 'owner') return;
|
|
|
|
// allow if all creds used in this workflow are a subset of
|
|
// all creds accessible to users who have access to this workflow
|
|
|
|
let workflowUserIds = [userId];
|
|
|
|
if (workflow.id && isSharingEnabled()) {
|
|
const workflowSharings = await Db.collections.SharedWorkflow.find({
|
|
relations: ['workflow'],
|
|
where: { workflowId: workflow.id },
|
|
select: ['userId'],
|
|
});
|
|
workflowUserIds = workflowSharings.map((s) => s.userId);
|
|
}
|
|
|
|
const credentialsWhere: FindOptionsWhere<SharedCredentials> = { userId: In(workflowUserIds) };
|
|
|
|
if (!isSharingEnabled()) {
|
|
const role = await Container.get(RoleService).findCredentialOwnerRole();
|
|
// If credential sharing is not enabled, get only credentials owned by this user
|
|
credentialsWhere.roleId = role.id;
|
|
}
|
|
|
|
const credentialSharings = await Db.collections.SharedCredentials.find({
|
|
where: credentialsWhere,
|
|
});
|
|
|
|
const accessibleCredIds = credentialSharings.map((s) => s.credentialsId);
|
|
|
|
const inaccessibleCredIds = workflowCredIds.filter((id) => !accessibleCredIds.includes(id));
|
|
|
|
if (inaccessibleCredIds.length === 0) return;
|
|
|
|
// if disallowed, flag only first node using first inaccessible cred
|
|
|
|
const nodeToFlag = credIdsToNodes[inaccessibleCredIds[0]][0];
|
|
|
|
throw new NodeOperationError(nodeToFlag, 'Node has no access to credential', {
|
|
description: 'Please recreate the credential or ask its owner to share it with you.',
|
|
severity: 'warning',
|
|
});
|
|
}
|
|
|
|
static async checkSubworkflowExecutePolicy(
|
|
subworkflow: Workflow,
|
|
userId: string,
|
|
parentWorkflowId?: string,
|
|
) {
|
|
/**
|
|
* Important considerations: both the current workflow and the parent can have empty IDs.
|
|
* This happens when a user is executing an unsaved workflow manually running a workflow
|
|
* loaded from a file or code, for instance.
|
|
* This is an important topic to keep in mind for all security checks
|
|
*/
|
|
if (!subworkflow.id) {
|
|
// It's a workflow from code and not loaded from DB
|
|
// No checks are necessary since it doesn't have any sort of settings
|
|
return;
|
|
}
|
|
|
|
let policy =
|
|
subworkflow.settings?.callerPolicy ?? config.getEnv('workflows.callerPolicyDefaultOption');
|
|
|
|
if (!isSharingEnabled()) {
|
|
// Community version allows only same owner workflows
|
|
policy = 'workflowsFromSameOwner';
|
|
}
|
|
|
|
const subworkflowOwner = await Container.get(OwnershipService).getWorkflowOwnerCached(
|
|
subworkflow.id,
|
|
);
|
|
|
|
const errorToThrow = new SubworkflowOperationError(
|
|
`Target workflow ID ${subworkflow.id ?? ''} may not be called`,
|
|
subworkflowOwner.id === userId
|
|
? 'Change the settings of the sub-workflow so it can be called by this one.'
|
|
: `${subworkflowOwner.firstName} (${subworkflowOwner.email}) can make this change. You may need to tell them the ID of this workflow, which is ${subworkflow.id}`,
|
|
);
|
|
|
|
if (policy === 'none') {
|
|
throw errorToThrow;
|
|
}
|
|
|
|
if (policy === 'workflowsFromAList') {
|
|
if (parentWorkflowId === undefined) {
|
|
throw errorToThrow;
|
|
}
|
|
const allowedCallerIds = subworkflow.settings.callerIds
|
|
?.split(',')
|
|
.map((id) => id.trim())
|
|
.filter((id) => id !== '');
|
|
|
|
if (!allowedCallerIds?.includes(parentWorkflowId)) {
|
|
throw errorToThrow;
|
|
}
|
|
}
|
|
|
|
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') {
|
|
throw errorToThrow;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static mapCredIdsToNodes(workflow: Workflow) {
|
|
return Object.values(workflow.nodes).reduce<{ [credentialId: string]: INode[] }>(
|
|
(map, node) => {
|
|
if (node.disabled || !node.credentials) return map;
|
|
|
|
Object.values(node.credentials).forEach((cred) => {
|
|
if (!cred.id) {
|
|
throw new NodeOperationError(node, 'Node uses invalid credential', {
|
|
description: 'Please recreate the credential.',
|
|
});
|
|
}
|
|
|
|
map[cred.id] = map[cred.id] ? [...map[cred.id], node] : [node];
|
|
});
|
|
|
|
return map;
|
|
},
|
|
{},
|
|
);
|
|
}
|
|
}
|