feat: check for cred when updating workflow and remove credential_usage table (#4350) (no-changelog)

* feat: check for cred when updating workflow and remove credential_usage table
This commit is contained in:
Omar Ajoue
2022-10-26 10:49:43 -03:00
committed by GitHub
parent e8935de3b2
commit c65deb2949
19 changed files with 781 additions and 210 deletions

View File

@@ -202,7 +202,6 @@ export async function init(
collections.Settings = linkRepository(entities.Settings);
collections.InstalledPackages = linkRepository(entities.InstalledPackages);
collections.InstalledNodes = linkRepository(entities.InstalledNodes);
collections.CredentialUsage = linkRepository(entities.CredentialUsage);
isInitialized = true;

View File

@@ -40,7 +40,6 @@ import type { SharedWorkflow } from './databases/entities/SharedWorkflow';
import type { TagEntity } from './databases/entities/TagEntity';
import type { User } from './databases/entities/User';
import type { WorkflowEntity } from './databases/entities/WorkflowEntity';
import { CredentialUsage } from './databases/entities/CredentialUsage';
export interface IActivationError {
time: number;
@@ -84,7 +83,6 @@ export interface IDatabaseCollections {
Settings: Repository<Settings>;
InstalledPackages: Repository<InstalledPackages>;
InstalledNodes: Repository<InstalledNodes>;
CredentialUsage: Repository<CredentialUsage>;
}
export interface IWebhookDb {

View File

@@ -29,6 +29,7 @@ import { v4 as uuid } from 'uuid';
import {
CredentialTypes,
Db,
ICredentialsDb,
ICredentialsTypeData,
ITransferNodeTypes,
IWorkflowErrorData,
@@ -704,3 +705,83 @@ export function generateFailedExecutionFromError(
stoppedAt: new Date(),
};
}
/** Get all nodes in a workflow where the node credential is not accessible to the user. */
export function getNodesWithInaccessibleCreds(workflow: WorkflowEntity, userCredIds: string[]) {
if (!workflow.nodes) {
return [];
}
return workflow.nodes.filter((node) => {
if (!node.credentials) return false;
const allUsedCredentials = Object.values(node.credentials);
const allUsedCredentialIds = allUsedCredentials.map((nodeCred) => nodeCred.id?.toString());
return allUsedCredentialIds.some(
(nodeCredId) => nodeCredId && !userCredIds.includes(nodeCredId),
);
});
}
export function validateWorkflowCredentialUsage(
newWorkflowVersion: WorkflowEntity,
previousWorkflowVersion: WorkflowEntity,
credentialsUserHasAccessTo: ICredentialsDb[],
) {
/**
* We only need to check nodes that use credentials the current user cannot access,
* since these can be 2 possibilities:
* - Same ID already exist: it's a read only node and therefore cannot be changed
* - It's a new node which indicates tampering and therefore must fail saving
*/
const allowedCredentialIds = credentialsUserHasAccessTo.map((cred) => cred.id.toString());
const nodesWithCredentialsUserDoesNotHaveAccessTo = getNodesWithInaccessibleCreds(
newWorkflowVersion,
allowedCredentialIds,
);
// If there are no nodes with credentials the user does not have access to we can skip the rest
if (nodesWithCredentialsUserDoesNotHaveAccessTo.length === 0) {
return newWorkflowVersion;
}
const previouslyExistingNodeIds = previousWorkflowVersion.nodes.map((node) => node.id);
// If it's a new node we can't allow it to be saved
// since it uses creds the node doesn't have access
const isTamperingAttempt = (inaccessibleCredNodeId: string) =>
!previouslyExistingNodeIds.includes(inaccessibleCredNodeId);
nodesWithCredentialsUserDoesNotHaveAccessTo.forEach((node) => {
if (isTamperingAttempt(node.id)) {
Logger.info('Blocked workflow update due to tampering attempt', {
nodeType: node.type,
nodeName: node.name,
nodeId: node.id,
nodeCredentials: node.credentials,
});
// Node is new, so this is probably a tampering attempt. Throw an error
throw new Error(
'Workflow contains new nodes with credentials the user does not have access to',
);
}
// Replace the node with the previous version of the node
// Since it cannot be modified (read only node)
const nodeIdx = newWorkflowVersion.nodes.findIndex(
(newWorkflowNode) => newWorkflowNode.id === node.id,
);
Logger.debug('Replacing node with previous version when saving updated workflow', {
nodeType: node.type,
nodeName: node.name,
nodeId: node.id,
});
newWorkflowVersion.nodes[nodeIdx] = previousWorkflowVersion.nodes.find(
(previousNode) => previousNode.id === node.id,
)!;
});
return newWorkflowVersion;
}

View File

@@ -1,28 +0,0 @@
import { Entity, ManyToOne, PrimaryColumn, RelationId } from 'typeorm';
import { WorkflowEntity } from './WorkflowEntity';
import { CredentialsEntity } from './CredentialsEntity';
import { AbstractEntity } from './AbstractEntity';
@Entity()
export class CredentialUsage extends AbstractEntity {
@ManyToOne(() => WorkflowEntity, {
onDelete: 'CASCADE',
})
workflow: WorkflowEntity;
@ManyToOne(() => CredentialsEntity, {
onDelete: 'CASCADE',
})
credential: CredentialsEntity;
@RelationId((credentialUsage: CredentialUsage) => credentialUsage.workflow)
@PrimaryColumn()
workflowId: number;
@PrimaryColumn()
nodeId: string;
@RelationId((credentialUsage: CredentialUsage) => credentialUsage.credential)
@PrimaryColumn()
credentialId: string;
}

View File

@@ -11,7 +11,6 @@ import { SharedWorkflow } from './SharedWorkflow';
import { SharedCredentials } from './SharedCredentials';
import { InstalledPackages } from './InstalledPackages';
import { InstalledNodes } from './InstalledNodes';
import { CredentialUsage } from './CredentialUsage';
export const entities = {
CredentialsEntity,
@@ -26,5 +25,4 @@ export const entities = {
SharedCredentials,
InstalledPackages,
InstalledNodes,
CredentialUsage,
};

View File

@@ -33,8 +33,6 @@ export class CreateCredentialUsageTable1665484192213 implements MigrationInterfa
async down(queryRunner: QueryRunner) {
const tablePrefix = getTablePrefix();
await queryRunner.query(`
DELETE FROM ${tablePrefix}role WHERE name='user' AND scope='workflow';
`);
await queryRunner.query(`DROP TABLE "${tablePrefix}credential_usage"`);
}
}

View File

@@ -0,0 +1,37 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { getTablePrefix, logMigrationEnd, logMigrationStart } from '../../utils/migrationHelpers';
export class RemoveCredentialUsageTable1665754637026 implements MigrationInterface {
name = 'RemoveCredentialUsageTable1665754637026';
async up(queryRunner: QueryRunner) {
logMigrationStart(this.name);
const tablePrefix = getTablePrefix();
await queryRunner.query(`DROP TABLE \`${tablePrefix}credential_usage\``);
logMigrationEnd(this.name);
}
async down(queryRunner: QueryRunner) {
const tablePrefix = getTablePrefix();
await queryRunner.query(
`CREATE TABLE \`${tablePrefix}credential_usage\` (` +
'`workflowId` int NOT NULL,' +
'`nodeId` char(200) NOT NULL,' +
"`credentialId` int NOT NULL DEFAULT '1'," +
`\`createdAt\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,` +
`\`updatedAt\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,` +
'PRIMARY KEY (`workflowId`, `nodeId`, `credentialId`)' +
") ENGINE='InnoDB';",
);
await queryRunner.query(
`ALTER TABLE \`${tablePrefix}credential_usage\` ADD CONSTRAINT \`FK_${tablePrefix}518e1ece107b859ca6ce9ed2487f7e23\` FOREIGN KEY (\`workflowId\`) REFERENCES \`${tablePrefix}workflow_entity\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE \`${tablePrefix}credential_usage\` ADD CONSTRAINT \`FK_${tablePrefix}7ce200a20ade7ae89fa7901da896993f\` FOREIGN KEY (\`credentialId\`) REFERENCES \`${tablePrefix}credentials_entity\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`,
);
}
}

View File

@@ -22,6 +22,7 @@ import { AddJsonKeyPinData1659895550980 } from './1659895550980-AddJsonKeyPinDat
import { CreateCredentialsUserRole1660062385367 } from './1660062385367-CreateCredentialsUserRole';
import { CreateWorkflowsEditorRole1663755770894 } from './1663755770894-CreateWorkflowsEditorRole';
import { CreateCredentialUsageTable1665484192213 } from './1665484192213-CreateCredentialUsageTable';
import { RemoveCredentialUsageTable1665754637026 } from './1665754637026-RemoveCredentialUsageTable';
export const mysqlMigrations = [
InitialMigration1588157391238,
@@ -48,4 +49,5 @@ export const mysqlMigrations = [
CreateCredentialsUserRole1660062385367,
CreateWorkflowsEditorRole1663755770894,
CreateCredentialUsageTable1665484192213,
RemoveCredentialUsageTable1665754637026,
];

View File

@@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { getTablePrefix, logMigrationEnd, logMigrationStart } from '../../utils/migrationHelpers';
export class RemoveCredentialUsageTable1665754637025 implements MigrationInterface {
name = 'RemoveCredentialUsageTable1665754637025';
async up(queryRunner: QueryRunner) {
logMigrationStart(this.name);
const tablePrefix = getTablePrefix();
await queryRunner.query(`DROP TABLE ${tablePrefix}credential_usage`);
logMigrationEnd(this.name);
}
async down(queryRunner: QueryRunner) {
const tablePrefix = getTablePrefix();
await queryRunner.query(
`CREATE TABLE ${tablePrefix}credential_usage (` +
'"workflowId" int NOT NULL,' +
'"nodeId" UUID NOT NULL,' +
'"credentialId" int NULL,' +
'"createdAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
'"updatedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
`CONSTRAINT "PK_${tablePrefix}feb7a6545aa714ac6e7f6b14825f0efc9353dd3a" PRIMARY KEY ("workflowId", "nodeId", "credentialId"), ` +
`CONSTRAINT "FK_${tablePrefix}518e1ece107b859ca6ce9ed2487f7e23" FOREIGN KEY ("workflowId") REFERENCES ${tablePrefix}workflow_entity ("id") ON DELETE CASCADE ON UPDATE CASCADE, ` +
`CONSTRAINT "FK_${tablePrefix}7ce200a20ade7ae89fa7901da896993f" FOREIGN KEY ("credentialId") REFERENCES ${tablePrefix}credentials_entity ("id") ON DELETE CASCADE ON UPDATE CASCADE ` +
');',
);
}
}

View File

@@ -20,6 +20,7 @@ import { AddJsonKeyPinData1659902242948 } from './1659902242948-AddJsonKeyPinDat
import { CreateCredentialsUserRole1660062385367 } from './1660062385367-CreateCredentialsUserRole';
import { CreateWorkflowsEditorRole1663755770893 } from './1663755770893-CreateWorkflowsEditorRole';
import { CreateCredentialUsageTable1665484192212 } from './1665484192212-CreateCredentialUsageTable';
import { RemoveCredentialUsageTable1665754637025 } from './1665754637025-RemoveCredentialUsageTable';
export const postgresMigrations = [
InitialMigration1587669153312,
@@ -44,4 +45,5 @@ export const postgresMigrations = [
AddJsonKeyPinData1659902242948,
CreateWorkflowsEditorRole1663755770893,
CreateCredentialUsageTable1665484192212,
RemoveCredentialUsageTable1665754637025,
];

View File

@@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { getTablePrefix, logMigrationEnd, logMigrationStart } from '../../utils/migrationHelpers';
export class RemoveCredentialUsageTable1665754637024 implements MigrationInterface {
name = 'RemoveCredentialUsageTable1665754637024';
async up(queryRunner: QueryRunner) {
logMigrationStart(this.name);
const tablePrefix = getTablePrefix();
await queryRunner.query(`DROP TABLE "${tablePrefix}credential_usage"`);
logMigrationEnd(this.name);
}
async down(queryRunner: QueryRunner) {
const tablePrefix = getTablePrefix();
await queryRunner.query(
`CREATE TABLE "${tablePrefix}credential_usage" (` +
`"workflowId" integer NOT NULL,` +
`"nodeId" varchar NOT NULL,` +
`"credentialId" integer NOT NULL,` +
`"createdAt" datetime(3) NOT NULL DEFAULT 'STRFTIME(''%Y-%m-%d %H:%M:%f'', ''NOW'')',` +
`"updatedAt" datetime(3) NOT NULL DEFAULT 'STRFTIME(''%Y-%m-%d %H:%M:%f'', ''NOW'')',` +
`PRIMARY KEY("workflowId", "nodeId", "credentialId"), ` +
`CONSTRAINT "FK_${tablePrefix}518e1ece107b859ca6ce9ed2487f7e23" FOREIGN KEY ("workflowId") REFERENCES "${tablePrefix}workflow_entity" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, ` +
`CONSTRAINT "FK_${tablePrefix}7ce200a20ade7ae89fa7901da896993f" FOREIGN KEY ("credentialId") REFERENCES "${tablePrefix}credentials_entity" ("id") ON DELETE CASCADE ON UPDATE NO ACTION ` +
`);`,
);
}
}

View File

@@ -19,6 +19,7 @@ import { AddJsonKeyPinData1659888469333 } from './1659888469333-AddJsonKeyPinDat
import { CreateCredentialsUserRole1660062385367 } from './1660062385367-CreateCredentialsUserRole';
import { CreateWorkflowsEditorRole1663755770892 } from './1663755770892-CreateWorkflowsUserRole';
import { CreateCredentialUsageTable1665484192211 } from './1665484192211-CreateCredentialUsageTable';
import { RemoveCredentialUsageTable1665754637024 } from './1665754637024-RemoveCredentialUsageTable';
const sqliteMigrations = [
InitialMigration1588102412422,
@@ -42,6 +43,7 @@ const sqliteMigrations = [
CreateCredentialsUserRole1660062385367,
CreateWorkflowsEditorRole1663755770892,
CreateCredentialUsageTable1665484192211,
RemoveCredentialUsageTable1665754637024,
];
export { sqliteMigrations };

View File

@@ -11,7 +11,6 @@ import { SharedWorkflow } from '../databases/entities/SharedWorkflow';
import { LoggerProxy } from 'n8n-workflow';
import * as TagHelpers from '../TagHelpers';
import { EECredentialsService as EECredentials } from '../credentials/credentials.service.ee';
import { CredentialUsage } from '../databases/entities/CredentialUsage';
// eslint-disable-next-line @typescript-eslint/naming-convention
export const EEWorkflowController = express.Router();
@@ -155,29 +154,6 @@ EEWorkflowController.post(
});
await transactionManager.save<SharedWorkflow>(newSharedWorkflow);
const credentialUsage: CredentialUsage[] = [];
newWorkflow.nodes.forEach((node) => {
if (!node.credentials) {
return;
}
Object.keys(node.credentials).forEach((credentialType) => {
const credentialId = node.credentials?.[credentialType].id;
if (credentialId) {
const newCredentialusage = new CredentialUsage();
Object.assign(newCredentialusage, {
credentialId,
nodeId: node.id,
workflowId: savedWorkflow?.id,
});
credentialUsage.push(newCredentialusage);
}
});
});
if (credentialUsage.length) {
await transactionManager.save<CredentialUsage>(credentialUsage);
}
});
if (!savedWorkflow) {
@@ -202,3 +178,28 @@ EEWorkflowController.post(
};
}),
);
EEWorkflowController.patch(
'/:id(\\d+)',
ResponseHelper.send(async (req: WorkflowRequest.Update) => {
const { id: workflowId } = req.params;
const updateData = new WorkflowEntity();
const { tags, ...rest } = req.body;
Object.assign(updateData, rest);
const updatedWorkflow = await EEWorkflows.updateWorkflow(
req.user,
updateData,
workflowId,
tags,
);
const { id, ...remainder } = updatedWorkflow;
return {
id: id.toString(),
...remainder,
};
}),
);

View File

@@ -33,6 +33,7 @@ import { getLogger } from '../Logger';
import type { WorkflowRequest } from '../requests';
import { getSharedWorkflowIds, isBelowOnboardingThreshold } from '../WorkflowHelpers';
import { EEWorkflowController } from './workflows.controller.ee';
import { WorkflowsService } from './workflows.services';
import { validate as jsonSchemaValidate } from 'jsonschema';
const activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
@@ -364,128 +365,12 @@ workflowsController.patch(
const { tags, ...rest } = req.body;
Object.assign(updateData, rest);
const shared = await Db.collections.SharedWorkflow.findOne({
relations: ['workflow'],
where: whereClause({
user: req.user,
entityType: 'workflow',
entityId: workflowId,
}),
});
if (!shared) {
LoggerProxy.info('User attempted to update a workflow without permissions', {
workflowId,
userId: req.user.id,
});
throw new ResponseHelper.ResponseError(
`Workflow with ID "${workflowId}" could not be found to be updated.`,
undefined,
404,
);
}
// check credentials for old format
await WorkflowHelpers.replaceInvalidCredentials(updateData);
WorkflowHelpers.addNodeIds(updateData);
await externalHooks.run('workflow.update', [updateData]);
if (shared.workflow.active) {
// When workflow gets saved always remove it as the triggers could have been
// changed and so the changes would not take effect
await activeWorkflowRunner.remove(workflowId);
}
if (updateData.settings) {
if (updateData.settings.timezone === 'DEFAULT') {
// Do not save the default timezone
delete updateData.settings.timezone;
}
if (updateData.settings.saveDataErrorExecution === 'DEFAULT') {
// Do not save when default got set
delete updateData.settings.saveDataErrorExecution;
}
if (updateData.settings.saveDataSuccessExecution === 'DEFAULT') {
// Do not save when default got set
delete updateData.settings.saveDataSuccessExecution;
}
if (updateData.settings.saveManualExecutions === 'DEFAULT') {
// Do not save when default got set
delete updateData.settings.saveManualExecutions;
}
if (
parseInt(updateData.settings.executionTimeout as string, 10) ===
config.get('executions.timeout')
) {
// Do not save when default got set
delete updateData.settings.executionTimeout;
}
}
if (updateData.name) {
updateData.updatedAt = new Date(); // required due to atomic update
await validateEntity(updateData);
}
await Db.collections.Workflow.update(workflowId, updateData);
if (tags && !config.getEnv('workflowTagsDisabled')) {
const tablePrefix = config.getEnv('database.tablePrefix');
await TagHelpers.removeRelations(workflowId, tablePrefix);
if (tags.length) {
await TagHelpers.createRelations(workflowId, tags, tablePrefix);
}
}
const options: FindManyOptions<WorkflowEntity> = {
relations: ['tags'],
};
if (config.getEnv('workflowTagsDisabled')) {
delete options.relations;
}
// We sadly get nothing back from "update". Neither if it updated a record
// nor the new value. So query now the hopefully updated entry.
const updatedWorkflow = await Db.collections.Workflow.findOne(workflowId, options);
if (updatedWorkflow === undefined) {
throw new ResponseHelper.ResponseError(
`Workflow with ID "${workflowId}" could not be found to be updated.`,
undefined,
400,
);
}
if (updatedWorkflow.tags?.length && tags?.length) {
updatedWorkflow.tags = TagHelpers.sortByRequestOrder(updatedWorkflow.tags, {
requestOrder: tags,
});
}
await externalHooks.run('workflow.afterUpdate', [updatedWorkflow]);
void InternalHooksManager.getInstance().onWorkflowSaved(req.user.id, updatedWorkflow, false);
if (updatedWorkflow.active) {
// When the workflow is supposed to be active add it again
try {
await externalHooks.run('workflow.activate', [updatedWorkflow]);
await activeWorkflowRunner.add(workflowId, shared.workflow.active ? 'update' : 'activate');
} catch (error) {
// If workflow could not be activated set it again to inactive
updateData.active = false;
await Db.collections.Workflow.update(workflowId, updateData);
// Also set it in the returned data
updatedWorkflow.active = false;
// Now return the original error for UI to display
throw error;
}
}
const updatedWorkflow = await WorkflowsService.updateWorkflow(
req.user,
updateData,
workflowId,
tags,
);
const { id, ...remainder } = updatedWorkflow;

View File

@@ -1,5 +1,5 @@
import { DeleteResult, EntityManager, In, Not } from 'typeorm';
import { Db, ICredentialsDb } from '..';
import { Db, ICredentialsDb, ResponseHelper, WorkflowHelpers } from '..';
import { SharedWorkflow } from '../databases/entities/SharedWorkflow';
import { User } from '../databases/entities/User';
import { WorkflowEntity } from '../databases/entities/WorkflowEntity';
@@ -7,6 +7,7 @@ import { RoleService } from '../role/role.service';
import { UserService } from '../user/user.service';
import { WorkflowsService } from './workflows.services';
import { WorkflowWithSharings } from './workflows.types';
import { EECredentialsService as EECredentials } from '../credentials/credentials.service.ee';
export class EEWorkflowsService extends WorkflowsService {
static async isOwned(
@@ -114,4 +115,33 @@ export class EEWorkflowsService extends WorkflowsService {
});
});
}
static async updateWorkflow(
user: User,
workflow: WorkflowEntity,
workflowId: string,
tags?: string[],
): Promise<WorkflowEntity> {
const previousVersion = await EEWorkflowsService.get({ id: parseInt(workflowId, 10) });
if (!previousVersion) {
throw new ResponseHelper.ResponseError('Workflow not found', undefined, 404);
}
const allCredentials = await EECredentials.getAll(user);
try {
workflow = WorkflowHelpers.validateWorkflowCredentialUsage(
workflow,
previousVersion,
allCredentials,
);
} catch (error) {
console.log(error);
throw new ResponseHelper.ResponseError(
'Invalid workflow credentials - make sure you have access to all credentials and try again.',
undefined,
400,
);
}
return super.updateWorkflow(user, workflow, workflowId, tags);
}
}

