mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-08 15:13:55 -05:00
feat: adds govslack support
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SuperAdmin)) {
|
||||
const hasGovSlackClientId = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedGovSlackClientId");
|
||||
const hasGovSlackClientSecret = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedGovSlackClientSecret");
|
||||
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||
if (!hasGovSlackClientId) {
|
||||
t.binary("encryptedGovSlackClientId").nullable();
|
||||
}
|
||||
if (!hasGovSlackClientSecret) {
|
||||
t.binary("encryptedGovSlackClientSecret").nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.SlackIntegrations)) {
|
||||
const hasIsGovSlack = await knex.schema.hasColumn(TableName.SlackIntegrations, "isGovSlack");
|
||||
|
||||
if (!hasIsGovSlack) {
|
||||
await knex.schema.alterTable(TableName.SlackIntegrations, (t) => {
|
||||
t.boolean("isGovSlack").defaultTo(false).notNullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SuperAdmin)) {
|
||||
const hasGovSlackClientId = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedGovSlackClientId");
|
||||
const hasGovSlackClientSecret = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedGovSlackClientSecret");
|
||||
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||
if (hasGovSlackClientId) {
|
||||
t.dropColumn("encryptedGovSlackClientId");
|
||||
}
|
||||
if (hasGovSlackClientSecret) {
|
||||
t.dropColumn("encryptedGovSlackClientSecret");
|
||||
}
|
||||
});
|
||||
}
|
||||
if (await knex.schema.hasTable(TableName.SlackIntegrations)) {
|
||||
const hasIsGovSlack = await knex.schema.hasColumn(TableName.SlackIntegrations, "isGovSlack");
|
||||
|
||||
if (hasIsGovSlack) {
|
||||
await knex.schema.alterTable(TableName.SlackIntegrations, (t) => {
|
||||
t.dropColumn("isGovSlack");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,8 @@ export const SlackIntegrationsSchema = z.object({
|
||||
slackBotId: z.string(),
|
||||
slackBotUserId: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
isGovSlack: z.boolean().default(false)
|
||||
});
|
||||
|
||||
export type TSlackIntegrations = z.infer<typeof SlackIntegrationsSchema>;
|
||||
|
||||
@@ -36,7 +36,9 @@ export const SuperAdminSchema = z.object({
|
||||
encryptedGitHubAppConnectionId: zodBuffer.nullable().optional(),
|
||||
encryptedGitHubAppConnectionPrivateKey: zodBuffer.nullable().optional(),
|
||||
encryptedEnvOverrides: zodBuffer.nullable().optional(),
|
||||
fipsEnabled: z.boolean().default(false)
|
||||
fipsEnabled: z.boolean().default(false),
|
||||
encryptedGovSlackClientId: zodBuffer.nullable().optional(),
|
||||
encryptedGovSlackClientSecret: zodBuffer.nullable().optional()
|
||||
});
|
||||
|
||||
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
||||
|
||||
@@ -246,8 +246,8 @@ const envSchema = z
|
||||
),
|
||||
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
|
||||
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||
WORKFLOW_SLACK_GOV_ENABLED: zodStrBool.default("false"),
|
||||
WORKFLOW_SLACK_GOV_BASE_URL: zpStr(z.string().optional().default("https://slack-gov.com")),
|
||||
WORKFLOW_GOV_SLACK_CLIENT_ID: zpStr(z.string().optional()),
|
||||
WORKFLOW_GOV_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"),
|
||||
|
||||
// Special Detection Feature
|
||||
|
||||
@@ -41,6 +41,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
updatedAt: true,
|
||||
encryptedSlackClientId: true,
|
||||
encryptedSlackClientSecret: true,
|
||||
encryptedGovSlackClientId: true,
|
||||
encryptedGovSlackClientSecret: true,
|
||||
encryptedMicrosoftTeamsAppId: true,
|
||||
encryptedMicrosoftTeamsClientSecret: true,
|
||||
encryptedMicrosoftTeamsBotId: true,
|
||||
@@ -107,6 +109,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
slackClientId: z.string().optional(),
|
||||
slackClientSecret: z.string().optional(),
|
||||
govSlackClientId: z.string().optional(),
|
||||
govSlackClientSecret: z.string().optional(),
|
||||
microsoftTeamsAppId: z.string().optional(),
|
||||
microsoftTeamsClientSecret: z.string().optional(),
|
||||
microsoftTeamsBotId: z.string().optional(),
|
||||
@@ -374,8 +378,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
200: z.object({
|
||||
slack: z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
govEnabled: z.boolean()
|
||||
clientSecret: z.string()
|
||||
}),
|
||||
govSlack: z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string()
|
||||
}),
|
||||
microsoftTeams: z.object({
|
||||
appId: z.string(),
|
||||
|
||||
@@ -8,6 +8,8 @@ import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { booleanSchema } from "../sanitizedSchemas";
|
||||
|
||||
const sanitizedSlackIntegrationSchema = WorkflowIntegrationsSchema.pick({
|
||||
id: true,
|
||||
description: true,
|
||||
@@ -36,7 +38,8 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
||||
],
|
||||
querystring: z.object({
|
||||
slug: slugSchema({ max: 64 }),
|
||||
description: z.string().optional()
|
||||
description: z.string().optional(),
|
||||
isGovSlack: booleanSchema.default(false).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.string()
|
||||
@@ -337,4 +340,24 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/oauth_redirect_gov",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
const installer = await server.services.slack.getSlackInstaller(true);
|
||||
|
||||
return installer.handleCallback(req.raw, res.raw, {
|
||||
failureAsync: async () => {
|
||||
return res.redirect(appCfg.SITE_URL as string);
|
||||
},
|
||||
successAsync: async () => {
|
||||
return res.redirect(`${appCfg.SITE_URL}/organization/settings?selectedTab=workflow-integrations`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
1
backend/src/services/slack/slack-constants.ts
Normal file
1
backend/src/services/slack/slack-constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const SLACK_GOV_BASE_URL = "https://slack-gov.com";
|
||||
@@ -6,13 +6,13 @@ import { logger } from "@app/lib/logger";
|
||||
import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { SLACK_GOV_BASE_URL } from "./slack-constants";
|
||||
import { TSendSlackNotificationDTO } from "./slack-types";
|
||||
|
||||
const COMPANY_BRAND_COLOR = "#e0ed34";
|
||||
const ERROR_COLOR = "#e74c3c";
|
||||
|
||||
export const fetchSlackChannels = async (botKey: string) => {
|
||||
const appCfg = getConfig();
|
||||
export const fetchSlackChannels = async (botKey: string, isGovSlack = false) => {
|
||||
const slackChannels: {
|
||||
name: string;
|
||||
id: string;
|
||||
@@ -20,9 +20,8 @@ export const fetchSlackChannels = async (botKey: string) => {
|
||||
|
||||
const webClientOptions: WebClientOptions = {};
|
||||
|
||||
if (appCfg.WORKFLOW_SLACK_GOV_ENABLED) {
|
||||
const govBaseUrl = appCfg.WORKFLOW_SLACK_GOV_BASE_URL;
|
||||
webClientOptions.slackApiUrl = `${govBaseUrl}/api`;
|
||||
if (isGovSlack) {
|
||||
webClientOptions.slackApiUrl = `${SLACK_GOV_BASE_URL}/api`;
|
||||
}
|
||||
|
||||
const slackWebClient = new WebClient(botKey, webClientOptions);
|
||||
@@ -277,7 +276,6 @@ export const sendSlackNotification = async ({
|
||||
targetChannelIds,
|
||||
slackIntegration
|
||||
}: TSendSlackNotificationDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
@@ -288,9 +286,8 @@ export const sendSlackNotification = async ({
|
||||
|
||||
const webClientOptions: WebClientOptions = {};
|
||||
|
||||
if (appCfg.WORKFLOW_SLACK_GOV_ENABLED) {
|
||||
const govBaseUrl = appCfg.WORKFLOW_SLACK_GOV_BASE_URL;
|
||||
webClientOptions.slackApiUrl = `${govBaseUrl}/api`;
|
||||
if (slackIntegration.isGovSlack) {
|
||||
webClientOptions.slackApiUrl = `${SLACK_GOV_BASE_URL}/api`;
|
||||
}
|
||||
|
||||
const slackWebClient = new WebClient(botKey, webClientOptions);
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 { SLACK_GOV_BASE_URL } from "./slack-constants";
|
||||
import { fetchSlackChannels } from "./slack-fns";
|
||||
import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
|
||||
import {
|
||||
@@ -58,7 +59,8 @@ export const slackServiceFactory = ({
|
||||
slackAppId,
|
||||
botAccessToken,
|
||||
slackBotId,
|
||||
slackBotUserId
|
||||
slackBotUserId,
|
||||
isGovSlack = false
|
||||
}: TCompleteSlackIntegrationDTO) => {
|
||||
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
orgId,
|
||||
@@ -90,7 +92,8 @@ export const slackServiceFactory = ({
|
||||
slackAppId,
|
||||
slackBotId,
|
||||
slackBotUserId,
|
||||
encryptedBotAccessToken
|
||||
encryptedBotAccessToken,
|
||||
isGovSlack
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -135,29 +138,44 @@ export const slackServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const getSlackInstaller = async () => {
|
||||
const getSlackInstaller = async (isGovSlack = false) => {
|
||||
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;
|
||||
let slackClientId = "";
|
||||
let slackClientSecret = "";
|
||||
|
||||
const decrypt = kmsService.decryptWithRootKey();
|
||||
|
||||
if (serverCfg.encryptedSlackClientId) {
|
||||
slackClientId = decrypt(Buffer.from(serverCfg.encryptedSlackClientId)).toString();
|
||||
}
|
||||
if (isGovSlack) {
|
||||
slackClientId = appCfg.WORKFLOW_GOV_SLACK_CLIENT_ID as string;
|
||||
slackClientSecret = appCfg.WORKFLOW_GOV_SLACK_CLIENT_SECRET as string;
|
||||
|
||||
if (serverCfg.encryptedSlackClientSecret) {
|
||||
slackClientSecret = decrypt(Buffer.from(serverCfg.encryptedSlackClientSecret)).toString();
|
||||
}
|
||||
if (serverCfg.encryptedGovSlackClientId) {
|
||||
slackClientId = decrypt(Buffer.from(serverCfg.encryptedGovSlackClientId)).toString();
|
||||
}
|
||||
|
||||
if (serverCfg.encryptedGovSlackClientSecret) {
|
||||
slackClientSecret = decrypt(Buffer.from(serverCfg.encryptedGovSlackClientSecret)).toString();
|
||||
}
|
||||
} else {
|
||||
slackClientId = appCfg.WORKFLOW_SLACK_CLIENT_ID as string;
|
||||
slackClientSecret = appCfg.WORKFLOW_SLACK_CLIENT_SECRET as string;
|
||||
|
||||
if (serverCfg.encryptedSlackClientId) {
|
||||
slackClientId = decrypt(Buffer.from(serverCfg.encryptedSlackClientId)).toString();
|
||||
}
|
||||
|
||||
if (serverCfg.encryptedSlackClientSecret) {
|
||||
slackClientSecret = decrypt(Buffer.from(serverCfg.encryptedSlackClientSecret)).toString();
|
||||
}
|
||||
}
|
||||
if (!slackClientId || !slackClientSecret) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid Slack configuration. ${
|
||||
message: `Invalid ${isGovSlack ? "GovSlack" : "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."
|
||||
: `Contact your instance admin to setup Slack integration in the Admin settings. Your configuration is missing ${isGovSlack ? "GovSlack" : "Slack"} client ID and secret.`
|
||||
}`
|
||||
});
|
||||
}
|
||||
@@ -180,6 +198,7 @@ export const slackServiceFactory = ({
|
||||
orgId: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
isGovSlack?: boolean;
|
||||
};
|
||||
|
||||
if (metadata.id) {
|
||||
@@ -205,7 +224,8 @@ export const slackServiceFactory = ({
|
||||
slackAppId: installation.appId || "",
|
||||
botAccessToken: installation.bot?.token || "",
|
||||
slackBotId: installation.bot?.id || "",
|
||||
slackBotUserId: installation.bot?.userId || ""
|
||||
slackBotUserId: installation.bot?.userId || "",
|
||||
isGovSlack: metadata.isGovSlack
|
||||
});
|
||||
},
|
||||
// for our use-case we don't need to implement this because this will only be used
|
||||
@@ -220,11 +240,10 @@ export const slackServiceFactory = ({
|
||||
}
|
||||
};
|
||||
|
||||
if (appCfg.WORKFLOW_SLACK_GOV_ENABLED) {
|
||||
const govBaseUrl = appCfg.WORKFLOW_SLACK_GOV_BASE_URL;
|
||||
installProviderOptions.authorizationUrl = `${govBaseUrl}/oauth/v2/authorize`;
|
||||
if (isGovSlack) {
|
||||
installProviderOptions.authorizationUrl = `${SLACK_GOV_BASE_URL}/oauth/v2/authorize`;
|
||||
installProviderOptions.clientOptions = {
|
||||
slackApiUrl: `${govBaseUrl}/api`
|
||||
slackApiUrl: `${SLACK_GOV_BASE_URL}/api`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -237,7 +256,8 @@ export const slackServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
slug,
|
||||
description
|
||||
description,
|
||||
isGovSlack = false
|
||||
}: TGetSlackInstallUrlDTO) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
@@ -252,15 +272,16 @@ export const slackServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
const installer = await getSlackInstaller();
|
||||
const installer = await getSlackInstaller(isGovSlack);
|
||||
const url = await installer.generateInstallUrl({
|
||||
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read"],
|
||||
metadata: JSON.stringify({
|
||||
slug,
|
||||
description,
|
||||
orgId: actorOrgId
|
||||
orgId: actorOrgId,
|
||||
isGovSlack
|
||||
}),
|
||||
redirectUri: `${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect`
|
||||
redirectUri: `${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect${isGovSlack ? "_gov" : ""}`
|
||||
});
|
||||
|
||||
return url;
|
||||
@@ -287,14 +308,15 @@ export const slackServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
const installer = await getSlackInstaller();
|
||||
const installer = await getSlackInstaller(slackIntegration.isGovSlack);
|
||||
const url = await installer.generateInstallUrl({
|
||||
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read"],
|
||||
metadata: JSON.stringify({
|
||||
id,
|
||||
orgId: slackIntegration.orgId
|
||||
orgId: slackIntegration.orgId,
|
||||
isGovSlack: slackIntegration.isGovSlack
|
||||
}),
|
||||
redirectUri: `${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect`
|
||||
redirectUri: `${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect${slackIntegration.isGovSlack ? "_gov" : ""}`
|
||||
});
|
||||
|
||||
return url;
|
||||
@@ -386,7 +408,7 @@ export const slackServiceFactory = ({
|
||||
cipherTextBlob: slackIntegration.encryptedBotAccessToken
|
||||
}).toString("utf8");
|
||||
|
||||
return fetchSlackChannels(botKey);
|
||||
return fetchSlackChannels(botKey, slackIntegration.isGovSlack);
|
||||
};
|
||||
|
||||
const updateSlackIntegration = async ({
|
||||
|
||||
@@ -7,6 +7,7 @@ import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
export type TGetSlackInstallUrlDTO = {
|
||||
slug: string;
|
||||
description?: string;
|
||||
isGovSlack?: boolean;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetReinstallUrlDTO = {
|
||||
@@ -39,6 +40,7 @@ export type TCompleteSlackIntegrationDTO = {
|
||||
botAccessToken: string;
|
||||
slackBotId: string;
|
||||
slackBotUserId: string;
|
||||
isGovSlack?: boolean;
|
||||
};
|
||||
|
||||
export type TReinstallSlackIntegrationDTO = {
|
||||
|
||||
@@ -101,6 +101,10 @@ let adminIntegrationsConfig: TAdminIntegrationConfig = {
|
||||
clientSecret: "",
|
||||
clientId: ""
|
||||
},
|
||||
govSlack: {
|
||||
clientSecret: "",
|
||||
clientId: ""
|
||||
},
|
||||
microsoftTeams: {
|
||||
appId: "",
|
||||
clientSecret: "",
|
||||
@@ -207,6 +211,13 @@ export const superAdminServiceFactory = ({
|
||||
? decrypt(serverCfg.encryptedSlackClientSecret).toString()
|
||||
: "";
|
||||
|
||||
const govSlackClientId = serverCfg.encryptedGovSlackClientId
|
||||
? decrypt(serverCfg.encryptedGovSlackClientId).toString()
|
||||
: "";
|
||||
const govSlackClientSecret = serverCfg.encryptedGovSlackClientSecret
|
||||
? decrypt(serverCfg.encryptedGovSlackClientSecret).toString()
|
||||
: "";
|
||||
|
||||
const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId
|
||||
? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString()
|
||||
: "";
|
||||
@@ -235,13 +246,14 @@ export const superAdminServiceFactory = ({
|
||||
? decrypt(serverCfg.encryptedGitHubAppConnectionPrivateKey).toString()
|
||||
: "";
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
return {
|
||||
slack: {
|
||||
clientSecret: slackClientSecret,
|
||||
clientId: slackClientId,
|
||||
govEnabled: appCfg.WORKFLOW_SLACK_GOV_ENABLED
|
||||
clientId: slackClientId
|
||||
},
|
||||
govSlack: {
|
||||
clientSecret: govSlackClientSecret,
|
||||
clientId: govSlackClientId
|
||||
},
|
||||
microsoftTeams: {
|
||||
appId: microsoftAppId,
|
||||
@@ -308,6 +320,8 @@ export const superAdminServiceFactory = ({
|
||||
data: TSuperAdminUpdate & {
|
||||
slackClientId?: string;
|
||||
slackClientSecret?: string;
|
||||
govSlackClientId?: string;
|
||||
govSlackClientSecret?: string;
|
||||
microsoftTeamsAppId?: string;
|
||||
microsoftTeamsClientSecret?: string;
|
||||
microsoftTeamsBotId?: string;
|
||||
@@ -384,6 +398,20 @@ export const superAdminServiceFactory = ({
|
||||
updatedData.slackClientSecret = undefined;
|
||||
}
|
||||
|
||||
if (data.govSlackClientId) {
|
||||
const encryptedClientId = encryptWithRoot(Buffer.from(data.govSlackClientId));
|
||||
|
||||
updatedData.encryptedGovSlackClientId = encryptedClientId;
|
||||
updatedData.govSlackClientId = undefined;
|
||||
}
|
||||
|
||||
if (data.govSlackClientSecret) {
|
||||
const encryptedClientSecret = encryptWithRoot(Buffer.from(data.govSlackClientSecret));
|
||||
|
||||
updatedData.encryptedGovSlackClientSecret = encryptedClientSecret;
|
||||
updatedData.govSlackClientSecret = undefined;
|
||||
}
|
||||
|
||||
let microsoftTeamsSettingsUpdated = false;
|
||||
if (data.microsoftTeamsAppId) {
|
||||
const encryptedClientId = encryptWithRoot(Buffer.from(data.microsoftTeamsAppId));
|
||||
|
||||
@@ -64,6 +64,10 @@ export type TAdminIntegrationConfig = {
|
||||
clientSecret: string;
|
||||
clientId: string;
|
||||
};
|
||||
govSlack: {
|
||||
clientSecret: string;
|
||||
clientId: string;
|
||||
};
|
||||
microsoftTeams: {
|
||||
appId: string;
|
||||
clientSecret: string;
|
||||
|
||||
@@ -78,6 +78,8 @@ export type TServerConfig = {
|
||||
export type TUpdateServerConfigDTO = {
|
||||
slackClientId?: string;
|
||||
slackClientSecret?: string;
|
||||
govSlackClientId?: string;
|
||||
govSlackClientSecret?: string;
|
||||
microsoftTeamsAppId?: string;
|
||||
microsoftTeamsClientSecret?: string;
|
||||
microsoftTeamsBotId?: string;
|
||||
@@ -119,7 +121,10 @@ export type AdminIntegrationsConfig = {
|
||||
slack: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
govEnabled: boolean;
|
||||
};
|
||||
govSlack: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
microsoftTeams: {
|
||||
appId: string;
|
||||
|
||||
@@ -29,15 +29,18 @@ export const workflowIntegrationKeys = {
|
||||
|
||||
export const fetchSlackInstallUrl = async ({
|
||||
slug,
|
||||
description
|
||||
description,
|
||||
isGovSlack
|
||||
}: {
|
||||
slug: string;
|
||||
description?: string;
|
||||
isGovSlack?: boolean;
|
||||
}) => {
|
||||
const { data } = await apiRequest.get<string>("/api/v1/workflow-integrations/slack/install", {
|
||||
params: {
|
||||
slug,
|
||||
description
|
||||
description,
|
||||
isGovSlack
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { BsSlack } from "react-icons/bs";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FormControl,
|
||||
Input
|
||||
} from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useUpdateServerConfig } from "@app/hooks/api";
|
||||
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
|
||||
|
||||
const getCustomSlackAppCreationUrl = () =>
|
||||
`https://api.slack-gov.com/apps?new_app=1&manifest_json=${encodeURIComponent(
|
||||
JSON.stringify({
|
||||
display_information: {
|
||||
name: "Infisical",
|
||||
description: "Get real-time Infisical updates in Slack",
|
||||
background_color: "#c2d62b",
|
||||
long_description: `This Slack application is designed specifically for use with your self-hosted Infisical instance, allowing seamless integration between your Infisical projects and your Slack workspace. With this integration, your team can stay up-to-date with the latest events, changes, and notifications directly inside Slack.
|
||||
- Notifications: Receive real-time updates and alerts about critical events in your Infisical projects. Whether it's a new project being created, updates to secrets, or changes to your team's configuration, you will be promptly notified within the designated Slack channels of your choice.
|
||||
- Customization: Tailor the notifications to your team's specific needs by configuring which types of events trigger alerts and in which channels they are sent.
|
||||
- Collaboration: Keep your entire team in the loop with notifications that help facilitate more efficient collaboration by ensuring that everyone is aware of important developments in your Infisical projects.
|
||||
|
||||
By integrating Infisical with Slack, you can enhance your workflow by combining the power of secure secrets management with the communication capabilities of Slack.`
|
||||
},
|
||||
features: {
|
||||
app_home: {
|
||||
home_tab_enabled: false,
|
||||
messages_tab_enabled: false,
|
||||
messages_tab_read_only_enabled: true
|
||||
},
|
||||
bot_user: {
|
||||
display_name: "Infisical",
|
||||
always_online: true
|
||||
}
|
||||
},
|
||||
oauth_config: {
|
||||
redirect_urls: [`${window.origin}/api/v1/workflow-integrations/slack/oauth_redirect_gov`],
|
||||
scopes: {
|
||||
bot: ["chat:write.public", "chat:write", "channels:read", "groups:read"]
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
org_deploy_enabled: false,
|
||||
socket_mode_enabled: false,
|
||||
token_rotation_enabled: false
|
||||
}
|
||||
})
|
||||
)}`;
|
||||
|
||||
const slackFormSchema = z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string()
|
||||
});
|
||||
|
||||
type TSlackForm = z.infer<typeof slackFormSchema>;
|
||||
|
||||
type Props = {
|
||||
adminIntegrationsConfig?: AdminIntegrationsConfig;
|
||||
};
|
||||
|
||||
export const GovSlackIntegrationForm = ({ adminIntegrationsConfig }: Props) => {
|
||||
const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig();
|
||||
const [isSlackClientIdFocused, setIsSlackClientIdFocused] = useToggle();
|
||||
const [isSlackClientSecretFocused, setIsSlackClientSecretFocused] = useToggle();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = useForm<TSlackForm>({
|
||||
resolver: zodResolver(slackFormSchema)
|
||||
});
|
||||
|
||||
const onSubmit = async (data: TSlackForm) => {
|
||||
await updateAdminServerConfig({
|
||||
govSlackClientId: data.clientId,
|
||||
govSlackClientSecret: data.clientSecret
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Updated admin gov slack configuration",
|
||||
type: "success"
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (adminIntegrationsConfig) {
|
||||
setValue("clientId", adminIntegrationsConfig.govSlack.clientId);
|
||||
setValue("clientSecret", adminIntegrationsConfig.govSlack.clientSecret);
|
||||
}
|
||||
}, [adminIntegrationsConfig]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="slack-integration" className="data-[state=open]:border-none">
|
||||
<AccordionTrigger className="flex h-fit w-full justify-start rounded-md border border-mineshaft-500 bg-mineshaft-700 px-4 py-6 text-sm transition-colors data-[state=open]:rounded-b-none">
|
||||
<div className="text-md group order-1 ml-3 flex items-center gap-2">
|
||||
<BsSlack className="text-lg group-hover:text-primary-400" />
|
||||
<div className="text-[15px] font-medium">GovSlack</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent childrenClassName="px-0 py-0">
|
||||
<div className="flex w-full flex-col justify-start rounded-md rounded-t-none border border-t-0 border-mineshaft-500 bg-mineshaft-700 px-4 py-4">
|
||||
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
|
||||
Step 1: Create your Infisical GovSlack App
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
onClick={() => window.open(getCustomSlackAppCreationUrl())}
|
||||
>
|
||||
Create GovSlack App
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
|
||||
Step 2: Configure your instance-wide settings to enable integration with Slack. Copy
|
||||
the values from the App Credentials page of your custom Slack App.
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="clientId"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Client ID"
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
type={isSlackClientIdFocused ? "text" : "password"}
|
||||
onFocus={() => setIsSlackClientIdFocused.on()}
|
||||
onBlur={() => setIsSlackClientIdFocused.off()}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="clientSecret"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Client Secret"
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
type={isSlackClientSecretFocused ? "text" : "password"}
|
||||
onFocus={() => setIsSlackClientSecretFocused.on()}
|
||||
onBlur={() => setIsSlackClientSecretFocused.off()}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
className="mt-2"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useGetAdminIntegrationsConfig } from "@app/hooks/api";
|
||||
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
|
||||
|
||||
import { GovSlackIntegrationForm } from "./GovSlackIntegrationForm";
|
||||
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
||||
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||
|
||||
@@ -19,6 +20,7 @@ interface WorkflowTabProps {
|
||||
const WorkflowTab = ({ adminIntegrationsConfig }: WorkflowTabProps) => (
|
||||
<div className="flex flex-col gap-2">
|
||||
<SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||
<GovSlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||
<MicrosoftTeamsIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,9 +18,8 @@ import { useToggle } from "@app/hooks";
|
||||
import { useUpdateServerConfig } from "@app/hooks/api";
|
||||
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
|
||||
|
||||
const getCustomSlackAppCreationUrl = (govEnabled: boolean) => {
|
||||
const baseUrl = govEnabled ? "https://api.slack-gov.com" : "https://api.slack.com";
|
||||
return `${baseUrl}/apps?new_app=1&manifest_json=${encodeURIComponent(
|
||||
const getCustomSlackAppCreationUrl = () =>
|
||||
`https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(
|
||||
JSON.stringify({
|
||||
display_information: {
|
||||
name: "Infisical",
|
||||
@@ -57,7 +56,6 @@ const getCustomSlackAppCreationUrl = (govEnabled: boolean) => {
|
||||
}
|
||||
})
|
||||
)}`;
|
||||
};
|
||||
|
||||
const slackFormSchema = z.object({
|
||||
clientId: z.string(),
|
||||
@@ -121,13 +119,7 @@ export const SlackIntegrationForm = ({ adminIntegrationsConfig }: Props) => {
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
getCustomSlackAppCreationUrl(
|
||||
adminIntegrationsConfig?.slack.govEnabled ?? false
|
||||
)
|
||||
)
|
||||
}
|
||||
onClick={() => window.open(getCustomSlackAppCreationUrl())}
|
||||
>
|
||||
Create Slack App
|
||||
</Button>
|
||||
|
||||
@@ -5,11 +5,12 @@ import axios from "axios";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import {
|
||||
fetchSlackInstallUrl,
|
||||
useGetAdminIntegrationsConfig,
|
||||
useGetSlackIntegrationById,
|
||||
useUpdateSlackIntegration
|
||||
} from "@app/hooks/api";
|
||||
@@ -22,7 +23,8 @@ type Props = {
|
||||
|
||||
const slackFormSchema = z.object({
|
||||
slug: slugSchema({ min: 1, field: "Alias" }),
|
||||
description: z.string().optional()
|
||||
description: z.string().optional(),
|
||||
integrationType: z.enum(["slack", "govSlack"]).default("slack")
|
||||
});
|
||||
|
||||
type TSlackFormData = z.infer<typeof slackFormSchema>;
|
||||
@@ -41,6 +43,7 @@ export const SlackIntegrationForm = ({ id, onClose }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { data: slackIntegration } = useGetSlackIntegrationById(id);
|
||||
const { mutateAsync: updateSlackIntegration } = useUpdateSlackIntegration();
|
||||
const { data: adminIntegrationsConfig } = useGetAdminIntegrationsConfig();
|
||||
|
||||
useEffect(() => {
|
||||
if (slackIntegration) {
|
||||
@@ -49,14 +52,16 @@ export const SlackIntegrationForm = ({ id, onClose }: Props) => {
|
||||
}
|
||||
}, [slackIntegration]);
|
||||
|
||||
const triggerSlackInstall = async (slug: string, description?: string) => {
|
||||
const triggerSlackInstall = async (slug: string, description?: string, isGovSlack?: boolean) => {
|
||||
setIsConnectLoading.on();
|
||||
try {
|
||||
const slackInstallUrl = await fetchSlackInstallUrl({
|
||||
slug,
|
||||
description
|
||||
description,
|
||||
isGovSlack
|
||||
});
|
||||
if (slackInstallUrl) {
|
||||
console.log("slackInstallUrl", slackInstallUrl);
|
||||
window.location.assign(slackInstallUrl);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -71,7 +76,7 @@ export const SlackIntegrationForm = ({ id, onClose }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSlackFormSubmit = async ({ slug, description }: TSlackFormData) => {
|
||||
const handleSlackFormSubmit = async ({ slug, description, integrationType }: TSlackFormData) => {
|
||||
if (id && slackIntegration) {
|
||||
if (!currentOrg) {
|
||||
return;
|
||||
@@ -90,12 +95,38 @@ export const SlackIntegrationForm = ({ id, onClose }: Props) => {
|
||||
type: "success"
|
||||
});
|
||||
} else {
|
||||
await triggerSlackInstall(slug, description);
|
||||
await triggerSlackInstall(slug, description, integrationType === "govSlack");
|
||||
}
|
||||
};
|
||||
|
||||
const isGovSlackAvailable =
|
||||
adminIntegrationsConfig?.govSlack?.clientId && adminIntegrationsConfig?.govSlack?.clientSecret;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleSlackFormSubmit)} autoComplete="off">
|
||||
{!slackIntegration && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="integrationType"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Integration Type"
|
||||
isRequired
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(value) => field.onChange(value)}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value="slack">Slack</SelectItem>
|
||||
{isGovSlackAvailable && <SelectItem value="govSlack">GovSlack</SelectItem>}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="slug"
|
||||
|
||||
Reference in New Issue
Block a user