Merge pull request #2358 from Infisical/feat/native-slack-integration

feat: native slack integration
This commit is contained in:
Maidul Islam
2024-09-11 09:36:25 -04:00
committed by GitHub
77 changed files with 3665 additions and 37 deletions
+19
View File
@@ -208,6 +208,23 @@ export const kmsServiceFactory = ({
return org.kmsDefaultKeyId;
};
const encryptWithRootKey = async () => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
return ({ plainText }: { plainText: Buffer }) => {
const encryptedPlainTextBlob = cipher.encrypt(plainText, ROOT_ENCRYPTION_KEY);
return Promise.resolve({ cipherTextBlob: encryptedPlainTextBlob });
};
};
const decryptWithRootKey = async () => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
return ({ cipherTextBlob }: { cipherTextBlob: Buffer }) => {
const decryptedBlob = cipher.decrypt(cipherTextBlob, ROOT_ENCRYPTION_KEY);
return Promise.resolve(decryptedBlob);
};
};
const decryptWithKmsKey = async ({
kmsId,
depth = 0
@@ -808,6 +825,8 @@ export const kmsServiceFactory = ({
decryptWithKmsKey,
encryptWithInputKey,
decryptWithInputKey,
encryptWithRootKey,
decryptWithRootKey,
getOrgKmsKeyId,
getProjectSecretManagerKmsKeyId,
updateProjectSecretManagerKmsKey,
+119 -2
View File
@@ -34,6 +34,8 @@ import { TProjectUserMembershipRoleDALFactory } from "../project-membership/proj
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { getPredefinedRoles } from "../project-role/project-role-fns";
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TProjectDALFactory } from "./project-dal";
import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns";
@@ -43,6 +45,7 @@ import {
TDeleteProjectDTO,
TGetProjectDTO,
TGetProjectKmsKey,
TGetProjectSlackConfig,
TListProjectAlertsDTO,
TListProjectCasDTO,
TListProjectCertificateTemplatesDTO,
@@ -54,6 +57,7 @@ import {
TUpdateProjectDTO,
TUpdateProjectKmsDTO,
TUpdateProjectNameDTO,
TUpdateProjectSlackConfig,
TUpdateProjectVersionLimitDTO,
TUpgradeProjectDTO
} from "./project-types";
@@ -76,6 +80,8 @@ type TProjectServiceFactoryDep = {
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
@@ -126,7 +132,9 @@ export const projectServiceFactory = ({
pkiAlertDAL,
keyStore,
kmsService,
projectBotDAL
projectBotDAL,
projectSlackConfigDAL,
slackIntegrationDAL
}: TProjectServiceFactoryDep) => {
/*
* Create workspace. Make user the admin
@@ -909,6 +917,113 @@ export const projectServiceFactory = ({
return { secretManagerKmsKey: kmsKey };
};
const getProjectSlackConfig = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId
}: TGetProjectSlackConfig) => {
const project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({
message: "Project not found"
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
return projectSlackConfigDAL.findOne({
projectId: project.id
});
};
const updateProjectSlackConfig = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId,
slackIntegrationId,
isAccessRequestNotificationEnabled,
accessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels
}: TUpdateProjectSlackConfig) => {
const project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({
message: "Project not found"
});
}
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(slackIntegrationId);
if (!slackIntegration) {
throw new NotFoundError({
message: "Slack integration not found"
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
if (slackIntegration.orgId !== project.orgId) {
throw new BadRequestError({
message: "Selected slack integration is not in the same organization"
});
}
return projectSlackConfigDAL.transaction(async (tx) => {
const slackConfig = await projectSlackConfigDAL.findOne(
{
projectId
},
tx
);
if (slackConfig) {
return projectSlackConfigDAL.updateById(
slackConfig.id,
{
slackIntegrationId,
isAccessRequestNotificationEnabled,
accessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels
},
tx
);
}
return projectSlackConfigDAL.create(
{
projectId,
slackIntegrationId,
isAccessRequestNotificationEnabled,
accessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels
},
tx
);
});
};
return {
createProject,
deleteProject,
@@ -929,6 +1044,8 @@ export const projectServiceFactory = ({
updateProjectKmsKey,
getProjectKmsBackup,
loadProjectKmsBackup,
getProjectKmsKeys
getProjectKmsKeys,
getProjectSlackConfig,
updateProjectSlackConfig
};
};
@@ -123,3 +123,13 @@ export type TLoadProjectKmsBackupDTO = {
export type TGetProjectKmsKey = TProjectPermission;
export type TListProjectCertificateTemplatesDTO = TProjectPermission;
export type TGetProjectSlackConfig = TProjectPermission;
export type TUpdateProjectSlackConfig = {
slackIntegrationId: string;
isAccessRequestNotificationEnabled: boolean;
accessRequestChannels: string;
isSecretRequestNotificationEnabled: boolean;
secretRequestChannels: string;
} & TProjectPermission;
@@ -0,0 +1,25 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TProjectSlackConfigDALFactory = ReturnType<typeof projectSlackConfigDALFactory>;
export const projectSlackConfigDALFactory = (db: TDbClient) => {
const projectSlackConfigOrm = ormify(db, TableName.ProjectSlackConfigs);
const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => {
return (tx || db.replicaNode())(TableName.ProjectSlackConfigs)
.join(
TableName.SlackIntegrations,
`${TableName.ProjectSlackConfigs}.slackIntegrationId`,
`${TableName.SlackIntegrations}.id`
)
.where("projectId", "=", projectId)
.select(selectAllTableCols(TableName.ProjectSlackConfigs), selectAllTableCols(TableName.SlackIntegrations))
.first();
};
return { ...projectSlackConfigOrm, getIntegrationDetailsByProject };
};
@@ -0,0 +1,16 @@
import z from "zod";
export const validateSlackChannelsField = z
.string()
.trim()
.default("")
.transform((data) => {
if (data === "") return "";
return data
.split(",")
.map((id) => id.trim())
.join(", ");
})
.refine((data) => data.split(",").length <= 20, {
message: "You can only select up to 20 slack channels"
});
+177
View File
@@ -0,0 +1,177 @@
import { WebClient } from "@slack/web-api";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal";
import { SlackTriggerFeature, TSlackNotification } from "./slack-types";
export const fetchSlackChannels = async (botKey: string) => {
const slackChannels: {
name: string;
id: string;
}[] = [];
const slackWebClient = new WebClient(botKey);
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) =>
slackChannels.push({
name: channel.name_normalized as string,
id: 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
return slackChannels;
};
const buildSlackPayload = (notification: TSlackNotification) => {
const appCfg = getConfig();
switch (notification.type) {
case SlackTriggerFeature.SECRET_APPROVAL: {
const { payload } = notification;
const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
*Environment*: ${payload.environment}
*Secret path*: ${payload.secretPath || "/"}
View the complete details <${appCfg.SITE_URL}/project/${payload.projectId}/approval?requestId=${
payload.requestId
}|here>.`;
const payloadBlocks = [
{
type: "header",
text: {
type: "plain_text",
text: "Secret approval request",
emoji: true
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: messageBody
}
}
];
return {
payloadMessage: messageBody,
payloadBlocks
};
}
case SlackTriggerFeature.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(", ")}
View the request and approve or deny it <${payload.approvalUrl}|here>.`;
const payloadBlocks = [
{
type: "header",
text: {
type: "plain_text",
text: "New access approval request pending for review",
emoji: true
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: messageBody
}
}
];
return {
payloadMessage: messageBody,
payloadBlocks
};
}
default: {
throw new BadRequestError({
message: "Slack notification type not supported."
});
}
}
};
export const triggerSlackNotification = async ({
projectId,
notification,
projectSlackConfigDAL,
projectDAL,
kmsService
}: {
projectId: string;
notification: TSlackNotification;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
projectDAL: Pick<TProjectDALFactory, "findById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}) => {
const { payloadMessage, payloadBlocks } = buildSlackPayload(notification);
const project = await projectDAL.findById(projectId);
const slackIntegration = await projectSlackConfigDAL.getIntegrationDetailsByProject(project.id);
if (!slackIntegration) {
return;
}
let targetChannelIds: string[] = [];
if (notification.type === SlackTriggerFeature.ACCESS_REQUEST) {
targetChannelIds = slackIntegration.accessRequestChannels?.split(", ") || [];
if (!targetChannelIds.length || !slackIntegration.isAccessRequestNotificationEnabled) {
return;
}
} else if (notification.type === SlackTriggerFeature.SECRET_APPROVAL) {
targetChannelIds = slackIntegration.secretRequestChannels?.split(", ") || [];
if (!targetChannelIds.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 slackWebClient = new WebClient(botKey);
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: payloadBlocks
})
.catch((err) => logger.error(err));
}
};
@@ -0,0 +1,56 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TSlackIntegrations, TWorkflowIntegrations } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TSlackIntegrationDALFactory = ReturnType<typeof slackIntegrationDALFactory>;
export const slackIntegrationDALFactory = (db: TDbClient) => {
const slackIntegrationOrm = ormify(db, TableName.SlackIntegrations);
const findByIdWithWorkflowIntegrationDetails = async (id: string, tx?: Knex) => {
try {
return await (tx || db.replicaNode())(TableName.SlackIntegrations)
.join(
TableName.WorkflowIntegrations,
`${TableName.SlackIntegrations}.id`,
`${TableName.WorkflowIntegrations}.id`
)
.select(selectAllTableCols(TableName.SlackIntegrations))
.select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations))
.select(db.ref("description").withSchema(TableName.WorkflowIntegrations))
.select(db.ref("integration").withSchema(TableName.WorkflowIntegrations))
.select(db.ref("slug").withSchema(TableName.WorkflowIntegrations))
.where(`${TableName.WorkflowIntegrations}.id`, "=", id)
.first();
} catch (error) {
throw new DatabaseError({ error, name: "Find by ID with Workflow integration details" });
}
};
const findWithWorkflowIntegrationDetails = async (
filter: Partial<TSlackIntegrations> & Partial<TWorkflowIntegrations>,
tx?: Knex
) => {
try {
return await (tx || db.replicaNode())(TableName.SlackIntegrations)
.join(
TableName.WorkflowIntegrations,
`${TableName.SlackIntegrations}.id`,
`${TableName.WorkflowIntegrations}.id`
)
.select(selectAllTableCols(TableName.SlackIntegrations))
.select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations))
.select(db.ref("description").withSchema(TableName.WorkflowIntegrations))
.select(db.ref("integration").withSchema(TableName.WorkflowIntegrations))
.select(db.ref("slug").withSchema(TableName.WorkflowIntegrations))
.where(filter);
} catch (error) {
throw new DatabaseError({ error, name: "Find with Workflow integration details" });
}
};
return { ...slackIntegrationOrm, findByIdWithWorkflowIntegrationDetails, findWithWorkflowIntegrationDetails };
};
+463
View File
@@ -0,0 +1,463 @@
import { ForbiddenError } from "@casl/ability";
import { InstallProvider } from "@slack/oauth";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { getServerCfg } from "../super-admin/super-admin-service";
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
import { WorkflowIntegration } from "../workflow-integration/workflow-integration-types";
import { fetchSlackChannels } from "./slack-fns";
import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
import {
TCompleteSlackIntegrationDTO,
TDeleteSlackIntegrationDTO,
TGetReinstallUrlDTO,
TGetSlackInstallUrlDTO,
TGetSlackIntegrationByIdDTO,
TGetSlackIntegrationByOrgDTO,
TGetSlackIntegrationChannelsDTO,
TReinstallSlackIntegrationDTO,
TUpdateSlackIntegrationDTO
} from "./slack-types";
type TSlackServiceFactoryDep = {
slackIntegrationDAL: Pick<
TSlackIntegrationDALFactory,
| "deleteById"
| "updateById"
| "create"
| "findByIdWithWorkflowIntegrationDetails"
| "findWithWorkflowIntegrationDetails"
>;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithRootKey" | "decryptWithRootKey">;
workflowIntegrationDAL: Pick<TWorkflowIntegrationDALFactory, "transaction" | "create" | "updateById" | "deleteById">;
};
export type TSlackServiceFactory = ReturnType<typeof slackServiceFactory>;
export const slackServiceFactory = ({
permissionService,
slackIntegrationDAL,
kmsService,
workflowIntegrationDAL
}: TSlackServiceFactoryDep) => {
const completeSlackIntegration = async ({
orgId,
slug,
description,
teamId,
teamName,
slackUserId,
slackAppId,
botAccessToken,
slackBotId,
slackBotUserId
}: TCompleteSlackIntegrationDTO) => {
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
orgId,
type: KmsDataKey.Organization
});
const { cipherTextBlob: encryptedBotAccessToken } = orgDataKeyEncryptor({
plainText: Buffer.from(botAccessToken, "utf8")
});
await workflowIntegrationDAL.transaction(async (tx) => {
const workflowIntegration = await workflowIntegrationDAL.create(
{
description,
orgId,
slug,
integration: WorkflowIntegration.SLACK
},
tx
);
await slackIntegrationDAL.create(
{
// @ts-expect-error id is kept as fixed because it is always equal to the workflow integration ID
id: workflowIntegration.id,
teamId,
teamName,
slackUserId,
slackAppId,
slackBotId,
slackBotUserId,
encryptedBotAccessToken
},
tx
);
});
};
const reinstallSlackIntegration = async ({
id,
teamId,
teamName,
slackUserId,
slackAppId,
botAccessToken,
slackBotId,
slackBotUserId
}: TReinstallSlackIntegrationDTO) => {
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
if (!slackIntegration) {
throw new NotFoundError({
message: "Slack integration not found"
});
}
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
orgId: slackIntegration.orgId,
type: KmsDataKey.Organization
});
const { cipherTextBlob: encryptedBotAccessToken } = orgDataKeyEncryptor({
plainText: Buffer.from(botAccessToken, "utf8")
});
await slackIntegrationDAL.updateById(id, {
teamId,
teamName,
slackUserId,
slackAppId,
slackBotId,
slackBotUserId,
encryptedBotAccessToken
});
};
const getSlackInstaller = async () => {
const appCfg = getConfig();
const serverCfg = await getServerCfg();
let slackClientId = appCfg.WORKFLOW_SLACK_CLIENT_ID as string;
let slackClientSecret = appCfg.WORKFLOW_SLACK_CLIENT_SECRET as string;
const decrypt = await kmsService.decryptWithRootKey();
if (serverCfg.encryptedSlackClientId) {
slackClientId = (await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientId) })).toString();
}
if (serverCfg.encryptedSlackClientSecret) {
slackClientSecret = (
await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientSecret) })
).toString();
}
if (!slackClientId || !slackClientSecret) {
throw new BadRequestError({
message: `Invalid Slack configuration. ${
appCfg.isCloud
? "Please contact the Infisical team."
: "Contact your instance admin to setup Slack integration in the Admin settings. Your configuration is missing Slack client ID and secret."
}`
});
}
return new InstallProvider({
clientId: slackClientId,
clientSecret: slackClientSecret,
stateSecret: appCfg.AUTH_SECRET,
legacyStateVerification: true,
installationStore: {
storeInstallation: async (installation) => {
if (installation.isEnterpriseInstall && installation.enterprise?.id) {
throw new BadRequestError({
message: "Enterprise not yet supported"
});
}
const metadata = JSON.parse(installation.metadata || "") as {
id?: string;
orgId: string;
slug: string;
description?: string;
};
if (metadata.id) {
return reinstallSlackIntegration({
id: metadata.id,
teamId: installation.team?.id || "",
teamName: installation.team?.name || "",
slackUserId: installation.user.id,
slackAppId: installation.appId || "",
botAccessToken: installation.bot?.token || "",
slackBotId: installation.bot?.id || "",
slackBotUserId: installation.bot?.userId || ""
});
}
return completeSlackIntegration({
orgId: metadata.orgId,
slug: metadata.slug,
description: metadata.description,
teamId: installation.team?.id || "",
teamName: installation.team?.name || "",
slackUserId: installation.user.id,
slackAppId: installation.appId || "",
botAccessToken: installation.bot?.token || "",
slackBotId: installation.bot?.id || "",
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: () => {
return {} as never;
},
// for our use-case we don't need to implement this yet
deleteInstallation: () => {
return {} as never;
}
}
});
};
const getInstallUrl = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
slug,
description
}: TGetSlackInstallUrlDTO) => {
const appCfg = getConfig();
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
const installer = await getSlackInstaller();
const url = await installer.generateInstallUrl({
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read"],
metadata: JSON.stringify({
slug,
description,
orgId: actorOrgId
}),
redirectUri: `${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect`
});
return url;
};
const getReinstallUrl = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetReinstallUrlDTO) => {
const appCfg = getConfig();
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
if (!slackIntegration) {
throw new NotFoundError({
message: "Slack integration not found"
});
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
slackIntegration.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
const installer = await getSlackInstaller();
const url = await installer.generateInstallUrl({
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read"],
metadata: JSON.stringify({
id,
orgId: slackIntegration.orgId
}),
redirectUri: `${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect`
});
return url;
};
const getSlackIntegrationsByOrg = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod
}: TGetSlackIntegrationByOrgDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
const slackIntegrations = await slackIntegrationDAL.findWithWorkflowIntegrationDetails({
orgId: actorOrgId
});
return slackIntegrations;
};
const getSlackIntegrationById = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
id
}: TGetSlackIntegrationByIdDTO) => {
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
if (!slackIntegration) {
throw new NotFoundError({
message: "Slack integration not found."
});
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
slackIntegration.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
return slackIntegration;
};
const getSlackIntegrationChannels = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
id
}: TGetSlackIntegrationChannelsDTO) => {
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
if (!slackIntegration) {
throw new NotFoundError({
message: "Slack integration not found."
});
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
slackIntegration.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
orgId: slackIntegration.orgId,
type: KmsDataKey.Organization
});
const botKey = orgDataKeyDecryptor({
cipherTextBlob: slackIntegration.encryptedBotAccessToken
}).toString("utf8");
return fetchSlackChannels(botKey);
};
const updateSlackIntegration = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
id,
slug,
description
}: TUpdateSlackIntegrationDTO) => {
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
if (!slackIntegration) {
throw new NotFoundError({
message: "Slack integration not found"
});
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
slackIntegration.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
return workflowIntegrationDAL.transaction(async (tx) => {
await workflowIntegrationDAL.updateById(
slackIntegration.id,
{
slug,
description
},
tx
);
const updatedIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(
slackIntegration.id,
tx
);
return updatedIntegration!;
});
};
const deleteSlackIntegration = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
id
}: TDeleteSlackIntegrationDTO) => {
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
if (!slackIntegration) {
throw new NotFoundError({
message: "Slack integration not found"
});
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
slackIntegration.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
await workflowIntegrationDAL.deleteById(id);
return slackIntegration;
};
return {
getInstallUrl,
getReinstallUrl,
getSlackIntegrationsByOrg,
getSlackIntegrationById,
completeSlackIntegration,
getSlackInstaller,
updateSlackIntegration,
deleteSlackIntegration,
getSlackIntegrationChannels
};
};
+79
View File
@@ -0,0 +1,79 @@
import { TOrgPermission } from "@app/lib/types";
export type TGetSlackInstallUrlDTO = {
slug: string;
description?: string;
} & Omit<TOrgPermission, "orgId">;
export type TGetReinstallUrlDTO = {
id: string;
} & Omit<TOrgPermission, "orgId">;
export type TGetSlackIntegrationByOrgDTO = Omit<TOrgPermission, "orgId">;
export type TGetSlackIntegrationByIdDTO = { id: string } & Omit<TOrgPermission, "orgId">;
export type TGetSlackIntegrationChannelsDTO = { id: string } & Omit<TOrgPermission, "orgId">;
export type TUpdateSlackIntegrationDTO = { id: string; slug?: string; description?: string } & Omit<
TOrgPermission,
"orgId"
>;
export type TDeleteSlackIntegrationDTO = {
id: string;
} & Omit<TOrgPermission, "orgId">;
export type TCompleteSlackIntegrationDTO = {
orgId: string;
slug: string;
description?: string;
teamId: string;
teamName: string;
slackUserId: string;
slackAppId: string;
botAccessToken: string;
slackBotId: string;
slackBotUserId: string;
};
export type TReinstallSlackIntegrationDTO = {
id: string;
teamId: string;
teamName: string;
slackUserId: string;
slackAppId: string;
botAccessToken: string;
slackBotId: string;
slackBotUserId: string;
};
export enum SlackTriggerFeature {
SECRET_APPROVAL = "secret-approval",
ACCESS_REQUEST = "access-request"
}
export type TSlackNotification =
| {
type: SlackTriggerFeature.SECRET_APPROVAL;
payload: {
userEmail: string;
environment: string;
secretPath: string;
requestId: string;
projectId: string;
};
}
| {
type: SlackTriggerFeature.ACCESS_REQUEST;
payload: {
requesterFullName: string;
requesterEmail: string;
isTemporary: boolean;
secretPath: string;
environment: string;
projectName: string;
permissions: string[];
approvalUrl: string;
};
};
@@ -6,10 +6,11 @@ import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError } from "@app/lib/errors";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TAuthLoginFactory } from "../auth/auth-login-service";
import { AuthMethod } from "../auth/auth-type";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgServiceFactory } from "../org/org-service";
import { TUserDALFactory } from "../user/user-dal";
import { TSuperAdminDALFactory } from "./super-admin-dal";
@@ -19,6 +20,7 @@ type TSuperAdminServiceFactoryDep = {
serverCfgDAL: TSuperAdminDALFactory;
userDAL: TUserDALFactory;
authService: Pick<TAuthLoginFactory, "generateUserTokens">;
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey">;
orgService: Pick<TOrgServiceFactory, "createOrganization">;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
@@ -39,6 +41,7 @@ export const superAdminServiceFactory = ({
authService,
orgService,
keyStore,
kmsService,
licenseService
}: TSuperAdminServiceFactoryDep) => {
const initServerCfg = async () => {
@@ -82,7 +85,12 @@ export const superAdminServiceFactory = ({
return newCfg;
};
const updateServerCfg = async (data: TSuperAdminUpdate, userId: string) => {
const updateServerCfg = async (
data: TSuperAdminUpdate & { slackClientId?: string; slackClientSecret?: string },
userId: string
) => {
const updatedData = data;
if (data.enabledLoginMethods) {
const superAdminUser = await userDAL.findById(userId);
const loginMethodToAuthMethod = {
@@ -113,7 +121,27 @@ export const superAdminServiceFactory = ({
});
}
}
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, data);
const encryptWithRoot = await kmsService.encryptWithRootKey();
if (data.slackClientId) {
const { cipherTextBlob: encryptedClientId } = await encryptWithRoot({
plainText: Buffer.from(data.slackClientId)
});
updatedData.encryptedSlackClientId = encryptedClientId;
updatedData.slackClientId = undefined;
}
if (data.slackClientSecret) {
const { cipherTextBlob: encryptedClientSecret } = await encryptWithRoot({
plainText: Buffer.from(data.slackClientSecret)
});
updatedData.encryptedSlackClientSecret = encryptedClientSecret;
updatedData.slackClientSecret = undefined;
}
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData);
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
@@ -232,11 +260,38 @@ export const superAdminServiceFactory = ({
return user;
};
const getAdminSlackConfig = async () => {
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (!serverCfg) {
throw new NotFoundError({ name: "Admin config", message: "Admin config not found" });
}
let clientId = "";
let clientSecret = "";
const decrypt = await kmsService.decryptWithRootKey();
if (serverCfg.encryptedSlackClientId) {
clientId = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientId })).toString();
}
if (serverCfg.encryptedSlackClientSecret) {
clientSecret = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientSecret })).toString();
}
return {
clientId,
clientSecret
};
};
return {
initServerCfg,
updateServerCfg,
adminSignUp,
getUsers,
deleteUser
deleteUser,
getAdminSlackConfig
};
};
@@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TWorkflowIntegrationDALFactory = ReturnType<typeof workflowIntegrationDALFactory>;
export const workflowIntegrationDALFactory = (db: TDbClient) => {
const workflowIntegrationOrm = ormify(db, TableName.WorkflowIntegrations);
return workflowIntegrationOrm;
};
@@ -0,0 +1,43 @@
import { ForbiddenError } from "@casl/ability";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TWorkflowIntegrationDALFactory } from "./workflow-integration-dal";
import { TGetWorkflowIntegrationsByOrg } from "./workflow-integration-types";
type TWorkflowIntegrationServiceFactoryDep = {
workflowIntegrationDAL: Pick<TWorkflowIntegrationDALFactory, "find">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
};
export type TWorkflowIntegrationServiceFactory = ReturnType<typeof workflowIntegrationServiceFactory>;
export const workflowIntegrationServiceFactory = ({
workflowIntegrationDAL,
permissionService
}: TWorkflowIntegrationServiceFactoryDep) => {
const getIntegrationsByOrg = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod
}: TGetWorkflowIntegrationsByOrg) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
return workflowIntegrationDAL.find({
orgId: actorOrgId
});
};
return {
getIntegrationsByOrg
};
};
@@ -0,0 +1,7 @@
import { TOrgPermission } from "@app/lib/types";
export enum WorkflowIntegration {
SLACK = "slack"
}
export type TGetWorkflowIntegrationsByOrg = Omit<TOrgPermission, "orgId">;