View File

@@ -1,8 +1,20 @@
import { FindOneOptions, ObjectLiteral } from 'typeorm';
import { Db } from '..';
import { LoggerProxy } from 'n8n-workflow';
import { FindManyOptions, FindOneOptions, ObjectLiteral } from 'typeorm';
import {
ActiveWorkflowRunner,
Db,
InternalHooksManager,
ResponseHelper,
whereClause,
WorkflowHelpers,
} from '..';
import config from '../../config';
import { SharedWorkflow } from '../databases/entities/SharedWorkflow';
import { User } from '../databases/entities/User';
import { WorkflowEntity } from '../databases/entities/WorkflowEntity';
import { validateEntity } from '../GenericHelpers';
import { externalHooks } from '../Server';
import * as TagHelpers from '../TagHelpers';
export class WorkflowsService {
static async getSharing(
@@ -34,4 +46,139 @@ export class WorkflowsService {
static async get(workflow: Partial<WorkflowEntity>, options?: { relations: string[] }) {
return Db.collections.Workflow.findOne(workflow, options);
}
static async updateWorkflow(
user: User,
workflow: WorkflowEntity,
workflowId: string,
tags?: string[],
): Promise<WorkflowEntity> {
const shared = await Db.collections.SharedWorkflow.findOne({
relations: ['workflow'],
where: whereClause({
user,
entityType: 'workflow',
entityId: workflowId,
}),
});
if (!shared) {
LoggerProxy.info('User attempted to update a workflow without permissions', {
workflowId,
userId: user.id,
});
throw new ResponseHelper.ResponseError(
`Workflow with ID "${workflowId}" could not be found to be updated.`,
undefined,
404,
);
}
// check credentials for old format
await WorkflowHelpers.replaceInvalidCredentials(workflow);
WorkflowHelpers.addNodeIds(workflow);
await externalHooks.run('workflow.update', [workflow]);
if (shared.workflow.active) {
// When workflow gets saved always remove it as the triggers could have been
// changed and so the changes would not take effect
await ActiveWorkflowRunner.getInstance().remove(workflowId);
}
if (workflow.settings) {
if (workflow.settings.timezone === 'DEFAULT') {
// Do not save the default timezone
delete workflow.settings.timezone;
}
if (workflow.settings.saveDataErrorExecution === 'DEFAULT') {
// Do not save when default got set
delete workflow.settings.saveDataErrorExecution;
}
if (workflow.settings.saveDataSuccessExecution === 'DEFAULT') {
// Do not save when default got set
delete workflow.settings.saveDataSuccessExecution;
}
if (workflow.settings.saveManualExecutions === 'DEFAULT') {
// Do not save when default got set
delete workflow.settings.saveManualExecutions;
}
if (
parseInt(workflow.settings.executionTimeout as string, 10) ===
config.get('executions.timeout')
) {
// Do not save when default got set
delete workflow.settings.executionTimeout;
}
}
if (workflow.name) {
workflow.updatedAt = new Date(); // required due to atomic update
await validateEntity(workflow);
}
await Db.collections.Workflow.update(workflowId, workflow);
if (tags && !config.getEnv('workflowTagsDisabled')) {
const tablePrefix = config.getEnv('database.tablePrefix');
await TagHelpers.removeRelations(workflowId, tablePrefix);
if (tags.length) {
await TagHelpers.createRelations(workflowId, tags, tablePrefix);
}
}
const options: FindManyOptions<WorkflowEntity> = {
relations: ['tags'],
};
if (config.getEnv('workflowTagsDisabled')) {
delete options.relations;
}
// We sadly get nothing back from "update". Neither if it updated a record
// nor the new value. So query now the hopefully updated entry.
const updatedWorkflow = await Db.collections.Workflow.findOne(workflowId, options);
if (updatedWorkflow === undefined) {
throw new ResponseHelper.ResponseError(
`Workflow with ID "${workflowId}" could not be found to be updated.`,
undefined,
400,
);
}
if (updatedWorkflow.tags?.length && tags?.length) {
updatedWorkflow.tags = TagHelpers.sortByRequestOrder(updatedWorkflow.tags, {
requestOrder: tags,
});
}
await externalHooks.run('workflow.afterUpdate', [updatedWorkflow]);
void InternalHooksManager.getInstance().onWorkflowSaved(user.id, updatedWorkflow, false);
if (updatedWorkflow.active) {
// When the workflow is supposed to be active add it again
try {
await externalHooks.run('workflow.activate', [updatedWorkflow]);
await ActiveWorkflowRunner.getInstance().add(
workflowId,
shared.workflow.active ? 'update' : 'activate',
);
} catch (error) {
// If workflow could not be activated set it again to inactive
workflow.active = false;
await Db.collections.Workflow.update(workflowId, workflow);
// Also set it in the returned data
updatedWorkflow.active = false;
// Now return the original error for UI to display
throw error;
}
}
return updatedWorkflow;
}
}