feat(workflow-integration): enhance project workflow integration with secret sync error notifications and improve Slack notification formatting

This commit is contained in:
Victor Santos
2025-10-27 09:29:16 -03:00
parent ecca51b1d6
commit c1dc3c2b76
13 changed files with 260 additions and 152 deletions
@@ -243,7 +243,8 @@ export const accessApprovalRequestServiceFactory = ({
);
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
const approvalPath = `/projects/secret-management/${project.id}/approval`;
const projectPath = `/projects/secret-management/${project.id}`;
const approvalPath = `${projectPath}/approval`;
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
await triggerWorkflowIntegrationNotification({
@@ -252,6 +253,7 @@ export const accessApprovalRequestServiceFactory = ({
type: TriggerFeature.ACCESS_REQUEST,
payload: {
projectName: project.name,
projectPath,
requesterFullName,
isTemporary,
requesterEmail: requestedByUser.email as string,
@@ -32,6 +32,7 @@ export type TNotification =
secretPath: string;
environment: string;
projectName: string;
projectPath: string;
permissions: string[];
approvalUrl: string;
note?: string;
@@ -61,6 +62,10 @@ export type TNotification =
syncDestination: string;
failureMessage: string;
syncUrl: string;
environment: string;
secretPath: string;
projectName: string;
projectPath: string;
};
};
+4 -1
View File
@@ -1244,7 +1244,10 @@ export const registerRoutes = async (
licenseService,
gatewayService,
gatewayV2Service,
notificationService
notificationService,
projectSlackConfigDAL,
projectMicrosoftTeamsConfigDAL,
microsoftTeamsService
});
const secretQueueService = secretQueueFactory({
@@ -769,7 +769,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
isAccessRequestNotificationEnabled: true,
accessRequestChannels: true,
isSecretRequestNotificationEnabled: true,
secretRequestChannels: true
secretRequestChannels: true,
isSecretSyncErrorNotificationEnabled: true,
secretSyncErrorChannels: true
}).merge(
z.object({
integration: z.literal(WorkflowIntegration.SLACK),
@@ -873,7 +875,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
accessRequestChannels: validateSlackChannelsField,
secretRequestChannels: validateSlackChannelsField,
isAccessRequestNotificationEnabled: z.boolean(),
isSecretRequestNotificationEnabled: z.boolean()
isSecretRequestNotificationEnabled: z.boolean(),
secretSyncErrorChannels: validateSlackChannelsField,
isSecretSyncErrorNotificationEnabled: z.boolean()
}),
z.object({
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
@@ -891,7 +895,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
isAccessRequestNotificationEnabled: true,
accessRequestChannels: true,
isSecretRequestNotificationEnabled: true,
secretRequestChannels: true
secretRequestChannels: true,
isSecretSyncErrorNotificationEnabled: true,
secretSyncErrorChannels: true
}).merge(
z.object({
integration: z.literal(WorkflowIntegration.SLACK),
@@ -1568,8 +1568,14 @@ export const projectServiceFactory = ({
isAccessRequestNotificationEnabled,
accessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels
}: TUpdateProjectWorkflowIntegration) => {
secretRequestChannels,
secretSyncErrorChannels,
isSecretSyncErrorNotificationEnabled
}: TUpdateProjectWorkflowIntegration & {
// workaround intersection type while we don't have the microsoft teams integration for failed secret syncs
isSecretSyncErrorNotificationEnabled: boolean;
secretSyncErrorChannels: string;
}) => {
const project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({
@@ -1591,6 +1597,7 @@ export const projectServiceFactory = ({
const sanitizedAccessRequestChannels = validateSlackChannelsField.parse(accessRequestChannels);
const sanitizedSecretRequestChannels = validateSlackChannelsField.parse(secretRequestChannels);
const sanitizedSecretSyncErrorChannels = validateSlackChannelsField.parse(secretSyncErrorChannels);
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId);
@@ -1628,7 +1635,9 @@ export const projectServiceFactory = ({
isAccessRequestNotificationEnabled,
accessRequestChannels: sanitizedAccessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels: sanitizedSecretRequestChannels
secretRequestChannels: sanitizedSecretRequestChannels,
isSecretSyncErrorNotificationEnabled,
secretSyncErrorChannels: sanitizedSecretSyncErrorChannels
},
tx
);
@@ -1641,7 +1650,9 @@ export const projectServiceFactory = ({
isAccessRequestNotificationEnabled,
accessRequestChannels: sanitizedAccessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels: sanitizedSecretRequestChannels
secretRequestChannels: sanitizedSecretRequestChannels,
isSecretSyncErrorNotificationEnabled,
secretSyncErrorChannels: sanitizedSecretSyncErrorChannels
},
tx
);
@@ -1651,6 +1662,7 @@ export const projectServiceFactory = ({
...updatedWorkflowIntegration,
accessRequestChannels: sanitizedAccessRequestChannels,
secretRequestChannels: sanitizedSecretRequestChannels,
secretSyncErrorChannels: sanitizedSecretSyncErrorChannels,
integrationId: slackIntegration.id,
integration: WorkflowIntegration.SLACK
} as const;
@@ -184,8 +184,10 @@ export type TUpdateProjectWorkflowIntegration = (
integration: WorkflowIntegration.SLACK;
isAccessRequestNotificationEnabled: boolean;
isSecretRequestNotificationEnabled: boolean;
isSecretSyncErrorNotificationEnabled: boolean;
accessRequestChannels?: string;
secretRequestChannels?: string;
secretSyncErrorChannels?: string;
}
| {
integrationId: string;
@@ -932,7 +932,9 @@ export const secretSyncQueueFactory = ({
break;
}
const syncPath = `/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`;
const baseProjectPath = `/projects/secret-management/${projectId}`;
const overviewPath = `${baseProjectPath}/overview`;
const syncPath = `${baseProjectPath}/integrations/secret-syncs/${destination}/${secretSync.id}`;
const notifications = [
triggerWorkflowIntegrationNotification({
@@ -944,10 +946,14 @@ export const secretSyncQueueFactory = ({
syncDestination,
failureMessage: failureMessage || "An unknown error occurred",
syncUrl: `${appCfg.SITE_URL}${syncPath}`,
syncActionLabel: actionLabel
syncActionLabel: actionLabel,
environment: environment?.name || "-",
secretPath: folder?.path || "-",
projectName: project.name,
projectPath: overviewPath
}
},
projectId: project.id
projectId
},
dependencies: {
projectDAL,
+47 -23
View File
@@ -76,6 +76,7 @@ View the complete details <${appCfg.SITE_URL}/projects/secret-management/${paylo
];
return {
headerBlocks: [],
payloadMessage: messageBody,
payloadBlocks,
color: COMPANY_BRAND_COLOR
@@ -83,20 +84,15 @@ View the complete details <${appCfg.SITE_URL}/projects/secret-management/${paylo
}
case TriggerFeature.ACCESS_REQUEST: {
const { payload } = notification;
const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${
payload.isTemporary ? "temporary" : "permanent"
} access to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}.
The following permissions are requested: ${payload.permissions.join(", ")}
const projectUrl = `${appCfg.SITE_URL}${payload.projectPath}`;
const accessType = payload.isTemporary ? "temporary" : "permanent";
const permissionsFormatted = payload.permissions.map((p) => `*${p}*`).join(", ");
View the request and approve or deny it <${payload.approvalUrl}|here>.${
payload.note
? `
User Note: ${payload.note}`
: ""
const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${accessType} access to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}.\n\nThe following permissions are requested: ${payload.permissions.join(", ")}${
payload.note ? `\n\nUser note\n${payload.note}` : ""
}`;
const payloadBlocks = [
const headerBlocks = [
{
type: "header",
text: {
@@ -104,17 +100,38 @@ User Note: ${payload.note}`
text: "New access approval request pending for review",
emoji: true
}
},
}
];
const payloadBlocks = [
{
type: "section",
text: {
type: "mrkdwn",
text: messageBody
text: `*${payload.requesterFullName}* (${payload.requesterEmail}) has requested *${accessType}* access to *${payload.secretPath}* in the *${payload.environment}* environment of *<${projectUrl}|${payload.projectName}>*.\n\nThe following permissions are requested: ${permissionsFormatted}${
payload.note ? `\n\n*User note*\n${payload.note}` : ""
}`
}
},
{
type: "actions",
elements: [
{
type: "button",
text: {
type: "plain_text",
text: "View request",
emoji: true
},
style: "primary",
url: payload.approvalUrl
}
]
}
];
return {
headerBlocks,
payloadMessage: messageBody,
payloadBlocks,
color: COMPANY_BRAND_COLOR
@@ -125,7 +142,7 @@ User Note: ${payload.note}`
const messageBody = `${payload.editorFullName} (${payload.editorEmail}) has updated the ${
payload.isTemporary ? "temporary" : "permanent"
} access request from ${payload.requesterFullName} (${payload.requesterEmail}) to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}.
The following permissions are requested: ${payload.permissions.join(", ")}
View the request and approve or deny it <${payload.approvalUrl}|here>.${
@@ -154,6 +171,7 @@ Editor Note: ${payload.editNote}`
];
return {
headerBlocks: [],
payloadMessage: messageBody,
payloadBlocks,
color: COMPANY_BRAND_COLOR
@@ -161,24 +179,26 @@ Editor Note: ${payload.editNote}`
}
case TriggerFeature.SECRET_SYNC_ERROR: {
const { payload } = notification;
const messageBody = `${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}
const projectUrl = `${appCfg.SITE_URL}${payload.projectPath}`;
const messageBody = `Secret sync ${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}\n\n\nEnvironment: ${payload.environment}\n\n\nSecret Path: ${payload.secretPath}\n\n\nProject: ${payload.projectName} (${projectUrl})\n\n\nReason:\n${payload.failureMessage}`;
Sync Error: ${payload.failureMessage}`;
const payloadBlocks = [
const headerBlocks = [
{
type: "header",
text: {
type: "plain_text",
text: `${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}`,
text: `Secret sync ${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}`,
emoji: true
}
},
}
];
const payloadBlocks = [
{
type: "section",
text: {
type: "mrkdwn",
text: `*Sync Error:* ${payload.failureMessage}`
text: `*Environment*\n${payload.environment}\n\n\n*Secret Path*\n${payload.secretPath}\n\n\n*Project*\n<${projectUrl}|${payload.projectName}>\n\n\n*Reason*\n${payload.failureMessage}`
}
},
{
@@ -188,9 +208,10 @@ Sync Error: ${payload.failureMessage}`;
type: "button",
text: {
type: "plain_text",
text: `Open ${payload.syncName}`,
text: "Open secret sync",
emoji: true
},
style: "primary",
url: payload.syncUrl
}
]
@@ -199,6 +220,7 @@ Sync Error: ${payload.failureMessage}`;
return {
payloadMessage: messageBody,
headerBlocks,
payloadBlocks,
color: ERROR_COLOR
};
@@ -227,14 +249,16 @@ export const sendSlackNotification = async ({
}).toString("utf8");
const slackWebClient = new WebClient(botKey);
const { payloadMessage, payloadBlocks, color } = buildSlackPayload(notification);
const { payloadMessage, payloadBlocks, color, headerBlocks } = buildSlackPayload(notification);
for await (const conversationId of targetChannelIds) {
// we send both text and blocks for compatibility with barebone clients
await slackWebClient.chat
.postMessage({
channel: conversationId,
text: payloadMessage,
blocks: headerBlocks,
attachments: [
{
color,