diff --git a/packages/cli/src/workflows/workflows.controller.ee.ts b/packages/cli/src/workflows/workflows.controller.ee.ts deleted file mode 100644 index 6026b9a034..0000000000 --- a/packages/cli/src/workflows/workflows.controller.ee.ts +++ /dev/null @@ -1,315 +0,0 @@ -import express from 'express'; -import { v4 as uuid } from 'uuid'; -import * as Db from '@/Db'; -import * as ResponseHelper from '@/ResponseHelper'; -import * as WorkflowHelpers from '@/WorkflowHelpers'; -import config from '@/config'; -import { WorkflowEntity } from '@db/entities/WorkflowEntity'; -import { validateEntity } from '@/GenericHelpers'; -import type { ListQuery, WorkflowRequest } from '@/requests'; -import { isSharingEnabled, rightDiff } from '@/UserManagement/UserManagementHelper'; -import { EnterpriseWorkflowService } from './workflow.service.ee'; -import { ExternalHooks } from '@/ExternalHooks'; -import { SharedWorkflow } from '@db/entities/SharedWorkflow'; -import { CredentialsService } from '../credentials/credentials.service'; -import type { IExecutionPushResponse } from '@/Interfaces'; -import * as GenericHelpers from '@/GenericHelpers'; -import { Container } from 'typedi'; -import { InternalHooks } from '@/InternalHooks'; -import { RoleService } from '@/services/role.service'; -import * as utils from '@/utils'; -import { listQueryMiddleware } from '@/middlewares'; -import { TagService } from '@/services/tag.service'; -import { Logger } from '@/Logger'; -import { WorkflowHistoryService } from './workflowHistory/workflowHistory.service.ee'; -import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; -import { BadRequestError } from '@/errors/response-errors/bad-request.error'; -import { NotFoundError } from '@/errors/response-errors/not-found.error'; -import { InternalServerError } from '@/errors/response-errors/internal-server.error'; -import { WorkflowService } from './workflow.service'; -import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; -import { TagRepository } from '@/databases/repositories/tag.repository'; - -export const EEWorkflowController = express.Router(); - -EEWorkflowController.use((req, res, next) => { - if (!isSharingEnabled()) { - // skip ee router and use free one - next('router'); - return; - } - // use ee router - next(); -}); - -/** - * (EE) PUT /workflows/:id/share - * - * Grant or remove users' access to a workflow. - */ - -EEWorkflowController.put( - '/:workflowId/share', - ResponseHelper.send(async (req: WorkflowRequest.Share) => { - const { workflowId } = req.params; - const { shareWithIds } = req.body; - - if ( - !Array.isArray(shareWithIds) || - !shareWithIds.every((userId) => typeof userId === 'string') - ) { - throw new BadRequestError('Bad request'); - } - - const isOwnedRes = await Container.get(EnterpriseWorkflowService).isOwned(req.user, workflowId); - const { ownsWorkflow } = isOwnedRes; - let { workflow } = isOwnedRes; - - if (!ownsWorkflow || !workflow) { - workflow = undefined; - // Allow owners/admins to share - if (req.user.hasGlobalScope('workflow:share')) { - const sharedRes = await Container.get(WorkflowService).getSharing(req.user, workflowId, { - allowGlobalScope: true, - globalScope: 'workflow:share', - }); - workflow = sharedRes?.workflow; - } - if (!workflow) { - throw new UnauthorizedError('Forbidden'); - } - } - - const ownerIds = ( - await Container.get(WorkflowRepository).getSharings( - Db.getConnection().createEntityManager(), - workflowId, - ['shared', 'shared.role'], - ) - ) - .filter((e) => e.role.name === 'owner') - .map((e) => e.userId); - - let newShareeIds: string[] = []; - await Db.transaction(async (trx) => { - // remove all sharings that are not supposed to exist anymore - await Container.get(WorkflowRepository).pruneSharings(trx, workflowId, [ - ...ownerIds, - ...shareWithIds, - ]); - - const sharings = await Container.get(WorkflowRepository).getSharings(trx, workflowId); - - // extract the new sharings that need to be added - newShareeIds = rightDiff( - [sharings, (sharing) => sharing.userId], - [shareWithIds, (shareeId) => shareeId], - ); - - if (newShareeIds.length) { - await Container.get(EnterpriseWorkflowService).share(trx, workflow!, newShareeIds); - } - }); - - void Container.get(InternalHooks).onWorkflowSharingUpdate( - workflowId, - req.user.id, - shareWithIds, - ); - }), -); - -EEWorkflowController.get( - '/:id(\\w+)', - (req, res, next) => (req.params.id === 'new' ? next('router') : next()), // skip ee router and use free one for naming - ResponseHelper.send(async (req: WorkflowRequest.Get) => { - const { id: workflowId } = req.params; - - const relations = ['shared', 'shared.user', 'shared.role']; - if (!config.getEnv('workflowTagsDisabled')) { - relations.push('tags'); - } - - const workflow = await Container.get(WorkflowRepository).get({ id: workflowId }, { relations }); - - if (!workflow) { - throw new NotFoundError(`Workflow with ID "${workflowId}" does not exist`); - } - - const userSharing = workflow.shared?.find((shared) => shared.user.id === req.user.id); - if (!userSharing && !req.user.hasGlobalScope('workflow:read')) { - throw new UnauthorizedError( - 'You do not have permission to access this workflow. Ask the owner to share it with you', - ); - } - - const enterpriseWorkflowService = Container.get(EnterpriseWorkflowService); - - enterpriseWorkflowService.addOwnerAndSharings(workflow); - await enterpriseWorkflowService.addCredentialsToWorkflow(workflow, req.user); - return workflow; - }), -); - -EEWorkflowController.post( - '/', - ResponseHelper.send(async (req: WorkflowRequest.Create) => { - delete req.body.id; // delete if sent - - const newWorkflow = new WorkflowEntity(); - - Object.assign(newWorkflow, req.body); - - newWorkflow.versionId = uuid(); - - await validateEntity(newWorkflow); - - await Container.get(ExternalHooks).run('workflow.create', [newWorkflow]); - - const { tags: tagIds } = req.body; - - if (tagIds?.length && !config.getEnv('workflowTagsDisabled')) { - newWorkflow.tags = await Container.get(TagRepository).findMany(tagIds); - } - - await WorkflowHelpers.replaceInvalidCredentials(newWorkflow); - - WorkflowHelpers.addNodeIds(newWorkflow); - - // This is a new workflow, so we simply check if the user has access to - // all used workflows - - const allCredentials = await CredentialsService.getMany(req.user); - - try { - Container.get(EnterpriseWorkflowService).validateCredentialPermissionsToUser( - newWorkflow, - allCredentials, - ); - } catch (error) { - throw new BadRequestError( - 'The workflow you are trying to save contains credentials that are not shared with you', - ); - } - - let savedWorkflow: undefined | WorkflowEntity; - - await Db.transaction(async (transactionManager) => { - savedWorkflow = await transactionManager.save(newWorkflow); - - const role = await Container.get(RoleService).findWorkflowOwnerRole(); - - const newSharedWorkflow = new SharedWorkflow(); - - Object.assign(newSharedWorkflow, { - role, - user: req.user, - workflow: savedWorkflow, - }); - - await transactionManager.save(newSharedWorkflow); - }); - - if (!savedWorkflow) { - Container.get(Logger).error('Failed to create workflow', { userId: req.user.id }); - throw new InternalServerError( - 'An error occurred while saving your workflow. Please try again.', - ); - } - - await Container.get(WorkflowHistoryService).saveVersion( - req.user, - savedWorkflow, - savedWorkflow.id, - ); - - if (tagIds && !config.getEnv('workflowTagsDisabled') && savedWorkflow.tags) { - savedWorkflow.tags = Container.get(TagService).sortByRequestOrder(savedWorkflow.tags, { - requestOrder: tagIds, - }); - } - - await Container.get(ExternalHooks).run('workflow.afterCreate', [savedWorkflow]); - void Container.get(InternalHooks).onWorkflowCreated(req.user, newWorkflow, false); - - return savedWorkflow; - }), -); - -/** - * (EE) GET /workflows - */ -EEWorkflowController.get( - '/', - listQueryMiddleware, - async (req: ListQuery.Request, res: express.Response) => { - try { - const sharedWorkflowIds = await WorkflowHelpers.getSharedWorkflowIds(req.user); - - const { workflows: data, count } = await Container.get(WorkflowService).getMany( - sharedWorkflowIds, - req.listQueryOptions, - ); - - res.json({ count, data }); - } catch (maybeError) { - const error = utils.toError(maybeError); - ResponseHelper.reportError(error); - ResponseHelper.sendErrorResponse(res, error); - } - }, -); - -EEWorkflowController.patch( - '/:id(\\w+)', - ResponseHelper.send(async (req: WorkflowRequest.Update) => { - const { id: workflowId } = req.params; - const forceSave = req.query.forceSave === 'true'; - - const updateData = new WorkflowEntity(); - const { tags, ...rest } = req.body; - Object.assign(updateData, rest); - - const safeWorkflow = await Container.get(EnterpriseWorkflowService).preventTampering( - updateData, - workflowId, - req.user, - ); - - const updatedWorkflow = await Container.get(WorkflowService).update( - req.user, - safeWorkflow, - workflowId, - tags, - forceSave, - ); - - return updatedWorkflow; - }), -); - -/** - * (EE) POST /workflows/run - */ -EEWorkflowController.post( - '/run', - ResponseHelper.send(async (req: WorkflowRequest.ManualRun): Promise => { - const workflow = new WorkflowEntity(); - Object.assign(workflow, req.body.workflowData); - - if (req.body.workflowData.id !== undefined) { - const safeWorkflow = await Container.get(EnterpriseWorkflowService).preventTampering( - workflow, - workflow.id, - req.user, - ); - req.body.workflowData.nodes = safeWorkflow.nodes; - } - - return Container.get(WorkflowService).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 b2ec5929f9..1443387b03 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -14,8 +14,8 @@ import { validateEntity } from '@/GenericHelpers'; import { ExternalHooks } from '@/ExternalHooks'; import type { ListQuery, WorkflowRequest } from '@/requests'; import { isBelowOnboardingThreshold } from '@/WorkflowHelpers'; -import { EEWorkflowController } from './workflows.controller.ee'; import { WorkflowService } from './workflow.service'; +import { isSharingEnabled, rightDiff } from '@/UserManagement/UserManagementHelper'; import { Container } from 'typedi'; import { InternalHooks } from '@/InternalHooks'; import { RoleService } from '@/services/role.service'; @@ -30,9 +30,13 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { NamingService } from '@/services/naming.service'; import { TagRepository } from '@/databases/repositories/tag.repository'; +import { EnterpriseWorkflowService } from './workflow.service.ee'; +import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; +import type { RoleNames } from '@/databases/entities/Role'; +import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; +import { CredentialsService } from '../credentials/credentials.service'; export const workflowsController = express.Router(); -workflowsController.use('/', EEWorkflowController); /** * POST /workflows @@ -62,6 +66,24 @@ workflowsController.post( WorkflowHelpers.addNodeIds(newWorkflow); + if (isSharingEnabled()) { + // This is a new workflow, so we simply check if the user has access to + // all used workflows + + const allCredentials = await CredentialsService.getMany(req.user); + + try { + Container.get(EnterpriseWorkflowService).validateCredentialPermissionsToUser( + newWorkflow, + allCredentials, + ); + } catch (error) { + throw new BadRequestError( + 'The workflow you are trying to save contains credentials that are not shared with you', + ); + } + } + let savedWorkflow: undefined | WorkflowEntity; await Db.transaction(async (transactionManager) => { @@ -112,7 +134,8 @@ workflowsController.get( listQueryMiddleware, async (req: ListQuery.Request, res: express.Response) => { try { - const sharedWorkflowIds = await WorkflowHelpers.getSharedWorkflowIds(req.user, ['owner']); + const roles: RoleNames[] = isSharingEnabled() ? [] : ['owner']; + const sharedWorkflowIds = await WorkflowHelpers.getSharedWorkflowIds(req.user, roles); const { workflows: data, count } = await Container.get(WorkflowService).getMany( sharedWorkflowIds, @@ -195,6 +218,37 @@ workflowsController.get( ResponseHelper.send(async (req: WorkflowRequest.Get) => { const { id: workflowId } = req.params; + if (isSharingEnabled()) { + const relations = ['shared', 'shared.user', 'shared.role']; + if (!config.getEnv('workflowTagsDisabled')) { + relations.push('tags'); + } + + const workflow = await Container.get(WorkflowRepository).get( + { id: workflowId }, + { relations }, + ); + + if (!workflow) { + throw new NotFoundError(`Workflow with ID "${workflowId}" does not exist`); + } + + const userSharing = workflow.shared?.find((shared) => shared.user.id === req.user.id); + if (!userSharing && !req.user.hasGlobalScope('workflow:read')) { + throw new UnauthorizedError( + 'You do not have permission to access this workflow. Ask the owner to share it with you', + ); + } + + const enterpriseWorkflowService = Container.get(EnterpriseWorkflowService); + + enterpriseWorkflowService.addOwnerAndSharings(workflow); + await enterpriseWorkflowService.addCredentialsToWorkflow(workflow, req.user); + return workflow; + } + + // sharing disabled + const extraRelations = config.getEnv('workflowTagsDisabled') ? [] : ['workflow.tags']; const shared = await Container.get(SharedWorkflowRepository).findSharing( @@ -226,18 +280,27 @@ workflowsController.patch( '/:id(\\w+)', ResponseHelper.send(async (req: WorkflowRequest.Update) => { const { id: workflowId } = req.params; + const forceSave = req.query.forceSave === 'true'; - const updateData = new WorkflowEntity(); + let updateData = new WorkflowEntity(); const { tags, ...rest } = req.body; Object.assign(updateData, rest); + if (isSharingEnabled()) { + updateData = await Container.get(EnterpriseWorkflowService).preventTampering( + updateData, + workflowId, + req.user, + ); + } + const updatedWorkflow = await Container.get(WorkflowService).update( req.user, updateData, workflowId, tags, - true, - ['owner'], + isSharingEnabled() ? forceSave : true, + isSharingEnabled() ? undefined : ['owner'], ); return updatedWorkflow; @@ -274,6 +337,19 @@ workflowsController.delete( workflowsController.post( '/run', ResponseHelper.send(async (req: WorkflowRequest.ManualRun): Promise => { + if (isSharingEnabled()) { + const workflow = Container.get(WorkflowRepository).create(req.body.workflowData); + + if (req.body.workflowData.id !== undefined) { + const safeWorkflow = await Container.get(EnterpriseWorkflowService).preventTampering( + workflow, + workflow.id, + req.user, + ); + req.body.workflowData.nodes = safeWorkflow.nodes; + } + } + return Container.get(WorkflowService).runManually( req.body, req.user, @@ -281,3 +357,81 @@ workflowsController.post( ); }), ); + +/** + * (EE) PUT /workflows/:id/share + * + * Grant or remove users' access to a workflow. + */ +workflowsController.put( + '/:workflowId/share', + ResponseHelper.send(async (req: WorkflowRequest.Share) => { + if (!isSharingEnabled()) throw new NotFoundError('Route not found'); + + const { workflowId } = req.params; + const { shareWithIds } = req.body; + + if ( + !Array.isArray(shareWithIds) || + !shareWithIds.every((userId) => typeof userId === 'string') + ) { + throw new BadRequestError('Bad request'); + } + + const isOwnedRes = await Container.get(EnterpriseWorkflowService).isOwned(req.user, workflowId); + const { ownsWorkflow } = isOwnedRes; + let { workflow } = isOwnedRes; + + if (!ownsWorkflow || !workflow) { + workflow = undefined; + // Allow owners/admins to share + if (req.user.hasGlobalScope('workflow:share')) { + const sharedRes = await Container.get(WorkflowService).getSharing(req.user, workflowId, { + allowGlobalScope: true, + globalScope: 'workflow:share', + }); + workflow = sharedRes?.workflow; + } + if (!workflow) { + throw new UnauthorizedError('Forbidden'); + } + } + + const ownerIds = ( + await Container.get(WorkflowRepository).getSharings( + Db.getConnection().createEntityManager(), + workflowId, + ['shared', 'shared.role'], + ) + ) + .filter((e) => e.role.name === 'owner') + .map((e) => e.userId); + + let newShareeIds: string[] = []; + await Db.transaction(async (trx) => { + // remove all sharings that are not supposed to exist anymore + await Container.get(WorkflowRepository).pruneSharings(trx, workflowId, [ + ...ownerIds, + ...shareWithIds, + ]); + + const sharings = await Container.get(WorkflowRepository).getSharings(trx, workflowId); + + // extract the new sharings that need to be added + newShareeIds = rightDiff( + [sharings, (sharing) => sharing.userId], + [shareWithIds, (shareeId) => shareeId], + ); + + if (newShareeIds.length) { + await Container.get(EnterpriseWorkflowService).share(trx, workflow!, newShareeIds); + } + }); + + void Container.get(InternalHooks).onWorkflowSharingUpdate( + workflowId, + req.user.id, + shareWithIds, + ); + }), +); diff --git a/packages/cli/test/integration/workflows.controller.test.ts b/packages/cli/test/integration/workflows.controller.test.ts index 1f34172739..79846b0d4f 100644 --- a/packages/cli/test/integration/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows.controller.test.ts @@ -19,6 +19,9 @@ import { createOwner } from './shared/db/users'; import { createWorkflow } from './shared/db/workflows'; import { createTag } from './shared/db/tags'; import { Push } from '@/push'; +import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee'; +import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; +import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity'; let owner: User; let authOwnerAgent: SuperAgentTest; @@ -607,3 +610,34 @@ describe('PATCH /workflows/:id', () => { expect(active).toBe(false); }); }); + +describe('POST /workflows/run', () => { + let sharingSpy: jest.SpyInstance; + let tamperingSpy: jest.SpyInstance; + let workflow: WorkflowEntity; + + beforeAll(() => { + const enterpriseWorkflowService = Container.get(EnterpriseWorkflowService); + const workflowRepository = Container.get(WorkflowRepository); + + sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled'); + tamperingSpy = jest.spyOn(enterpriseWorkflowService, 'preventTampering'); + workflow = workflowRepository.create({ id: uuid() }); + }); + + test('should prevent tampering if sharing is enabled', async () => { + sharingSpy.mockReturnValue(true); + + await authOwnerAgent.post('/workflows/run').send({ workflowData: workflow }); + + expect(tamperingSpy).toHaveBeenCalledTimes(1); + }); + + test('should skip tampering prevention if sharing is disabled', async () => { + sharingSpy.mockReturnValue(false); + + await authOwnerAgent.post('/workflows/run').send({ workflowData: workflow }); + + expect(tamperingSpy).not.toHaveBeenCalled(); + }); +});