diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index ca5b9996e..574883989 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -52,7 +52,6 @@ import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData' import config from '@/config'; import { User } from '@db/entities/User'; -import { whereClause } from '@/WorkflowHelpers'; import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import * as ActiveExecutions from '@/ActiveExecutions'; import { createErrorExecution } from '@/GenericHelpers'; @@ -60,6 +59,7 @@ import { WORKFLOW_REACTIVATE_INITIAL_TIMEOUT, WORKFLOW_REACTIVATE_MAX_TIMEOUT } import { NodeTypes } from '@/NodeTypes'; import { WorkflowRunner } from '@/WorkflowRunner'; import { ExternalHooks } from '@/ExternalHooks'; +import { whereClause } from './UserManagement/UserManagementHelper'; const WEBHOOK_PROD_UNREGISTERED_HINT = `The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)`; diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index 3445546af..bdc5b98f4 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -43,13 +43,14 @@ import { } from 'n8n-workflow'; import * as Db from '@/Db'; -import { ICredentialsDb, WhereClause } from '@/Interfaces'; +import { ICredentialsDb } from '@/Interfaces'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; import { User } from '@db/entities/User'; import { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { NodeTypes } from '@/NodeTypes'; import { CredentialsOverwrites } from '@/CredentialsOverwrites'; import { CredentialTypes } from '@/CredentialTypes'; +import { whereClause } from './UserManagement/UserManagementHelper'; const mockNodeTypes: INodeTypes = { nodeTypes: {} as INodeTypeData, @@ -738,28 +739,6 @@ export class CredentialsHelper extends ICredentialsHelper { } } -/** - * Build a `where` clause for a `find()` or `findOne()` operation - * in the `shared_workflow` or `shared_credentials` tables. - */ -export function whereClause({ - user, - entityType, - entityId = '', -}: { - user: User; - entityType: 'workflow' | 'credentials'; - entityId?: string; -}): WhereClause { - const where: WhereClause = entityId ? { [entityType]: { id: entityId } } : {}; - - if (user.globalRole.name !== 'owner') { - where.user = { id: user.id }; - } - - return where; -} - /** * Get a credential if it has been shared with a user. */ diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index aef7140d7..59ae83395 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -24,7 +24,7 @@ import { WorkflowExecute } from 'n8n-core'; // eslint-disable-next-line import/no-extraneous-dependencies import PCancelable from 'p-cancelable'; -import { Repository } from 'typeorm'; +import type { FindOperator, Repository } from 'typeorm'; import { ChildProcess } from 'child_process'; import { Url } from 'url'; @@ -710,7 +710,7 @@ export interface IWorkflowExecuteProcess { workflowExecute: WorkflowExecute; } -export type WhereClause = Record; +export type WhereClause = Record }>; // ---------------------------------- // community nodes diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index aa9c265f9..810d028b2 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -89,7 +89,7 @@ import * as Queue from '@/Queue'; import { InternalHooksManager } from '@/InternalHooksManager'; import { getCredentialTranslationPath } from '@/TranslationHelpers'; import { WEBHOOK_METHODS } from '@/WebhookHelpers'; -import { getSharedWorkflowIds, whereClause } from '@/WorkflowHelpers'; +import { getSharedWorkflowIds } from '@/WorkflowHelpers'; import { nodesController } from '@/api/nodes.api'; import { workflowsController } from '@/workflows/workflows.controller'; @@ -122,6 +122,7 @@ import { isEmailSetUp, isSharingEnabled, isUserManagementEnabled, + whereClause, } from '@/UserManagement/UserManagementHelper'; import * as Db from '@/Db'; import { diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 98cec9ce5..2cefec4da 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -13,6 +13,7 @@ import { Role } from '@db/entities/Role'; import { AuthenticatedRequest } from '@/requests'; import config from '@/config'; import { getWebhookBaseUrl } from '../WebhookHelpers'; +import { WhereClause } from '@/Interfaces'; export async function getWorkflowOwner(workflowId: string | number): Promise { const sharedWorkflow = await Db.collections.SharedWorkflow.findOneOrFail({ @@ -210,3 +211,31 @@ export function rightDiff( return acc; }, []); } + +/** + * Build a `where` clause for a TypeORM entity search, + * checking for member access if the user is not an owner. + */ +export function whereClause({ + user, + entityType, + entityId = '', + roles = [], +}: { + user: User; + entityType: 'workflow' | 'credentials'; + entityId?: string; + roles?: string[]; +}): WhereClause { + const where: WhereClause = entityId ? { [entityType]: { id: entityId } } : {}; + + // TODO: Decide if owner access should be restricted + if (user.globalRole.name !== 'owner') { + where.user = { id: user.id }; + if (roles?.length) { + where.role = { name: In(roles) }; + } + } + + return where; +} diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 3e72eb5ed..83da7da11 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -21,7 +21,6 @@ import { IDataObject, IExecuteData, IExecuteWorkflowInfo, - INode, INodeExecutionData, INodeParameters, IRun, @@ -62,7 +61,7 @@ import * as Push from '@/Push'; import * as ResponseHelper from '@/ResponseHelper'; import * as WebhookHelpers from '@/WebhookHelpers'; import * as WorkflowHelpers from '@/WorkflowHelpers'; -import { getUserById, getWorkflowOwner } from '@/UserManagement/UserManagementHelper'; +import { getUserById, getWorkflowOwner, whereClause } from '@/UserManagement/UserManagementHelper'; import { findSubworkflowStart } from '@/utils'; import { PermissionChecker } from './UserManagement/PermissionChecker'; @@ -852,7 +851,7 @@ export async function getWorkflowData( const shared = await Db.collections.SharedWorkflow.findOne({ relations, - where: WorkflowHelpers.whereClause({ + where: whereClause({ user, entityType: 'workflow', entityId: workflowInfo.id, diff --git a/packages/cli/src/WorkflowHelpers.ts b/packages/cli/src/WorkflowHelpers.ts index a644e115d..f7902f492 100644 --- a/packages/cli/src/WorkflowHelpers.ts +++ b/packages/cli/src/WorkflowHelpers.ts @@ -33,7 +33,6 @@ import { ITransferNodeTypes, IWorkflowErrorData, IWorkflowExecutionDataProcess, - WhereClause, } from '@/Interfaces'; import { NodeTypes } from '@/NodeTypes'; import { WorkflowRunner } from '@/WorkflowRunner'; @@ -41,7 +40,7 @@ import { WorkflowRunner } from '@/WorkflowRunner'; import config from '@/config'; import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { User } from '@db/entities/User'; -import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper'; +import { getWorkflowOwner, whereClause } from '@/UserManagement/UserManagementHelper'; const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType'); @@ -573,40 +572,14 @@ export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promi return workflow; } -/** - * Build a `where` clause for a TypeORM entity search, - * checking for member access if the user is not an owner. - */ -export function whereClause({ - user, - entityType, - entityId = '', -}: { - user: User; - entityType: 'workflow' | 'credentials'; - entityId?: string; -}): WhereClause { - const where: WhereClause = entityId ? { [entityType]: { id: entityId } } : {}; - - // TODO: Decide if owner access should be restricted - if (user.globalRole.name !== 'owner') { - where.user = { id: user.id }; - } - - return where; -} - /** * Get the IDs of the workflows that have been shared with the user. * Returns all IDs if user is global owner (see `whereClause`) */ -export async function getSharedWorkflowIds(user: User): Promise { +export async function getSharedWorkflowIds(user: User, roles?: string[]): Promise { const sharedWorkflows = await Db.collections.SharedWorkflow.find({ - relations: ['workflow'], - where: whereClause({ - user, - entityType: 'workflow', - }), + relations: ['workflow', 'role'], + where: whereClause({ user, entityType: 'workflow', roles }), }); return sharedWorkflows.map(({ workflow }) => workflow.id); diff --git a/packages/cli/src/workflows/workflows.controller.ee.ts b/packages/cli/src/workflows/workflows.controller.ee.ts index d7125c622..03894b448 100644 --- a/packages/cli/src/workflows/workflows.controller.ee.ts +++ b/packages/cli/src/workflows/workflows.controller.ee.ts @@ -14,7 +14,6 @@ import { SharedWorkflow } from '@db/entities/SharedWorkflow'; import { LoggerProxy } from 'n8n-workflow'; import * as TagHelpers from '@/TagHelpers'; import { EECredentialsService as EECredentials } from '../credentials/credentials.service.ee'; -import { WorkflowsService } from './workflows.services'; import { IExecutionPushResponse } from '@/Interfaces'; import * as GenericHelpers from '@/GenericHelpers'; @@ -197,7 +196,7 @@ EEWorkflowController.post( EEWorkflowController.get( '/', ResponseHelper.send(async (req: WorkflowRequest.GetAll) => { - const workflows = (await WorkflowsService.getMany( + const workflows = (await EEWorkflows.getMany( req.user, req.query.filter, )) as unknown as WorkflowEntity[]; @@ -222,7 +221,7 @@ EEWorkflowController.patch( const safeWorkflow = await EEWorkflows.preventTampering(updateData, workflowId, req.user); - const updatedWorkflow = await WorkflowsService.update( + const updatedWorkflow = await EEWorkflows.update( req.user, safeWorkflow, workflowId, @@ -256,6 +255,6 @@ EEWorkflowController.post( req.body.workflowData.nodes = safeWorkflow.nodes; - return WorkflowsService.runManually(req.body, req.user, GenericHelpers.getSessionId(req)); + return EEWorkflows.runManually(req.body, req.user, GenericHelpers.getSessionId(req)); }), ); diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 663f71b60..aef763854 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -9,7 +9,6 @@ import * as Db from '@/Db'; import * as GenericHelpers from '@/GenericHelpers'; import * as ResponseHelper from '@/ResponseHelper'; import * as WorkflowHelpers from '@/WorkflowHelpers'; -import { whereClause } from '@/CredentialsHelper'; import { IWorkflowResponse, IExecutionPushResponse } from '@/Interfaces'; import config from '@/config'; import * as TagHelpers from '@/TagHelpers'; @@ -23,6 +22,7 @@ import type { WorkflowRequest } from '@/requests'; import { isBelowOnboardingThreshold } from '@/WorkflowHelpers'; import { EEWorkflowController } from './workflows.controller.ee'; import { WorkflowsService } from './workflows.services'; +import { whereClause } from '@/UserManagement/UserManagementHelper'; export const workflowsController = express.Router(); @@ -201,7 +201,7 @@ workflowsController.get( ResponseHelper.send(async (req: WorkflowRequest.Get) => { const { id: workflowId } = req.params; - let relations = ['workflow', 'workflow.tags']; + let relations = ['workflow', 'workflow.tags', 'role']; if (config.getEnv('workflowTagsDisabled')) { relations = relations.filter((relation) => relation !== 'workflow.tags'); @@ -213,6 +213,7 @@ workflowsController.get( user: req.user, entityType: 'workflow', entityId: workflowId, + roles: ['owner'], }), }); @@ -252,7 +253,14 @@ workflowsController.patch( const { tags, ...rest } = req.body; Object.assign(updateData, rest); - const updatedWorkflow = await WorkflowsService.update(req.user, updateData, workflowId, tags); + const updatedWorkflow = await WorkflowsService.update( + req.user, + updateData, + workflowId, + tags, + false, + ['owner'], + ); const { id, ...remainder } = updatedWorkflow; @@ -275,11 +283,12 @@ workflowsController.delete( await externalHooks.run('workflow.delete', [workflowId]); const shared = await Db.collections.SharedWorkflow.findOne({ - relations: ['workflow'], + relations: ['workflow', 'role'], where: whereClause({ user: req.user, entityType: 'workflow', entityId: workflowId, + roles: ['owner'], }), }); diff --git a/packages/cli/src/workflows/workflows.services.ee.ts b/packages/cli/src/workflows/workflows.services.ee.ts index 0ff086c86..7b0a3afac 100644 --- a/packages/cli/src/workflows/workflows.services.ee.ts +++ b/packages/cli/src/workflows/workflows.services.ee.ts @@ -11,8 +11,14 @@ import { UserService } from '@/user/user.service'; import { WorkflowsService } from './workflows.services'; import type { WorkflowWithSharingsAndCredentials } from './workflows.types'; import { EECredentialsService as EECredentials } from '@/credentials/credentials.service.ee'; +import { getSharedWorkflowIds } from '@/WorkflowHelpers'; export class EEWorkflowsService extends WorkflowsService { + static async getWorkflowIdsForUser(user: User) { + // Get all workflows regardless of role + return getSharedWorkflowIds(user); + } + static async isOwned( user: User, workflowId: string, diff --git a/packages/cli/src/workflows/workflows.services.ts b/packages/cli/src/workflows/workflows.services.ts index c940fa1f9..15add92f4 100644 --- a/packages/cli/src/workflows/workflows.services.ts +++ b/packages/cli/src/workflows/workflows.services.ts @@ -6,7 +6,6 @@ import * as Db from '@/Db'; import { InternalHooksManager } from '@/InternalHooksManager'; import * as ResponseHelper from '@/ResponseHelper'; import * as WorkflowHelpers from '@/WorkflowHelpers'; -import { whereClause } from '@/CredentialsHelper'; import config from '@/config'; import { SharedWorkflow } from '@db/entities/SharedWorkflow'; import { User } from '@db/entities/User'; @@ -21,6 +20,7 @@ import { WorkflowRunner } from '@/WorkflowRunner'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; import * as TestWebhooks from '@/TestWebhooks'; import { getSharedWorkflowIds } from '@/WorkflowHelpers'; +import { whereClause } from '@/UserManagement/UserManagementHelper'; export interface IGetWorkflowsQueryFilter { id?: number | string; @@ -93,8 +93,13 @@ export class WorkflowsService { return Db.collections.Workflow.findOne(workflow, options); } + // Warning: this function is overriden by EE to disregard role list. + static async getWorkflowIdsForUser(user: User, roles?: string[]) { + return getSharedWorkflowIds(user, roles); + } + static async getMany(user: User, rawFilter: string) { - const sharedWorkflowIds = await getSharedWorkflowIds(user); + const sharedWorkflowIds = await this.getWorkflowIdsForUser(user, ['owner']); if (sharedWorkflowIds.length === 0) { // return early since without shared workflows there can be no hits // (note: getSharedWorkflowIds() returns _all_ workflow ids for global owners) @@ -172,15 +177,16 @@ export class WorkflowsService { workflow: WorkflowEntity, workflowId: string, tags?: string[], - // eslint-disable-next-line @typescript-eslint/no-unused-vars forceSave?: boolean, + roles?: string[], ): Promise { const shared = await Db.collections.SharedWorkflow.findOne({ - relations: ['workflow'], + relations: ['workflow', 'role'], where: whereClause({ user, entityType: 'workflow', entityId: workflowId, + roles, }), });