n8n/packages/cli/src/UserManagement/PermissionChecker.ts

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;
},
{},
);
}
}