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 57d01be29..4e36d85a4 100644 --- a/backend/src/services/app-connection/github/github-connection-fns.ts +++ b/backend/src/services/app-connection/github/github-connection-fns.ts @@ -12,7 +12,6 @@ import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { logger } from "@app/lib/logger"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns"; -import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { AppConnection } from "../app-connection-enums"; import { GitHubConnectionMethod } from "./github-connection-enums"; @@ -30,6 +29,23 @@ export const getGitHubConnectionListItem = () => { }; }; +export const getGitHubInstanceApiUrl = async (config: { + credentials: Pick; +}) => { + const host = config.credentials.host || "github.com"; + + await blockLocalAndPrivateIpAddresses(host); + + let apiBase: string; + if (config.credentials.instanceType === "server") { + apiBase = `${host}/api/v3`; + } else { + apiBase = `api.${host}`; + } + + return apiBase; +}; + export const requestWithGitHubGateway = async ( appConnection: { gatewayId?: string | null }, gatewayService: Pick, @@ -141,7 +157,7 @@ export const makePaginatedGitHubRequest = async ( const token = method === GitHubConnectionMethod.OAuth ? credentials.accessToken : await getGitHubAppAuthToken(appConnection); - let url: string | null = `https://api.${credentials.host || "github.com"}${path}`; + let url: string | null = `https://${await getGitHubInstanceApiUrl(appConnection)}${path}`; let results: T[] = []; let i = 0; @@ -355,7 +371,7 @@ export const validateGitHubConnectionCredentials = async ( }; }[]; }>(config, gatewayService, { - url: IntegrationUrls.GITHUB_USER_INSTALLATIONS.replace("api.github.com", `api.${host}`), + url: `https://${await getGitHubInstanceApiUrl(config)}/user/installations`, headers: { Accept: "application/json", Authorization: `Bearer ${tokenResp.data.access_token}`, diff --git a/backend/src/services/app-connection/github/github-connection-schemas.ts b/backend/src/services/app-connection/github/github-connection-schemas.ts index bf92ec155..2a39208c3 100644 --- a/backend/src/services/app-connection/github/github-connection-schemas.ts +++ b/backend/src/services/app-connection/github/github-connection-schemas.ts @@ -10,26 +10,59 @@ import { import { GitHubConnectionMethod } from "./github-connection-enums"; -export const GitHubConnectionOAuthInputCredentialsSchema = z.object({ - code: z.string().trim().min(1, "OAuth code required"), - host: z.string().trim().optional() -}); +export const GitHubConnectionOAuthInputCredentialsSchema = z.union([ + z.object({ + code: z.string().trim().min(1, "OAuth code required"), + instanceType: z.literal("server"), + host: z.string().trim().min(1, "Host is required for server instance type") + }), + z.object({ + code: z.string().trim().min(1, "OAuth code required"), + instanceType: z.literal("cloud").optional(), + host: z.string().trim().optional() + }) +]); -export const GitHubConnectionAppInputCredentialsSchema = z.object({ - code: z.string().trim().min(1, "GitHub App code required"), - installationId: z.string().min(1, "GitHub App Installation ID required"), - host: z.string().trim().optional() -}); +export const GitHubConnectionAppInputCredentialsSchema = z.union([ + z.object({ + code: z.string().trim().min(1, "GitHub App code required"), + installationId: z.string().min(1, "GitHub App Installation ID required"), + instanceType: z.literal("server"), + host: z.string().trim().min(1, "Host is required for server instance type") + }), + z.object({ + code: z.string().trim().min(1, "GitHub App code required"), + installationId: z.string().min(1, "GitHub App Installation ID required"), + instanceType: z.literal("cloud").optional(), + host: z.string().trim().optional() + }) +]); -export const GitHubConnectionOAuthOutputCredentialsSchema = z.object({ - accessToken: z.string(), - host: z.string().trim().optional() -}); +export const GitHubConnectionOAuthOutputCredentialsSchema = z.union([ + z.object({ + accessToken: z.string(), + instanceType: z.literal("server"), + host: z.string().trim().min(1) + }), + z.object({ + accessToken: z.string(), + instanceType: z.literal("cloud").optional(), + host: z.string().trim().optional() + }) +]); -export const GitHubConnectionAppOutputCredentialsSchema = z.object({ - installationId: z.string(), - host: z.string().trim().optional() -}); +export const GitHubConnectionAppOutputCredentialsSchema = z.union([ + z.object({ + installationId: z.string(), + instanceType: z.literal("server"), + host: z.string().trim().min(1) + }), + z.object({ + installationId: z.string(), + instanceType: z.literal("cloud").optional(), + host: z.string().trim().optional() + }) +]); export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [ z.object({ @@ -84,11 +117,17 @@ export const GitHubConnectionSchema = z.intersection( export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [ BaseGitHubConnectionSchema.extend({ method: z.literal(GitHubConnectionMethod.App), - credentials: GitHubConnectionAppOutputCredentialsSchema.pick({}) + credentials: z.object({ + instanceType: z.string().optional(), + host: z.string().optional() + }) }), BaseGitHubConnectionSchema.extend({ method: z.literal(GitHubConnectionMethod.OAuth), - credentials: GitHubConnectionOAuthOutputCredentialsSchema.pick({}) + credentials: z.object({ + instanceType: z.string().optional(), + host: z.string().optional() + }) }) ]); diff --git a/backend/src/services/secret-sync/github/github-sync-fns.ts b/backend/src/services/secret-sync/github/github-sync-fns.ts index b37d5e90e..e2cf8f6e8 100644 --- a/backend/src/services/secret-sync/github/github-sync-fns.ts +++ b/backend/src/services/secret-sync/github/github-sync-fns.ts @@ -3,6 +3,7 @@ import sodium from "libsodium-wrappers"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { getGitHubAppAuthToken, + getGitHubInstanceApiUrl, GitHubConnectionMethod, makePaginatedGitHubRequest, requestWithGitHubGateway @@ -73,7 +74,7 @@ const getPublicKey = async ( } const response = await requestWithGitHubGateway(connection, gatewayService, { - url: `https://api.${connection.credentials.host || "github.com"}${path}`, + url: `https://${await getGitHubInstanceApiUrl(connection)}${path}`, method: "GET", headers: { Accept: "application/vnd.github+json", @@ -111,7 +112,7 @@ const deleteSecret = async ( } await requestWithGitHubGateway(connection, gatewayService, { - url: `https://api.${connection.credentials.host || "github.com"}${path}`, + url: `https://${await getGitHubInstanceApiUrl(connection)}${path}`, method: "DELETE", headers: { Accept: "application/vnd.github+json", @@ -157,7 +158,7 @@ const putSecret = async ( } await requestWithGitHubGateway(connection, gatewayService, { - url: `https://api.${connection.credentials.host || "github.com"}${path}`, + url: `https://${await getGitHubInstanceApiUrl(connection)}${path}`, method: "PUT", headers: { Accept: "application/vnd.github+json", diff --git a/frontend/src/hooks/api/appConnections/types/github-connection.ts b/frontend/src/hooks/api/appConnections/types/github-connection.ts index a8b734aa9..27bed2dcb 100644 --- a/frontend/src/hooks/api/appConnections/types/github-connection.ts +++ b/frontend/src/hooks/api/appConnections/types/github-connection.ts @@ -11,6 +11,7 @@ export type TGitHubConnection = TRootAppConnection & { app: AppConnection.GitHub method: GitHubConnectionMethod.OAuth; credentials: { code: string; + instanceType?: "cloud" | "server"; host?: string; }; } @@ -19,6 +20,7 @@ export type TGitHubConnection = TRootAppConnection & { app: AppConnection.GitHub credentials: { code: string; installationId: string; + instanceType?: "cloud" | "server"; host?: string; }; } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx index eb93993af..d80bb23fe 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx @@ -48,9 +48,16 @@ const formSchema = genericAppConnectionFieldsSchema.extend({ app: z.literal(AppConnection.GitHub), method: z.nativeEnum(GitHubConnectionMethod), credentials: z - .object({ - host: z.string().optional() - }) + .union([ + z.object({ + instanceType: z.literal("cloud").optional(), + host: z.string().optional() + }), + z.object({ + instanceType: z.literal("server"), + host: z.string().min(1, "Required") + }) + ]) .optional() }); @@ -70,7 +77,10 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => { defaultValues: appConnection ?? { app: AppConnection.GitHub, method: GitHubConnectionMethod.App, - gatewayId: null + gatewayId: null, + credentials: { + instanceType: "cloud" + } } }); @@ -78,6 +88,7 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => { handleSubmit, control, watch, + setValue, formState: { isSubmitting, isDirty } } = form; @@ -85,6 +96,7 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => { const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); const selectedMethod = watch("method"); + const instanceType = watch("credentials.instanceType"); const onSubmit = (formData: FormData) => { setIsRedirecting(true); @@ -103,7 +115,7 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => { switch (formData.method) { case GitHubConnectionMethod.App: window.location.assign( - `${githubHost}/apps/${appClientSlug}/installations/new?state=${state}` + `${githubHost}/${formData.credentials?.instanceType === "server" ? "github-apps" : "apps"}/${appClientSlug}/installations/new?state=${state}` ); break; case GitHubConnectionMethod.OAuth: @@ -175,13 +187,54 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => { )} /> - {subscription.gateway && ( - - - -
GitHub Enterprise Options
-
- + + + +
GitHub Enterprise Options
+
+ + ( + + + + )} + /> + + ( + + + + )} + /> + {subscription.gateway && instanceType === "server" && ( { ( { /> )} - ( - - - - )} - /> - -
-
- )} + )} +
+
+