mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: add webhook secret key functionality to workflows and update related components
This commit is contained in:
@@ -527,6 +527,35 @@ export default class Workflow extends BaseModel {
|
||||
})
|
||||
public triggerArguments?: JSONObject = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadWorkflow,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditWorkflow,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
isDefaultValueColumn: false,
|
||||
required: false,
|
||||
type: TableColumnType.LongText,
|
||||
title: "Webhook Secret Key",
|
||||
description:
|
||||
"Secret key used to trigger this workflow via webhook. Use this instead of the workflow ID for security.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.LongText,
|
||||
nullable: true,
|
||||
})
|
||||
public webhookSecretKey?: string = undefined;
|
||||
|
||||
// This is a BullMQ job key that is used to schedule job for this workflow. This is used internally to remove existing job.
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1774559064920 implements MigrationInterface {
|
||||
public name = "MigrationName1774559064920";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Workflow" ADD "webhookSecretKey" text`,
|
||||
);
|
||||
|
||||
// Set secret key to existing workflow ID so current webhook URLs keep working.
|
||||
await queryRunner.query(
|
||||
`UPDATE "Workflow" SET "webhookSecretKey" = "_id"::text WHERE "webhookSecretKey" IS NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Workflow" DROP COLUMN "webhookSecretKey"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -274,6 +274,7 @@ import { MigrationName1774524742177 } from "./1774524742177-MigrationName";
|
||||
import { MigrationName1774524742178 } from "./1774524742178-MigrationName";
|
||||
import { MigrationName1774524742179 } from "./1774524742179-MigrationName";
|
||||
import { MigrationName1774559064919 } from "./1774559064919-MigrationName";
|
||||
import { MigrationName1774559064920 } from "./1774559064920-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -552,4 +553,5 @@ export default [
|
||||
MigrationName1774524742178,
|
||||
MigrationName1774524742179,
|
||||
MigrationName1774559064919,
|
||||
MigrationName1774559064920,
|
||||
];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { WorkflowHostname } from "../EnvironmentConfig";
|
||||
import ClusterKeyAuthorization from "../Middleware/ClusterKeyAuthorization";
|
||||
import { OnUpdate } from "../Types/Database/Hooks";
|
||||
import CreateBy from "../Types/Database/CreateBy";
|
||||
import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import EmptyResponseData from "../../Types/API/EmptyResponse";
|
||||
import Protocol from "../../Types/API/Protocol";
|
||||
@@ -17,12 +18,28 @@ import {
|
||||
import API from "../../Utils/API";
|
||||
import Model from "../../Models/DatabaseModels/Workflow";
|
||||
import logger from "../Utils/Logger";
|
||||
import UUID from "../../Utils/UUID";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
protected override async onBeforeCreate(
|
||||
createBy: CreateBy<Model>,
|
||||
): Promise<OnCreate<Model>> {
|
||||
// Auto-generate webhook secret key for new workflows.
|
||||
if (!createBy.data.webhookSecretKey) {
|
||||
createBy.data.webhookSecretKey = UUID.generate();
|
||||
}
|
||||
|
||||
return {
|
||||
createBy,
|
||||
carryForward: null,
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
protected override async onUpdateSuccess(
|
||||
onUpdate: OnUpdate<Model>,
|
||||
|
||||
@@ -13,6 +13,8 @@ import ComponentMetadata, { Port } from "../../../../Types/Workflow/Component";
|
||||
import ComponentID from "../../../../Types/Workflow/ComponentID";
|
||||
import WebhookComponents from "../../../../Types/Workflow/Components/Webhook";
|
||||
import CaptureSpan from "../../../Utils/Telemetry/CaptureSpan";
|
||||
import WorkflowService from "../../../Services/WorkflowService";
|
||||
import Workflow from "../../../../Models/DatabaseModels/Workflow";
|
||||
|
||||
export default class WebhookTrigger extends TriggerCode {
|
||||
public constructor() {
|
||||
@@ -54,7 +56,7 @@ export default class WebhookTrigger extends TriggerCode {
|
||||
@CaptureSpan()
|
||||
public override async init(props: InitProps): Promise<void> {
|
||||
props.router.get(
|
||||
`/trigger/:workflowId`,
|
||||
`/trigger/:secretkey`,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.initTrigger(req, res, props);
|
||||
@@ -65,7 +67,7 @@ export default class WebhookTrigger extends TriggerCode {
|
||||
);
|
||||
|
||||
props.router.post(
|
||||
`/trigger/:workflowId`,
|
||||
`/trigger/:secretkey`,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.initTrigger(req, res, props);
|
||||
@@ -82,12 +84,33 @@ export default class WebhookTrigger extends TriggerCode {
|
||||
res: ExpressResponse,
|
||||
props: InitProps,
|
||||
): Promise<void> {
|
||||
/// Run Graph.
|
||||
const secretKey: string = req.params["secretkey"] as string;
|
||||
|
||||
// check if this workflow has the trigger enabled.
|
||||
if (!secretKey) {
|
||||
throw new BadDataException("Secret key is required to trigger workflow.");
|
||||
}
|
||||
|
||||
// Look up the workflow by webhook secret key.
|
||||
const workflow: Workflow | null = await WorkflowService.findOneBy({
|
||||
query: {
|
||||
webhookSecretKey: secretKey,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!workflow || !workflow._id) {
|
||||
throw new BadDataException(
|
||||
"Workflow not found for the provided secret key.",
|
||||
);
|
||||
}
|
||||
|
||||
const executeWorkflow: ExecuteWorkflowType = {
|
||||
workflowId: new ObjectID(req.params["workflowId"] as string),
|
||||
workflowId: new ObjectID(workflow._id),
|
||||
returnValues: {
|
||||
"request-headers": req.headers,
|
||||
"request-params": req.query,
|
||||
|
||||
@@ -713,12 +713,25 @@ ${contextBlock}
|
||||
if (breakdown.affectedResources && breakdown.affectedResources.length > 0) {
|
||||
const resourceLines: Array<string> = [];
|
||||
|
||||
// Sort by metric value descending (worst first)
|
||||
// Sort by metric value descending (worst first) and filter out zero-value resources
|
||||
const sortedResources: Array<KubernetesAffectedResource> = [
|
||||
...breakdown.affectedResources,
|
||||
].sort((a: KubernetesAffectedResource, b: KubernetesAffectedResource) => {
|
||||
return b.metricValue - a.metricValue;
|
||||
});
|
||||
]
|
||||
.filter((r: KubernetesAffectedResource) => {
|
||||
return r.metricValue > 0;
|
||||
})
|
||||
.sort(
|
||||
(
|
||||
a: KubernetesAffectedResource,
|
||||
b: KubernetesAffectedResource,
|
||||
) => {
|
||||
return b.metricValue - a.metricValue;
|
||||
},
|
||||
);
|
||||
|
||||
if (sortedResources.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Show top 10 affected resources
|
||||
const resourcesToShow: Array<KubernetesAffectedResource> =
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface ComponentProps {
|
||||
component: NodeDataProp;
|
||||
graphComponents: Array<NodeDataProp>;
|
||||
workflowId: ObjectID;
|
||||
webhookSecretKey?: string | undefined;
|
||||
}
|
||||
|
||||
const ComponentSettingsModal: FunctionComponent<ComponentProps> = (
|
||||
@@ -179,6 +180,7 @@ const ComponentSettingsModal: FunctionComponent<ComponentProps> = (
|
||||
<DocumentationViewer
|
||||
documentationLink={component.metadata.documentationLink}
|
||||
workflowId={props.workflowId}
|
||||
webhookSecretKey={props.webhookSecretKey}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -14,6 +14,7 @@ import useAsyncEffect from "use-async-effect";
|
||||
export interface ComponentProps {
|
||||
documentationLink: Route;
|
||||
workflowId: ObjectID;
|
||||
webhookSecretKey?: string | undefined;
|
||||
}
|
||||
|
||||
const DocumentationViewer: FunctionComponent<ComponentProps> = (
|
||||
@@ -30,6 +31,10 @@ const DocumentationViewer: FunctionComponent<ComponentProps> = (
|
||||
): string => {
|
||||
text = text.replace("{{serverUrl}}", HOME_URL.toString());
|
||||
text = text.replace("{{workflowId}}", props.workflowId.toString());
|
||||
text = text.replace(
|
||||
"{{webhookSecretKey}}",
|
||||
props.webhookSecretKey || "Loading...",
|
||||
);
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
@@ -111,6 +111,7 @@ export interface ComponentProps {
|
||||
workflowId: ObjectID;
|
||||
onRunModalUpdate: (isModalShown: boolean) => void;
|
||||
onRun: (trigger: NodeDataProp) => void;
|
||||
webhookSecretKey?: string | undefined;
|
||||
}
|
||||
|
||||
const Workflow: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
|
||||
@@ -562,6 +563,7 @@ const Workflow: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
|
||||
return node.data as NodeDataProp;
|
||||
})}
|
||||
workflowId={props.workflowId}
|
||||
webhookSecretKey={props.webhookSecretKey}
|
||||
component={selectedNodeData}
|
||||
title={
|
||||
selectedNodeData && selectedNodeData.metadata.title
|
||||
|
||||
Reference in New Issue
Block a user