mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 08:28:22 +00:00
feat: integrated secret approval request
This commit is contained in:
Generated
+1
@@ -34,6 +34,7 @@
|
|||||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||||
"@sindresorhus/slugify": "1.1.0",
|
"@sindresorhus/slugify": "1.1.0",
|
||||||
"@slack/oauth": "^3.0.1",
|
"@slack/oauth": "^3.0.1",
|
||||||
|
"@slack/web-api": "^7.3.4",
|
||||||
"@team-plain/typescript-sdk": "^4.6.1",
|
"@team-plain/typescript-sdk": "^4.6.1",
|
||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
"ajv": "^8.12.0",
|
"ajv": "^8.12.0",
|
||||||
|
|||||||
@@ -131,6 +131,7 @@
|
|||||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||||
"@sindresorhus/slugify": "1.1.0",
|
"@sindresorhus/slugify": "1.1.0",
|
||||||
"@slack/oauth": "^3.0.1",
|
"@slack/oauth": "^3.0.1",
|
||||||
|
"@slack/web-api": "^7.3.4",
|
||||||
"@team-plain/typescript-sdk": "^4.6.1",
|
"@team-plain/typescript-sdk": "^4.6.1",
|
||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
"ajv": "^8.12.0",
|
"ajv": "^8.12.0",
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { TSecretApprovalRequests } from "@app/db/schemas";
|
import { TSecretApprovalRequests } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
|
||||||
|
import { TSlackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal";
|
||||||
|
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
|
||||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
|
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
|
||||||
|
|
||||||
@@ -13,6 +18,69 @@ type TSendApprovalEmails = {
|
|||||||
secretApprovalRequest: TSecretApprovalRequests;
|
secretApprovalRequest: TSecretApprovalRequests;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TTriggerSecretApprovalSlackNotif = {
|
||||||
|
environment: string;
|
||||||
|
projectId: string;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectWithOrg">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
secretApprovalRequest: TSecretApprovalRequests;
|
||||||
|
secretPath: string;
|
||||||
|
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findOne">;
|
||||||
|
userDAL: Pick<TUserDALFactory, "findById">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const triggerSecretApprovalSlackNotif = async ({
|
||||||
|
projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
secretApprovalRequest,
|
||||||
|
slackIntegrationDAL,
|
||||||
|
userDAL,
|
||||||
|
environment,
|
||||||
|
secretPath
|
||||||
|
}: TTriggerSecretApprovalSlackNotif) => {
|
||||||
|
// construct message here
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const project = await projectDAL.findProjectWithOrg(projectId);
|
||||||
|
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
|
||||||
|
|
||||||
|
const messageBody = `A secret approval request has been opened by ${user.email}.
|
||||||
|
*Environment*: ${environment}
|
||||||
|
*Secret path*: ${secretPath || "/"}
|
||||||
|
|
||||||
|
View the complete details <${appCfg.SITE_URL}/project/${project.id}/approval?requestId=${
|
||||||
|
secretApprovalRequest.id
|
||||||
|
}|here>.`;
|
||||||
|
|
||||||
|
const payloadBlocks = [
|
||||||
|
{
|
||||||
|
type: "header",
|
||||||
|
text: {
|
||||||
|
type: "plain_text",
|
||||||
|
text: "Secret approval request",
|
||||||
|
emoji: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "section",
|
||||||
|
text: {
|
||||||
|
type: "mrkdwn",
|
||||||
|
text: messageBody
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
await triggerSlackNotification({
|
||||||
|
projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
slackIntegrationDAL,
|
||||||
|
payloadMessage: messageBody,
|
||||||
|
payloadBlocks,
|
||||||
|
feature: SlackTriggerFeature.SECRET_APPROVAL
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const sendApprovalEmailsFn = async ({
|
export const sendApprovalEmailsFn = async ({
|
||||||
secretApprovalPolicyDAL,
|
secretApprovalPolicyDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
|
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
|
||||||
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
||||||
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||||
|
import { TSlackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/pr
|
|||||||
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
|
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
|
||||||
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
|
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
|
||||||
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
|
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
|
||||||
import { sendApprovalEmailsFn } from "./secret-approval-request-fns";
|
import { sendApprovalEmailsFn, triggerSecretApprovalSlackNotif } from "./secret-approval-request-fns";
|
||||||
import { TSecretApprovalRequestReviewerDALFactory } from "./secret-approval-request-reviewer-dal";
|
import { TSecretApprovalRequestReviewerDALFactory } from "./secret-approval-request-reviewer-dal";
|
||||||
import { TSecretApprovalRequestSecretDALFactory } from "./secret-approval-request-secret-dal";
|
import { TSecretApprovalRequestSecretDALFactory } from "./secret-approval-request-secret-dal";
|
||||||
import {
|
import {
|
||||||
@@ -89,7 +90,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
|
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
userDAL: Pick<TUserDALFactory, "find" | "findOne">;
|
userDAL: Pick<TUserDALFactory, "find" | "findOne" | "findById">;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||||
projectDAL: Pick<
|
projectDAL: Pick<
|
||||||
TProjectDALFactory,
|
TProjectDALFactory,
|
||||||
@@ -104,6 +105,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
|
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
|
||||||
|
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findOne">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -132,6 +134,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
secretVersionTagV2BridgeDAL,
|
secretVersionTagV2BridgeDAL,
|
||||||
|
slackIntegrationDAL,
|
||||||
licenseService
|
licenseService
|
||||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||||
@@ -1069,6 +1072,18 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
return { ...doc, commits: approvalCommits };
|
return { ...doc, commits: approvalCommits };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||||
|
await triggerSecretApprovalSlackNotif({
|
||||||
|
projectId,
|
||||||
|
secretPath: policy.secretPath as string,
|
||||||
|
environment: env.name,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
secretApprovalRequest,
|
||||||
|
slackIntegrationDAL,
|
||||||
|
userDAL
|
||||||
|
});
|
||||||
|
|
||||||
await sendApprovalEmailsFn({
|
await sendApprovalEmailsFn({
|
||||||
projectDAL,
|
projectDAL,
|
||||||
secretApprovalPolicyDAL,
|
secretApprovalPolicyDAL,
|
||||||
@@ -1331,6 +1346,18 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
return { ...doc, commits: approvalCommits };
|
return { ...doc, commits: approvalCommits };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||||
|
await triggerSecretApprovalSlackNotif({
|
||||||
|
secretPath: policy.secretPath as string,
|
||||||
|
environment: env.name,
|
||||||
|
projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
secretApprovalRequest,
|
||||||
|
slackIntegrationDAL,
|
||||||
|
userDAL
|
||||||
|
});
|
||||||
|
|
||||||
await sendApprovalEmailsFn({
|
await sendApprovalEmailsFn({
|
||||||
projectDAL,
|
projectDAL,
|
||||||
secretApprovalPolicyDAL,
|
secretApprovalPolicyDAL,
|
||||||
|
|||||||
@@ -876,7 +876,8 @@ export const registerRoutes = async (
|
|||||||
smtpService,
|
smtpService,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
licenseService
|
licenseService,
|
||||||
|
slackIntegrationDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretService = secretServiceFactory({
|
const secretService = secretServiceFactory({
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { Block, WebClient } from "@slack/web-api";
|
||||||
|
|
||||||
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
|
import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
|
||||||
|
import { SlackTriggerFeature } from "./slack-types";
|
||||||
|
|
||||||
|
export const triggerSlackNotification = async ({
|
||||||
|
projectId,
|
||||||
|
payloadBlocks,
|
||||||
|
payloadMessage,
|
||||||
|
slackIntegrationDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
feature
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
payloadBlocks: Block[];
|
||||||
|
payloadMessage: string;
|
||||||
|
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findOne">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
feature: SlackTriggerFeature;
|
||||||
|
}) => {
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
const slackIntegration = await slackIntegrationDAL.findOne({
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!slackIntegration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetChannels: string[] = [];
|
||||||
|
if (feature === SlackTriggerFeature.ACCESS_REQUEST) {
|
||||||
|
targetChannels = slackIntegration.accessRequestChannels?.split(",") || [];
|
||||||
|
if (!targetChannels.length || !slackIntegration.isAccessRequestNotificationEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (feature === SlackTriggerFeature.SECRET_APPROVAL) {
|
||||||
|
targetChannels = slackIntegration.secretRequestChannels?.split(",") || [];
|
||||||
|
if (!targetChannels.length || !slackIntegration.isSecretRequestNotificationEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId: project.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const botKey = orgDataKeyDecryptor({
|
||||||
|
cipherTextBlob: slackIntegration.encryptedBotAccessToken
|
||||||
|
}).toString("utf8");
|
||||||
|
|
||||||
|
const targetChannelSet = new Set<string>(targetChannels);
|
||||||
|
const slackWebClient = new WebClient(botKey);
|
||||||
|
const channelIdsToSendNotif: string[] = [];
|
||||||
|
let cursor;
|
||||||
|
|
||||||
|
do {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const response = await slackWebClient.conversations.list({
|
||||||
|
cursor,
|
||||||
|
limit: 1000,
|
||||||
|
types: "public_channel,private_channel"
|
||||||
|
});
|
||||||
|
|
||||||
|
response.channels?.forEach((channel) => {
|
||||||
|
if (channel.name_normalized && targetChannelSet.has(channel.name_normalized)) {
|
||||||
|
channelIdsToSendNotif.push(channel.id as string);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the cursor for the next page
|
||||||
|
cursor = response.response_metadata?.next_cursor;
|
||||||
|
} while (cursor); // Continue while there is a cursor
|
||||||
|
|
||||||
|
for await (const conversationId of channelIdsToSendNotif) {
|
||||||
|
// we send both text and blocks for compatibility with barebone clients
|
||||||
|
await slackWebClient.chat.postMessage({
|
||||||
|
channel: conversationId,
|
||||||
|
text: payloadMessage,
|
||||||
|
blocks: payloadBlocks
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -102,9 +102,12 @@ export const slackServiceFactory = ({
|
|||||||
slackBotUserId: installation.bot?.userId || ""
|
slackBotUserId: installation.bot?.userId || ""
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// for our use-case we don't need to implement this because this will only be used
|
||||||
|
// when listening for events from slack
|
||||||
fetchInstallation: () => {
|
fetchInstallation: () => {
|
||||||
return {} as never;
|
return {} as never;
|
||||||
},
|
},
|
||||||
|
// for our use-case we don't need to implement this yet
|
||||||
deleteInstallation: () => {
|
deleteInstallation: () => {
|
||||||
return {} as never;
|
return {} as never;
|
||||||
}
|
}
|
||||||
@@ -132,7 +135,7 @@ export const slackServiceFactory = ({
|
|||||||
|
|
||||||
const installer = await getSlackInstaller();
|
const installer = await getSlackInstaller();
|
||||||
const url = await installer.generateInstallUrl({
|
const url = await installer.generateInstallUrl({
|
||||||
scopes: ["chat:write"],
|
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read", "im:read", "mpim:read"],
|
||||||
metadata: JSON.stringify({
|
metadata: JSON.stringify({
|
||||||
projectId: project.id
|
projectId: project.id
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -12,3 +12,8 @@ export type TCompleteSlackIntegrationDTO = {
|
|||||||
slackBotId: string;
|
slackBotId: string;
|
||||||
slackBotUserId: string;
|
slackBotUserId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum SlackTriggerFeature {
|
||||||
|
SECRET_APPROVAL = "secret-approval",
|
||||||
|
ACCESS_REQUEST = "access-request"
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user