From b67fcad252a8bbeab380c18e1c7ad5d42f69523a Mon Sep 17 00:00:00 2001 From: Sid <58144379+sidwebworks@users.noreply.github.com> Date: Fri, 11 Jul 2025 23:56:55 +0530 Subject: [PATCH] feat: migrate github app connection to env override (#4004) * feat: migrate github app connection to env override * fix: remove usage of github app integration * chore: lint fix * fix: migration cleanup * fix: refactor integrations tab * fix: content * fix: remove integrations tab --------- Co-authored-by: sidwebworks --- ...0_github-app-connection-to-environments.ts | 66 ++++++ .../github/github-connection-fns.ts | 15 +- .../components/GitHubAppConnectionForm.tsx | 222 ------------------ .../components/IntegrationsPageForm.tsx | 19 +- 4 files changed, 72 insertions(+), 250 deletions(-) create mode 100644 backend/src/db/migrations/20250711005900_github-app-connection-to-environments.ts delete mode 100644 frontend/src/pages/admin/IntegrationsPage/components/GitHubAppConnectionForm.tsx diff --git a/backend/src/db/migrations/20250711005900_github-app-connection-to-environments.ts b/backend/src/db/migrations/20250711005900_github-app-connection-to-environments.ts new file mode 100644 index 000000000..14e9ddd65 --- /dev/null +++ b/backend/src/db/migrations/20250711005900_github-app-connection-to-environments.ts @@ -0,0 +1,66 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { selectAllTableCols } from "@app/lib/knex"; + +import { TableName } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { getMigrationEncryptionServices } from "./utils/services"; + +export async function up(knex: Knex) { + const existingSuperAdminsWithGithubConnection = await knex(TableName.SuperAdmin) + .select(selectAllTableCols(TableName.SuperAdmin)) + .whereNotNull(`${TableName.SuperAdmin}.encryptedGitHubAppConnectionClientId`); + + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + + const decryptor = kmsService.decryptWithRootKey(); + const encryptor = kmsService.encryptWithRootKey(); + + const tasks = existingSuperAdminsWithGithubConnection.map(async (admin) => { + const overrides = ( + admin.encryptedEnvOverrides ? JSON.parse(decryptor(Buffer.from(admin.encryptedEnvOverrides)).toString()) : {} + ) as Record; + + if (admin.encryptedGitHubAppConnectionClientId) { + overrides.INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID = decryptor( + admin.encryptedGitHubAppConnectionClientId + ).toString(); + } + + if (admin.encryptedGitHubAppConnectionClientSecret) { + overrides.INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET = decryptor( + admin.encryptedGitHubAppConnectionClientSecret + ).toString(); + } + + if (admin.encryptedGitHubAppConnectionPrivateKey) { + overrides.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY = decryptor( + admin.encryptedGitHubAppConnectionPrivateKey + ).toString(); + } + + if (admin.encryptedGitHubAppConnectionSlug) { + overrides.INF_APP_CONNECTION_GITHUB_APP_SLUG = decryptor(admin.encryptedGitHubAppConnectionSlug).toString(); + } + + if (admin.encryptedGitHubAppConnectionId) { + overrides.INF_APP_CONNECTION_GITHUB_APP_ID = decryptor(admin.encryptedGitHubAppConnectionId).toString(); + } + + const encryptedEnvOverrides = encryptor(Buffer.from(JSON.stringify(overrides))); + + await knex(TableName.SuperAdmin).where({ id: admin.id }).update({ + encryptedEnvOverrides + }); + }); + + await Promise.all(tasks); +} + +export async function down() { + // No down migration needed as this migration is only for data transformation + // and does not change the schema. +} 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 ebdd09289..d8c98e832 100644 --- a/backend/src/services/app-connection/github/github-connection-fns.ts +++ b/backend/src/services/app-connection/github/github-connection-fns.ts @@ -7,7 +7,6 @@ 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"; @@ -15,14 +14,13 @@ 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: gitHubAppConnection.appSlug || INF_APP_CONNECTION_GITHUB_APP_SLUG + appClientSlug: INF_APP_CONNECTION_GITHUB_APP_SLUG }; }; @@ -32,10 +30,9 @@ 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; + const appId = appCfg.INF_APP_CONNECTION_GITHUB_APP_ID; + const appPrivateKey = appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY; switch (method) { case GitHubConnectionMethod.App: @@ -157,8 +154,6 @@ 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, @@ -170,8 +165,8 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect const { clientId, clientSecret } = method === GitHubConnectionMethod.App ? { - clientId: gitHubAppConnection.clientId || INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID, - clientSecret: gitHubAppConnection.clientSecret || INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET + clientId: INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID, + clientSecret: INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET } : // oauth { diff --git a/frontend/src/pages/admin/IntegrationsPage/components/GitHubAppConnectionForm.tsx b/frontend/src/pages/admin/IntegrationsPage/components/GitHubAppConnectionForm.tsx deleted file mode 100644 index 30af87db3..000000000 --- a/frontend/src/pages/admin/IntegrationsPage/components/GitHubAppConnectionForm.tsx +++ /dev/null @@ -1,222 +0,0 @@ -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)} - /> - - )} - /> - - ( - -