diff --git a/backend/src/db/migrations/20250620144939_add-instance-github-app-connection-credentials.ts b/backend/src/db/migrations/20250620144939_add-instance-github-app-connection-credentials.ts new file mode 100644 index 000000000..0da41b2e1 --- /dev/null +++ b/backend/src/db/migrations/20250620144939_add-instance-github-app-connection-credentials.ts @@ -0,0 +1,91 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasEncryptedGithubAppConnectionClientIdColumn = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedGitHubAppConnectionClientId" + ); + const hasEncryptedGithubAppConnectionClientSecretColumn = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedGitHubAppConnectionClientSecret" + ); + + const hasEncryptedGithubAppConnectionSlugColumn = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedGitHubAppConnectionSlug" + ); + + const hasEncryptedGithubAppConnectionAppIdColumn = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedGitHubAppConnectionId" + ); + + const hasEncryptedGithubAppConnectionAppPrivateKeyColumn = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedGitHubAppConnectionPrivateKey" + ); + + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + if (!hasEncryptedGithubAppConnectionClientIdColumn) { + t.binary("encryptedGitHubAppConnectionClientId").nullable(); + } + if (!hasEncryptedGithubAppConnectionClientSecretColumn) { + t.binary("encryptedGitHubAppConnectionClientSecret").nullable(); + } + if (!hasEncryptedGithubAppConnectionSlugColumn) { + t.binary("encryptedGitHubAppConnectionSlug").nullable(); + } + if (!hasEncryptedGithubAppConnectionAppIdColumn) { + t.binary("encryptedGitHubAppConnectionId").nullable(); + } + if (!hasEncryptedGithubAppConnectionAppPrivateKeyColumn) { + t.binary("encryptedGitHubAppConnectionPrivateKey").nullable(); + } + }); +} + +export async function down(knex: Knex): Promise { + const hasEncryptedGithubAppConnectionClientIdColumn = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedGitHubAppConnectionClientId" + ); + const hasEncryptedGithubAppConnectionClientSecretColumn = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedGitHubAppConnectionClientSecret" + ); + + const hasEncryptedGithubAppConnectionSlugColumn = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedGitHubAppConnectionSlug" + ); + + const hasEncryptedGithubAppConnectionAppIdColumn = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedGitHubAppConnectionId" + ); + + const hasEncryptedGithubAppConnectionAppPrivateKeyColumn = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedGitHubAppConnectionPrivateKey" + ); + + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + if (hasEncryptedGithubAppConnectionClientIdColumn) { + t.dropColumn("encryptedGitHubAppConnectionClientId"); + } + if (hasEncryptedGithubAppConnectionClientSecretColumn) { + t.dropColumn("encryptedGitHubAppConnectionClientSecret"); + } + if (hasEncryptedGithubAppConnectionSlugColumn) { + t.dropColumn("encryptedGitHubAppConnectionSlug"); + } + if (hasEncryptedGithubAppConnectionAppIdColumn) { + t.dropColumn("encryptedGitHubAppConnectionId"); + } + if (hasEncryptedGithubAppConnectionAppPrivateKeyColumn) { + t.dropColumn("encryptedGitHubAppConnectionPrivateKey"); + } + }); +} diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts index ec35042ad..de4975b20 100644 --- a/backend/src/db/schemas/super-admin.ts +++ b/backend/src/db/schemas/super-admin.ts @@ -29,7 +29,12 @@ export const SuperAdminSchema = z.object({ adminIdentityIds: z.string().array().nullable().optional(), encryptedMicrosoftTeamsAppId: zodBuffer.nullable().optional(), encryptedMicrosoftTeamsClientSecret: zodBuffer.nullable().optional(), - encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional() + encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional(), + encryptedGitHubAppConnectionClientId: zodBuffer.nullable().optional(), + encryptedGitHubAppConnectionClientSecret: zodBuffer.nullable().optional(), + encryptedGitHubAppConnectionSlug: zodBuffer.nullable().optional(), + encryptedGitHubAppConnectionId: zodBuffer.nullable().optional(), + encryptedGitHubAppConnectionPrivateKey: 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 775417303..bde1b805e 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2021,10 +2021,16 @@ export const registerRoutes = async ( if (licenseSyncJob) { cronJobs.push(licenseSyncJob); } + const microsoftTeamsSyncJob = await microsoftTeamsService.initializeBackgroundSync(); if (microsoftTeamsSyncJob) { cronJobs.push(microsoftTeamsSyncJob); } + + const adminIntegrationsSyncJob = await superAdminService.initializeAdminIntegrationConfigSync(); + if (adminIntegrationsSyncJob) { + cronJobs.push(adminIntegrationsSyncJob); + } } server.decorate("store", { diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 0bade9904..f01f1722c 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -37,7 +37,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { encryptedSlackClientSecret: true, encryptedMicrosoftTeamsAppId: true, encryptedMicrosoftTeamsClientSecret: true, - encryptedMicrosoftTeamsBotId: true + encryptedMicrosoftTeamsBotId: true, + encryptedGitHubAppConnectionClientId: true, + encryptedGitHubAppConnectionClientSecret: true, + encryptedGitHubAppConnectionSlug: true, + encryptedGitHubAppConnectionId: true, + encryptedGitHubAppConnectionPrivateKey: true }).extend({ isMigrationModeOn: z.boolean(), defaultAuthOrgSlug: z.string().nullable(), @@ -87,6 +92,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { microsoftTeamsAppId: z.string().optional(), microsoftTeamsClientSecret: z.string().optional(), microsoftTeamsBotId: z.string().optional(), + gitHubAppConnectionClientId: z.string().optional(), + gitHubAppConnectionClientSecret: z.string().optional(), + gitHubAppConnectionSlug: z.string().optional(), + gitHubAppConnectionId: z.string().optional(), + gitHubAppConnectionPrivateKey: z.string().optional(), authConsentContent: z .string() .trim() @@ -348,6 +358,13 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { appId: z.string(), clientSecret: z.string(), botId: z.string() + }), + gitHubAppConnection: z.object({ + clientId: z.string(), + clientSecret: z.string(), + appSlug: z.string(), + appId: z.string(), + privateKey: z.string() }) }) } diff --git a/backend/src/services/app-connection/github/github-connection-fns.ts b/backend/src/services/app-connection/github/github-connection-fns.ts index 6ec675c0f..ebdd09289 100644 --- a/backend/src/services/app-connection/github/github-connection-fns.ts +++ b/backend/src/services/app-connection/github/github-connection-fns.ts @@ -7,6 +7,7 @@ import { request } from "@app/lib/config/request"; import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors"; import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { getInstanceIntegrationsConfig } from "@app/services/super-admin/super-admin-service"; import { AppConnection } from "../app-connection-enums"; import { GitHubConnectionMethod } from "./github-connection-enums"; @@ -14,13 +15,14 @@ import { TGitHubConnection, TGitHubConnectionConfig } from "./github-connection- export const getGitHubConnectionListItem = () => { const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig(); + const { gitHubAppConnection } = getInstanceIntegrationsConfig(); return { name: "GitHub" as const, app: AppConnection.GitHub as const, methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth], oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, - appClientSlug: INF_APP_CONNECTION_GITHUB_APP_SLUG + appClientSlug: gitHubAppConnection.appSlug || INF_APP_CONNECTION_GITHUB_APP_SLUG }; }; @@ -30,23 +32,24 @@ export const getGitHubClient = (appConnection: TGitHubConnection) => { const { method, credentials } = appConnection; let client: Octokit; + const { gitHubAppConnection } = getInstanceIntegrationsConfig(); + + const appId = gitHubAppConnection.appId || appCfg.INF_APP_CONNECTION_GITHUB_APP_ID; + const appPrivateKey = gitHubAppConnection.privateKey || appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY; switch (method) { case GitHubConnectionMethod.App: - if (!appCfg.INF_APP_CONNECTION_GITHUB_APP_ID || !appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY) { + if (!appId || !appPrivateKey) { throw new InternalServerError({ - message: `GitHub ${getAppConnectionMethodName(method).replace( - "GitHub", - "" - )} environment variables have not been configured` + message: `GitHub ${getAppConnectionMethodName(method).replace("GitHub", "")} has not been configured` }); } client = new Octokit({ authStrategy: createAppAuth, auth: { - appId: appCfg.INF_APP_CONNECTION_GITHUB_APP_ID, - privateKey: appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY, + appId, + privateKey: appPrivateKey, installationId: credentials.installationId } }); @@ -154,6 +157,8 @@ type TokenRespData = { export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => { const { credentials, method } = config; + const { gitHubAppConnection } = getInstanceIntegrationsConfig(); + const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET, @@ -165,8 +170,8 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect const { clientId, clientSecret } = method === GitHubConnectionMethod.App ? { - clientId: INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID, - clientSecret: INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET + clientId: gitHubAppConnection.clientId || INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID, + clientSecret: gitHubAppConnection.clientSecret || INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET } : // oauth { diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index ff319796e..8a0d4dd14 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -1,4 +1,5 @@ import bcrypt from "bcrypt"; +import { CronJob } from "cron"; import jwt from "jsonwebtoken"; import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; @@ -8,6 +9,7 @@ import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { TIdentityDALFactory } from "@app/services/identity/identity-dal"; import { TAuthLoginFactory } from "../auth/auth-login-service"; @@ -35,6 +37,7 @@ import { TAdminBootstrapInstanceDTO, TAdminGetIdentitiesDTO, TAdminGetUsersDTO, + TAdminIntegrationConfig, TAdminSignUpDTO, TGetOrganizationsDTO } from "./super-admin-types"; @@ -70,6 +73,31 @@ export let getServerCfg: () => Promise< } >; +let adminIntegrationsConfig: TAdminIntegrationConfig = { + slack: { + clientSecret: "", + clientId: "" + }, + microsoftTeams: { + appId: "", + clientSecret: "", + botId: "" + }, + gitHubAppConnection: { + clientId: "", + clientSecret: "", + appSlug: "", + appId: "", + privateKey: "" + } +}; + +Object.freeze(adminIntegrationsConfig); + +export const getInstanceIntegrationsConfig = () => { + return adminIntegrationsConfig; +}; + const ADMIN_CONFIG_KEY = "infisical-admin-cfg"; const ADMIN_CONFIG_KEY_EXP = 60; // 60s export const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000"; @@ -138,6 +166,74 @@ export const superAdminServiceFactory = ({ return serverCfg; }; + const getAdminIntegrationsConfig = async () => { + const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); + + if (!serverCfg) { + throw new NotFoundError({ name: "AdminConfig", message: "Admin config not found" }); + } + + const decrypt = kmsService.decryptWithRootKey(); + + const slackClientId = serverCfg.encryptedSlackClientId ? decrypt(serverCfg.encryptedSlackClientId).toString() : ""; + const slackClientSecret = serverCfg.encryptedSlackClientSecret + ? decrypt(serverCfg.encryptedSlackClientSecret).toString() + : ""; + + const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId + ? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString() + : ""; + const microsoftClientSecret = serverCfg.encryptedMicrosoftTeamsClientSecret + ? decrypt(serverCfg.encryptedMicrosoftTeamsClientSecret).toString() + : ""; + const microsoftBotId = serverCfg.encryptedMicrosoftTeamsBotId + ? decrypt(serverCfg.encryptedMicrosoftTeamsBotId).toString() + : ""; + + const gitHubAppConnectionClientId = serverCfg.encryptedGitHubAppConnectionClientId + ? decrypt(serverCfg.encryptedGitHubAppConnectionClientId).toString() + : ""; + const gitHubAppConnectionClientSecret = serverCfg.encryptedGitHubAppConnectionClientSecret + ? decrypt(serverCfg.encryptedGitHubAppConnectionClientSecret).toString() + : ""; + + const gitHubAppConnectionAppSlug = serverCfg.encryptedGitHubAppConnectionSlug + ? decrypt(serverCfg.encryptedGitHubAppConnectionSlug).toString() + : ""; + + const gitHubAppConnectionAppId = serverCfg.encryptedGitHubAppConnectionId + ? decrypt(serverCfg.encryptedGitHubAppConnectionId).toString() + : ""; + const gitHubAppConnectionAppPrivateKey = serverCfg.encryptedGitHubAppConnectionPrivateKey + ? decrypt(serverCfg.encryptedGitHubAppConnectionPrivateKey).toString() + : ""; + + return { + slack: { + clientSecret: slackClientSecret, + clientId: slackClientId + }, + microsoftTeams: { + appId: microsoftAppId, + clientSecret: microsoftClientSecret, + botId: microsoftBotId + }, + gitHubAppConnection: { + clientId: gitHubAppConnectionClientId, + clientSecret: gitHubAppConnectionClientSecret, + appSlug: gitHubAppConnectionAppSlug, + appId: gitHubAppConnectionAppId, + privateKey: gitHubAppConnectionAppPrivateKey + } + }; + }; + + const $syncAdminIntegrationConfig = async () => { + const config = await getAdminIntegrationsConfig(); + Object.freeze(config); + adminIntegrationsConfig = config; + }; + const updateServerCfg = async ( data: TSuperAdminUpdate & { slackClientId?: string; @@ -145,6 +241,11 @@ export const superAdminServiceFactory = ({ microsoftTeamsAppId?: string; microsoftTeamsClientSecret?: string; microsoftTeamsBotId?: string; + gitHubAppConnectionClientId?: string; + gitHubAppConnectionClientSecret?: string; + gitHubAppConnectionSlug?: string; + gitHubAppConnectionId?: string; + gitHubAppConnectionPrivateKey?: string; }, userId: string ) => { @@ -236,10 +337,51 @@ export const superAdminServiceFactory = ({ updatedData.microsoftTeamsBotId = undefined; microsoftTeamsSettingsUpdated = true; } + + let gitHubAppConnectionSettingsUpdated = false; + if (data.gitHubAppConnectionClientId !== undefined) { + const encryptedClientId = encryptWithRoot(Buffer.from(data.gitHubAppConnectionClientId)); + updatedData.encryptedGitHubAppConnectionClientId = encryptedClientId; + updatedData.gitHubAppConnectionClientId = undefined; + gitHubAppConnectionSettingsUpdated = true; + } + + if (data.gitHubAppConnectionClientSecret !== undefined) { + const encryptedClientSecret = encryptWithRoot(Buffer.from(data.gitHubAppConnectionClientSecret)); + updatedData.encryptedGitHubAppConnectionClientSecret = encryptedClientSecret; + updatedData.gitHubAppConnectionClientSecret = undefined; + gitHubAppConnectionSettingsUpdated = true; + } + + if (data.gitHubAppConnectionSlug !== undefined) { + const encryptedAppSlug = encryptWithRoot(Buffer.from(data.gitHubAppConnectionSlug)); + updatedData.encryptedGitHubAppConnectionSlug = encryptedAppSlug; + updatedData.gitHubAppConnectionSlug = undefined; + gitHubAppConnectionSettingsUpdated = true; + } + + if (data.gitHubAppConnectionId !== undefined) { + const encryptedAppId = encryptWithRoot(Buffer.from(data.gitHubAppConnectionId)); + updatedData.encryptedGitHubAppConnectionId = encryptedAppId; + updatedData.gitHubAppConnectionId = undefined; + gitHubAppConnectionSettingsUpdated = true; + } + + if (data.gitHubAppConnectionPrivateKey !== undefined) { + const encryptedAppPrivateKey = encryptWithRoot(Buffer.from(data.gitHubAppConnectionPrivateKey)); + updatedData.encryptedGitHubAppConnectionPrivateKey = encryptedAppPrivateKey; + updatedData.gitHubAppConnectionPrivateKey = undefined; + gitHubAppConnectionSettingsUpdated = true; + } + const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData); await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg)); + if (gitHubAppConnectionSettingsUpdated) { + await $syncAdminIntegrationConfig(); + } + if ( updatedServerCfg.encryptedMicrosoftTeamsAppId && updatedServerCfg.encryptedMicrosoftTeamsClientSecret && @@ -593,43 +735,6 @@ export const superAdminServiceFactory = ({ await userDAL.updateById(userId, { superAdmin: true }); }; - const getAdminIntegrationsConfig = async () => { - const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); - - if (!serverCfg) { - throw new NotFoundError({ name: "AdminConfig", message: "Admin config not found" }); - } - - const decrypt = kmsService.decryptWithRootKey(); - - const slackClientId = serverCfg.encryptedSlackClientId ? decrypt(serverCfg.encryptedSlackClientId).toString() : ""; - const slackClientSecret = serverCfg.encryptedSlackClientSecret - ? decrypt(serverCfg.encryptedSlackClientSecret).toString() - : ""; - - const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId - ? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString() - : ""; - const microsoftClientSecret = serverCfg.encryptedMicrosoftTeamsClientSecret - ? decrypt(serverCfg.encryptedMicrosoftTeamsClientSecret).toString() - : ""; - const microsoftBotId = serverCfg.encryptedMicrosoftTeamsBotId - ? decrypt(serverCfg.encryptedMicrosoftTeamsBotId).toString() - : ""; - - return { - slack: { - clientSecret: slackClientSecret, - clientId: slackClientId - }, - microsoftTeams: { - appId: microsoftAppId, - clientSecret: microsoftClientSecret, - botId: microsoftBotId - } - }; - }; - const getConfiguredEncryptionStrategies = async () => { const appCfg = getConfig(); @@ -696,6 +801,19 @@ export const superAdminServiceFactory = ({ return (await keyStore.getItem("invalidating-cache")) !== null; }; + const initializeAdminIntegrationConfigSync = async () => { + logger.info("Setting up background sync process for admin integrations config"); + + // initial sync upon startup + await $syncAdminIntegrationConfig(); + + // sync admin integrations config every 5 minutes + const job = new CronJob("*/5 * * * *", $syncAdminIntegrationConfig); + job.start(); + + return job; + }; + return { initServerCfg, updateServerCfg, @@ -714,6 +832,7 @@ export const superAdminServiceFactory = ({ checkIfInvalidatingCache, getOrganizations, deleteOrganization, - deleteOrganizationMembership + deleteOrganizationMembership, + initializeAdminIntegrationConfigSync }; }; diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 22803a650..205c59f2c 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -55,3 +55,22 @@ export enum CacheType { ALL = "all", SECRETS = "secrets" } + +export type TAdminIntegrationConfig = { + slack: { + clientSecret: string; + clientId: string; + }; + microsoftTeams: { + appId: string; + clientSecret: string; + botId: string; + }; + gitHubAppConnection: { + clientId: string; + clientSecret: string; + appSlug: string; + appId: string; + privateKey: string; + }; +}; diff --git a/docs/images/integrations/github/app/self-hosted-github-app-admin-panel.png b/docs/images/integrations/github/app/self-hosted-github-app-admin-panel.png new file mode 100644 index 000000000..2aedd1887 Binary files /dev/null and b/docs/images/integrations/github/app/self-hosted-github-app-admin-panel.png differ diff --git a/docs/integrations/app-connections/github.mdx b/docs/integrations/app-connections/github.mdx index 2f8caffed..8f4283ae9 100644 --- a/docs/integrations/app-connections/github.mdx +++ b/docs/integrations/app-connections/github.mdx @@ -53,7 +53,22 @@ Infisical supports two methods for connecting to GitHub. Obtain the necessary Github application credentials. This would be the application slug, client ID, app ID, client secret, and private key. ![integrations github app credentials](/images/integrations/github/app/self-hosted-github-app-credentials.png) - Back in your Infisical instance, add the five new environment variables for the credentials of your GitHub application: + Back in your Infisical instance, you can configure the GitHub App credentials in one of two ways: + + **Option 1: Server Admin Panel (Recommended)** + + Navigate to the server admin panel > **Integrations** > **GitHub App** and enter the GitHub application credentials: + ![integrations github app admin panel](/images/integrations/github/app/self-hosted-github-app-admin-panel.png) + + - **Client ID**: The Client ID of your GitHub application + - **Client Secret**: The Client Secret of your GitHub application + - **App Slug**: The Slug of your GitHub application (found in the URL) + - **App ID**: The App ID of your GitHub application + - **Private Key**: The Private Key of your GitHub application + + **Option 2: Environment Variables** + + Alternatively, you can add the new environment variables for the credentials of your GitHub application: - `INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID`: The **Client ID** of your GitHub application. - `INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET`: The **Client Secret** of your GitHub application. @@ -61,7 +76,7 @@ Infisical supports two methods for connecting to GitHub. - `INF_APP_CONNECTION_GITHUB_APP_ID`: The **App ID** of your GitHub application. - `INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY`: The **Private Key** of your GitHub application. - Once added, restart your Infisical instance and use the GitHub integration via app authentication. + Once configured, you can use the GitHub integration via app authentication. If you configured the credentials using environment variables, restart your Infisical instance for the changes to take effect. If you configured them through the server admin panel, allow approximately 5 minutes for the changes to propagate. @@ -158,4 +173,5 @@ Infisical supports two methods for connecting to GitHub. + diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 871985ce1..a8f0fb385 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -16,6 +16,12 @@ export const ROUTE_PATHS = Object.freeze({ PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset"), PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup") }, + Admin: { + IntegrationsPage: setRoute( + "/admin/integrations", + "/_authenticate/_inject-org-details/admin/_admin-layout/integrations" + ) + }, Organization: { Settings: { OauthCallbackPage: setRoute( diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 149014d85..c5d92b9da 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -56,6 +56,11 @@ export type TUpdateServerConfigDTO = { microsoftTeamsAppId?: string; microsoftTeamsClientSecret?: string; microsoftTeamsBotId?: string; + gitHubAppConnectionClientId?: string; + gitHubAppConnectionClientSecret?: string; + gitHubAppConnectionSlug?: string; + gitHubAppConnectionId?: string; + gitHubAppConnectionPrivateKey?: string; } & Partial; export type TCreateAdminUserDTO = { @@ -100,6 +105,13 @@ export type AdminIntegrationsConfig = { clientSecret: string; botId: string; }; + gitHubAppConnection: { + clientId: string; + clientSecret: string; + appSlug: string; + appId: string; + privateKey: string; + }; }; export type TGetServerRootKmsEncryptionDetails = { diff --git a/frontend/src/pages/admin/IntegrationsPage/components/GitHubAppConnectionForm.tsx b/frontend/src/pages/admin/IntegrationsPage/components/GitHubAppConnectionForm.tsx new file mode 100644 index 000000000..30af87db3 --- /dev/null +++ b/frontend/src/pages/admin/IntegrationsPage/components/GitHubAppConnectionForm.tsx @@ -0,0 +1,222 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { FaGithub } from "react-icons/fa"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Button, + FormControl, + Input, + TextArea +} from "@app/components/v2"; +import { useToggle } from "@app/hooks"; +import { useUpdateServerConfig } from "@app/hooks/api"; +import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types"; + +const gitHubAppFormSchema = z.object({ + clientId: z.string(), + clientSecret: z.string(), + appSlug: z.string(), + appId: z.string(), + privateKey: z.string() +}); + +type TGitHubAppConnectionForm = z.infer; + +type Props = { + adminIntegrationsConfig?: AdminIntegrationsConfig; +}; + +export const GitHubAppConnectionForm = ({ adminIntegrationsConfig }: Props) => { + const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig(); + const [isGitHubAppClientSecretFocused, setIsGitHubAppClientSecretFocused] = useToggle(); + const { + control, + handleSubmit, + setValue, + formState: { isSubmitting, isDirty } + } = useForm({ + resolver: zodResolver(gitHubAppFormSchema) + }); + + const onSubmit = async (data: TGitHubAppConnectionForm) => { + await updateAdminServerConfig({ + gitHubAppConnectionClientId: data.clientId, + gitHubAppConnectionClientSecret: data.clientSecret, + gitHubAppConnectionSlug: data.appSlug, + gitHubAppConnectionId: data.appId, + gitHubAppConnectionPrivateKey: data.privateKey + }); + + createNotification({ + text: "Updated GitHub app connection configuration. It can take up to 5 minutes to take effect.", + type: "success" + }); + }; + + useEffect(() => { + if (adminIntegrationsConfig) { + setValue("clientId", adminIntegrationsConfig.gitHubAppConnection.clientId); + setValue("clientSecret", adminIntegrationsConfig.gitHubAppConnection.clientSecret); + setValue("appSlug", adminIntegrationsConfig.gitHubAppConnection.appSlug); + setValue("appId", adminIntegrationsConfig.gitHubAppConnection.appId); + setValue("privateKey", adminIntegrationsConfig.gitHubAppConnection.privateKey); + } + }, [adminIntegrationsConfig]); + + return ( +
+ + + +
+ +
GitHub App
+
+
+ +
+
+ Step 1: Create and configure GitHub App. Please refer to the documentation below for + more information. +
+ +
+ Step 2: Configure your instance-wide settings to enable GitHub App connections. Copy + the credentials from your GitHub App's settings page. +
+ ( + + field.onChange(e.target.value)} + /> + + )} + /> + ( + + setIsGitHubAppClientSecretFocused.on()} + onBlur={() => setIsGitHubAppClientSecretFocused.off()} + onChange={(e) => field.onChange(e.target.value)} + /> + + )} + /> + + ( + + field.onChange(e.target.value)} + /> + + )} + /> + + ( + + field.onChange(e.target.value)} + /> + + )} + /> + + ( + +