diff --git a/backend/src/db/migrations/20240830142938_native-slack-integration.ts b/backend/src/db/migrations/20240830142938_native-slack-integration.ts index e7e8f8458..e06c06105 100644 --- a/backend/src/db/migrations/20240830142938_native-slack-integration.ts +++ b/backend/src/db/migrations/20240830142938_native-slack-integration.ts @@ -53,16 +53,20 @@ export async function up(knex: Knex): Promise { await createOnUpdateTrigger(knex, TableName.ProjectSlackConfigs); } - if (!(await knex.schema.hasTable(TableName.AdminSlackConfig))) { - await knex.schema.createTable(TableName.AdminSlackConfig, (tb) => { - tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); - tb.binary("encryptedClientId").notNullable(); - tb.binary("encryptedClientSecret").notNullable(); - tb.timestamps(true, true, true); - }); + const doesSuperAdminHaveSlackClientId = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedSlackClientId"); + const doesSuperAdminHaveSlackClientSecret = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedSlackClientSecret" + ); - await createOnUpdateTrigger(knex, TableName.AdminSlackConfig); - } + await knex.schema.alterTable(TableName.SuperAdmin, (tb) => { + if (!doesSuperAdminHaveSlackClientId) { + tb.binary("encryptedSlackClientId"); + } + if (!doesSuperAdminHaveSlackClientSecret) { + tb.binary("encryptedSlackClientSecret"); + } + }); } export async function down(knex: Knex): Promise { @@ -72,9 +76,21 @@ export async function down(knex: Knex): Promise { await knex.schema.dropTableIfExists(TableName.SlackIntegrations); await dropOnUpdateTrigger(knex, TableName.SlackIntegrations); - await knex.schema.dropTableIfExists(TableName.AdminSlackConfig); - await dropOnUpdateTrigger(knex, TableName.AdminSlackConfig); - await knex.schema.dropTableIfExists(TableName.WorkflowIntegrations); await dropOnUpdateTrigger(knex, TableName.WorkflowIntegrations); + + const doesSuperAdminHaveSlackClientId = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedSlackClientId"); + const doesSuperAdminHaveSlackClientSecret = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedSlackClientSecret" + ); + + await knex.schema.alterTable(TableName.SuperAdmin, (tb) => { + if (doesSuperAdminHaveSlackClientId) { + tb.dropColumn("encryptedSlackClientId"); + } + if (doesSuperAdminHaveSlackClientSecret) { + tb.dropColumn("encryptedSlackClientSecret"); + } + }); } diff --git a/backend/src/db/schemas/admin-slack-configs.ts b/backend/src/db/schemas/admin-slack-configs.ts deleted file mode 100644 index 987a1af3d..000000000 --- a/backend/src/db/schemas/admin-slack-configs.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Code generated by automation script, DO NOT EDIT. -// Automated by pulling database and generating zod schema -// To update. Just run npm run generate:schema -// Written by akhilmhdh. - -import { z } from "zod"; - -import { zodBuffer } from "@app/lib/zod"; - -import { TImmutableDBKeys } from "./models"; - -export const AdminSlackConfigsSchema = z.object({ - id: z.string().uuid(), - encryptedClientId: zodBuffer, - encryptedClientSecret: zodBuffer, - createdAt: z.date(), - updatedAt: z.date() -}); - -export type TAdminSlackConfigs = z.infer; -export type TAdminSlackConfigsInsert = Omit, TImmutableDBKeys>; -export type TAdminSlackConfigsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 5c9a3a7c0..d856cab49 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -2,7 +2,6 @@ export * from "./access-approval-policies"; export * from "./access-approval-policies-approvers"; export * from "./access-approval-requests"; export * from "./access-approval-requests-reviewers"; -export * from "./admin-slack-configs"; export * from "./api-keys"; export * from "./audit-log-streams"; export * from "./audit-logs"; diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts index b676b81a8..edab3a0e9 100644 --- a/backend/src/db/schemas/super-admin.ts +++ b/backend/src/db/schemas/super-admin.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const SuperAdminSchema = z.object({ @@ -19,7 +21,9 @@ export const SuperAdminSchema = z.object({ trustLdapEmails: z.boolean().default(false).nullable().optional(), trustOidcEmails: z.boolean().default(false).nullable().optional(), defaultAuthOrgId: z.string().uuid().nullable().optional(), - enabledLoginMethods: z.string().array().nullable().optional() + enabledLoginMethods: z.string().array().nullable().optional(), + encryptedSlackClientId: zodBuffer.nullable().optional(), + encryptedSlackClientSecret: zodBuffer.nullable().optional() }); export type TSuperAdmin = z.infer; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 00ab3a7db..2fd63de33 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -182,7 +182,6 @@ import { secretVersionV2BridgeDALFactory } from "@app/services/secret-v2-bridge/ import { secretVersionV2TagBridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal"; import { serviceTokenServiceFactory } from "@app/services/service-token/service-token-service"; -import { adminSlackConfigDALFactory } from "@app/services/slack/admin-slack-config-dal"; import { projectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal"; import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal"; import { slackServiceFactory } from "@app/services/slack/slack-service"; @@ -330,7 +329,6 @@ export const registerRoutes = async ( const slackIntegrationDAL = slackIntegrationDALFactory(db); const projectSlackConfigDAL = projectSlackConfigDALFactory(db); - const adminSlackConfigDAL = adminSlackConfigDALFactory(db); const workflowIntegrationDAL = workflowIntegrationDALFactory(db); const permissionService = permissionServiceFactory({ @@ -532,7 +530,6 @@ export const registerRoutes = async ( orgService, keyStore, licenseService, - adminSlackConfigDAL, kmsService }); @@ -1173,7 +1170,6 @@ export const registerRoutes = async ( permissionService, kmsService, slackIntegrationDAL, - adminSlackConfigDAL, workflowIntegrationDAL }); diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index bbfe228be..d23597569 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -21,7 +21,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { schema: { response: { 200: z.object({ - config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).extend({ + config: SuperAdminSchema.omit({ + createdAt: true, + updatedAt: true, + encryptedSlackClientId: true, + encryptedSlackClientSecret: true + }).extend({ isMigrationModeOn: z.boolean(), defaultAuthOrgSlug: z.string().nullable(), isSecretScanningDisabled: z.boolean() @@ -62,7 +67,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { .optional() .refine((methods) => !methods || methods.length > 0, { message: "At least one login method should be enabled." - }) + }), + slackClientId: z.string().optional(), + slackClientSecret: z.string().optional() }), response: { 200: z.object({ @@ -172,38 +179,6 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }); - server.route({ - method: "PUT", - url: "/integrations/slack/config", - config: { - rateLimit: writeLimit - }, - schema: { - body: z.object({ - clientId: z.string(), - clientSecret: z.string() - }), - response: { - 200: z.object({ - clientId: z.string(), - clientSecret: z.string() - }) - } - }, - onRequest: (req, res, done) => { - verifyAuth([AuthMode.JWT])(req, res, () => { - verifySuperAdmin(req, res, done); - }); - }, - handler: async (req) => { - const adminSlackConfig = await server.services.superAdmin.updateAdminSlackConfig({ - ...req.body - }); - - return adminSlackConfig; - } - }); - server.route({ method: "DELETE", url: "/user-management/users/:userId", diff --git a/backend/src/services/slack/admin-slack-config-dal.ts b/backend/src/services/slack/admin-slack-config-dal.ts deleted file mode 100644 index b741bf685..000000000 --- a/backend/src/services/slack/admin-slack-config-dal.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TAdminSlackConfigDALFactory = ReturnType; - -export const adminSlackConfigDALFactory = (db: TDbClient) => { - const adminSlackConfigOrm = ormify(db, TableName.AdminSlackConfig); - - return adminSlackConfigOrm; -}; diff --git a/backend/src/services/slack/slack-fns.ts b/backend/src/services/slack/slack-fns.ts index 6eab272d2..6f62fa125 100644 --- a/backend/src/services/slack/slack-fns.ts +++ b/backend/src/services/slack/slack-fns.ts @@ -6,12 +6,9 @@ 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 { TAdminSlackConfigDALFactory } from "./admin-slack-config-dal"; import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal"; import { SlackTriggerFeature } from "./slack-types"; -const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000"; - export const getCustomSlackBotManifest = () => { const appCfg = getConfig(); @@ -142,29 +139,3 @@ export const triggerSlackNotification = async ({ .catch((err) => void logger.error(err)); } }; - -export const getAdminSlackCredentials = async ({ - adminSlackConfigDAL, - kmsService -}: { - adminSlackConfigDAL: Pick; - kmsService: Pick; -}) => { - const adminSlackConfig = await adminSlackConfigDAL.findById(ADMIN_CONFIG_DB_UUID); - let clientId = ""; - let clientSecret = ""; - const decrypt = await kmsService.decryptWithRootKey(); - - if (adminSlackConfig.encryptedClientId) { - clientId = (await decrypt({ cipherTextBlob: adminSlackConfig.encryptedClientId })).toString(); - } - - if (adminSlackConfig.encryptedClientSecret) { - clientSecret = (await decrypt({ cipherTextBlob: adminSlackConfig.encryptedClientSecret })).toString(); - } - - return { - clientId, - clientSecret - }; -}; diff --git a/backend/src/services/slack/slack-service.ts b/backend/src/services/slack/slack-service.ts index 5e8c6e055..41ba58b3b 100644 --- a/backend/src/services/slack/slack-service.ts +++ b/backend/src/services/slack/slack-service.ts @@ -8,10 +8,10 @@ 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 { TAdminSlackConfigDALFactory } from "./admin-slack-config-dal"; -import { fetchSlackChannels, getAdminSlackCredentials } from "./slack-fns"; +import { fetchSlackChannels } from "./slack-fns"; import { TSlackIntegrationDALFactory } from "./slack-integration-dal"; import { TCompleteSlackIntegrationDTO, @@ -36,7 +36,6 @@ type TSlackServiceFactoryDep = { >; permissionService: Pick; kmsService: Pick; - adminSlackConfigDAL: Pick; workflowIntegrationDAL: Pick; }; @@ -46,7 +45,6 @@ export const slackServiceFactory = ({ permissionService, slackIntegrationDAL, kmsService, - adminSlackConfigDAL, workflowIntegrationDAL }: TSlackServiceFactoryDep) => { const completeSlackIntegration = async ({ @@ -138,20 +136,21 @@ export const slackServiceFactory = ({ const getSlackInstaller = async () => { const appCfg = getConfig(); - const adminSlackCredentials = await getAdminSlackCredentials({ - kmsService, - adminSlackConfigDAL - }); + const serverCfg = await getServerCfg(); - let slackClientId = ""; - let slackClientSecret = ""; + let slackClientId = appCfg.SLACK_CLIENT_ID as string; + let slackClientSecret = appCfg.SLACK_CLIENT_SECRET as string; - if (adminSlackCredentials.clientId && adminSlackCredentials.clientSecret) { - slackClientId = adminSlackCredentials.clientId; - slackClientSecret = adminSlackCredentials.clientSecret; - } else { - slackClientId = appCfg.SLACK_CLIENT_ID as string; - slackClientSecret = appCfg.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) { diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 2ede9d415..015506d0f 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -6,22 +6,20 @@ 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 { TAdminSlackConfigDALFactory } from "../slack/admin-slack-config-dal"; -import { getAdminSlackCredentials, getCustomSlackBotManifest } from "../slack/slack-fns"; +import { getCustomSlackBotManifest } from "../slack/slack-fns"; import { TUserDALFactory } from "../user/user-dal"; import { TSuperAdminDALFactory } from "./super-admin-dal"; -import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO, TUpdateAdminSlackConfigDTO } from "./super-admin-types"; +import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types"; type TSuperAdminServiceFactoryDep = { serverCfgDAL: TSuperAdminDALFactory; userDAL: TUserDALFactory; - adminSlackConfigDAL: Pick; authService: Pick; kmsService: Pick; orgService: Pick; @@ -43,7 +41,6 @@ export const superAdminServiceFactory = ({ userDAL, authService, orgService, - adminSlackConfigDAL, keyStore, kmsService, licenseService @@ -89,7 +86,14 @@ 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 = { @@ -120,7 +124,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)); @@ -245,47 +269,30 @@ export const superAdminServiceFactory = ({ )}`; }; - const updateAdminSlackConfig = async ({ clientId, clientSecret }: TUpdateAdminSlackConfigDTO) => { - return adminSlackConfigDAL.transaction(async (tx) => { - const adminSlackConfig = await adminSlackConfigDAL.findById(ADMIN_CONFIG_DB_UUID, tx); - const encrypt = await kmsService.encryptWithRootKey(); - - const { cipherTextBlob: encryptedClientId } = await encrypt({ plainText: Buffer.from(clientId) }); - const { cipherTextBlob: encryptedClientSecret } = await encrypt({ plainText: Buffer.from(clientSecret) }); - - if (adminSlackConfig) { - await adminSlackConfigDAL.updateById( - ADMIN_CONFIG_DB_UUID, - { - encryptedClientId, - encryptedClientSecret - }, - tx - ); - } else { - await adminSlackConfigDAL.create( - { - // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition - id: ADMIN_CONFIG_DB_UUID, - encryptedClientId, - encryptedClientSecret - }, - tx - ); - } - - return { - clientId, - clientSecret - }; - }); - }; - const getAdminSlackConfig = async () => { - return getAdminSlackCredentials({ - kmsService, - adminSlackConfigDAL - }); + 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 { @@ -295,7 +302,6 @@ export const superAdminServiceFactory = ({ getUsers, deleteUser, getCustomSlackBotCreationUrl, - updateAdminSlackConfig, getAdminSlackConfig }; }; diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index b194869ae..2d10941b4 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -31,8 +31,3 @@ export enum LoginMethod { LDAP = "ldap", OIDC = "oidc" } - -export type TUpdateAdminSlackConfigDTO = { - clientId: string; - clientSecret: string; -}; diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index bd3f5fc70..9ab51d250 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -33,7 +33,11 @@ export const useCreateAdminUser = () => { export const useUpdateServerConfig = () => { const queryClient = useQueryClient(); - return useMutation>({ + return useMutation< + TServerConfig, + {}, + Partial + >({ mutationFn: async (opt) => { const { data } = await apiRequest.patch<{ config: TServerConfig }>( "/api/v1/admin/config", diff --git a/frontend/src/views/admin/DashboardPage/IntegrationPanel.tsx b/frontend/src/views/admin/DashboardPage/IntegrationPanel.tsx index 4531c1c4b..b114d4021 100644 --- a/frontend/src/views/admin/DashboardPage/IntegrationPanel.tsx +++ b/frontend/src/views/admin/DashboardPage/IntegrationPanel.tsx @@ -8,7 +8,7 @@ import { Button, FormControl, Input } from "@app/components/v2"; import { useGetAdminSlackConfig, useGetCustomSlackAppCreationUrl, - useUpdateAdminSlackConfig + useUpdateServerConfig } from "@app/hooks/api"; const slackFormSchema = z.object({ @@ -31,7 +31,7 @@ export const IntegrationPanel = () => { const { data: customSlackAppCreationUrl } = useGetCustomSlackAppCreationUrl(); const { data: adminSlackConfig } = useGetAdminSlackConfig(); - const { mutateAsync: updateAdminSlackConfig } = useUpdateAdminSlackConfig(); + const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig(); useEffect(() => { if (adminSlackConfig) { @@ -41,7 +41,10 @@ export const IntegrationPanel = () => { }, [adminSlackConfig]); const onSlackFormSubmit = async (data: TSlackForm) => { - await updateAdminSlackConfig(data); + await updateAdminServerConfig({ + slackClientId: data.clientId, + slackClientSecret: data.clientSecret + }); createNotification({ text: "Updated admin slack configuration",