mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-18 13:15:39 +00:00
feat(slack-integration): add support for secret sync error notifications and enhance notification handling
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TableName, TMicrosoftTeamsIntegrations } from "@app/db/schemas";
|
||||
import { TProjectMicrosoftTeamsConfigs } from "@app/db/schemas/project-microsoft-teams-configs";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TProjectMicrosoftTeamsConfigDALFactory = ReturnType<typeof projectMicrosoftTeamsConfigDALFactory>;
|
||||
export type TProjectMicrosoftTeamsConfigWithIntegrations = TProjectMicrosoftTeamsConfigs & TMicrosoftTeamsIntegrations;
|
||||
|
||||
export const projectMicrosoftTeamsConfigDALFactory = (db: TDbClient) => {
|
||||
const projectMicrosoftTeamsConfigOrm = ormify(db, TableName.ProjectMicrosoftTeamsConfigs);
|
||||
|
||||
const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => {
|
||||
return (tx || db.replicaNode())(TableName.ProjectMicrosoftTeamsConfigs)
|
||||
return (tx || db.replicaNode())<TProjectMicrosoftTeamsConfigWithIntegrations>(
|
||||
TableName.ProjectMicrosoftTeamsConfigs
|
||||
)
|
||||
.join(
|
||||
TableName.MicrosoftTeamsIntegrations,
|
||||
`${TableName.ProjectMicrosoftTeamsConfigs}.microsoftTeamsIntegrationId`,
|
||||
|
||||
@@ -10,6 +10,8 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
|
||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
|
||||
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { SecretNameSchema } from "@app/server/lib/schemas";
|
||||
import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
@@ -62,8 +64,11 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||
import { TMicrosoftTeamsServiceFactory } from "../microsoft-teams/microsoft-teams-service";
|
||||
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal";
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
||||
|
||||
export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
|
||||
|
||||
@@ -104,6 +109,9 @@ type TSecretSyncQueueFactoryDep = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||
};
|
||||
|
||||
type SecretSyncActionJob = Job<
|
||||
@@ -147,7 +155,10 @@ export const secretSyncQueueFactory = ({
|
||||
licenseService,
|
||||
gatewayService,
|
||||
gatewayV2Service,
|
||||
notificationService
|
||||
notificationService,
|
||||
projectSlackConfigDAL,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
microsoftTeamsService
|
||||
}: TSecretSyncQueueFactoryDep) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
@@ -923,32 +934,57 @@ export const secretSyncQueueFactory = ({
|
||||
|
||||
const syncPath = `/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`;
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
projectAdmins.map((admin) => ({
|
||||
userId: admin.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.SECRET_SYNC_FAILED,
|
||||
title: `Secret Sync Failed to ${actionLabel} Secrets`,
|
||||
body: `Your **${syncDestination}** sync **${name}** failed to complete${failureMessage ? `: \`${failureMessage}\`` : ""}`,
|
||||
link: syncPath
|
||||
}))
|
||||
);
|
||||
const notifications = [
|
||||
triggerWorkflowIntegrationNotification({
|
||||
input: {
|
||||
notification: {
|
||||
type: TriggerFeature.SECRET_SYNC_ERROR,
|
||||
payload: {
|
||||
syncName: name,
|
||||
syncDestination,
|
||||
failureMessage: failureMessage || "An unknown error occurred",
|
||||
syncUrl: `${appCfg.SITE_URL}${syncPath}`,
|
||||
syncActionLabel: actionLabel
|
||||
}
|
||||
},
|
||||
projectId: project.id
|
||||
},
|
||||
dependencies: {
|
||||
projectDAL,
|
||||
projectSlackConfigDAL,
|
||||
kmsService,
|
||||
microsoftTeamsService,
|
||||
projectMicrosoftTeamsConfigDAL
|
||||
}
|
||||
}),
|
||||
notificationService.createUserNotifications(
|
||||
projectAdmins.map((admin) => ({
|
||||
userId: admin.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.SECRET_SYNC_FAILED,
|
||||
title: `Secret Sync Failed to ${actionLabel} Secrets`,
|
||||
body: `Your **${syncDestination}** sync **${name}** failed to complete${failureMessage ? `: \`${failureMessage}\`` : ""}`,
|
||||
link: syncPath
|
||||
}))
|
||||
),
|
||||
smtpService.sendMail({
|
||||
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||
template: SmtpTemplates.SecretSyncFailed,
|
||||
subjectLine: `Secret Sync Failed to ${actionLabel} Secrets`,
|
||||
substitutions: {
|
||||
syncName: name,
|
||||
syncDestination,
|
||||
content: `Your ${syncDestination} Sync named "${name}" failed while attempting to ${action.toLowerCase()} secrets.`,
|
||||
failureMessage,
|
||||
secretPath: folder?.path,
|
||||
environment: environment?.name,
|
||||
projectName: project.name,
|
||||
syncUrl: `${appCfg.SITE_URL}${syncPath}`
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||
template: SmtpTemplates.SecretSyncFailed,
|
||||
subjectLine: `Secret Sync Failed to ${actionLabel} Secrets`,
|
||||
substitutions: {
|
||||
syncName: name,
|
||||
syncDestination,
|
||||
content: `Your ${syncDestination} Sync named "${name}" failed while attempting to ${action.toLowerCase()} secrets.`,
|
||||
failureMessage,
|
||||
secretPath: folder?.path,
|
||||
environment: environment?.name,
|
||||
projectName: project.name,
|
||||
syncUrl: `${appCfg.SITE_URL}${syncPath}`
|
||||
}
|
||||
});
|
||||
await Promise.allSettled(notifications);
|
||||
};
|
||||
|
||||
const queueSecretSyncsSyncSecretsByPath = async ({
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TableName, TProjectSlackConfigs, TSlackIntegrations } from "@app/db/schemas";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TProjectSlackConfigDALFactory = ReturnType<typeof projectSlackConfigDALFactory>;
|
||||
export type TProjectSlackConfigWithIntegrations = TProjectSlackConfigs & TSlackIntegrations;
|
||||
|
||||
export const projectSlackConfigDALFactory = (db: TDbClient) => {
|
||||
const projectSlackConfigOrm = ormify(db, TableName.ProjectSlackConfigs);
|
||||
|
||||
const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => {
|
||||
return (tx || db.replicaNode())(TableName.ProjectSlackConfigs)
|
||||
return (tx || db.replicaNode())<TProjectSlackConfigWithIntegrations>(TableName.ProjectSlackConfigs)
|
||||
.join(
|
||||
TableName.SlackIntegrations,
|
||||
`${TableName.ProjectSlackConfigs}.slackIntegrationId`,
|
||||
|
||||
@@ -8,6 +8,9 @@ import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/ty
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TSendSlackNotificationDTO } from "./slack-types";
|
||||
|
||||
const COMPANY_BRAND_COLOR = "#e0ed34";
|
||||
const ERROR_COLOR = "#e74c3c";
|
||||
|
||||
export const fetchSlackChannels = async (botKey: string) => {
|
||||
const slackChannels: {
|
||||
name: string;
|
||||
@@ -74,7 +77,8 @@ View the complete details <${appCfg.SITE_URL}/projects/secret-management/${paylo
|
||||
|
||||
return {
|
||||
payloadMessage: messageBody,
|
||||
payloadBlocks
|
||||
payloadBlocks,
|
||||
color: COMPANY_BRAND_COLOR
|
||||
};
|
||||
}
|
||||
case TriggerFeature.ACCESS_REQUEST: {
|
||||
@@ -112,7 +116,8 @@ User Note: ${payload.note}`
|
||||
|
||||
return {
|
||||
payloadMessage: messageBody,
|
||||
payloadBlocks
|
||||
payloadBlocks,
|
||||
color: COMPANY_BRAND_COLOR
|
||||
};
|
||||
}
|
||||
case TriggerFeature.ACCESS_REQUEST_UPDATED: {
|
||||
@@ -150,7 +155,52 @@ Editor Note: ${payload.editNote}`
|
||||
|
||||
return {
|
||||
payloadMessage: messageBody,
|
||||
payloadBlocks
|
||||
payloadBlocks,
|
||||
color: COMPANY_BRAND_COLOR
|
||||
};
|
||||
}
|
||||
case TriggerFeature.SECRET_SYNC_ERROR: {
|
||||
const { payload } = notification;
|
||||
const messageBody = `${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}
|
||||
|
||||
Sync Error: ${payload.failureMessage}`;
|
||||
|
||||
const payloadBlocks = [
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: `${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}`,
|
||||
emoji: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*Sync Error:* ${payload.failureMessage}`
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "actions",
|
||||
elements: [
|
||||
{
|
||||
type: "button",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: `Open ${payload.syncName}`,
|
||||
emoji: true
|
||||
},
|
||||
url: payload.syncUrl
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
payloadMessage: messageBody,
|
||||
payloadBlocks,
|
||||
color: ERROR_COLOR
|
||||
};
|
||||
}
|
||||
default: {
|
||||
@@ -177,7 +227,7 @@ export const sendSlackNotification = async ({
|
||||
}).toString("utf8");
|
||||
const slackWebClient = new WebClient(botKey);
|
||||
|
||||
const { payloadMessage, payloadBlocks } = buildSlackPayload(notification);
|
||||
const { payloadMessage, payloadBlocks, color } = buildSlackPayload(notification);
|
||||
|
||||
for await (const conversationId of targetChannelIds) {
|
||||
// we send both text and blocks for compatibility with barebone clients
|
||||
@@ -185,7 +235,12 @@ export const sendSlackNotification = async ({
|
||||
.postMessage({
|
||||
channel: conversationId,
|
||||
text: payloadMessage,
|
||||
blocks: payloadBlocks
|
||||
attachments: [
|
||||
{
|
||||
color,
|
||||
blocks: payloadBlocks
|
||||
}
|
||||
]
|
||||
})
|
||||
.catch((err) => logger.error(err));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user