Checkpoint

This commit is contained in:
x032205
2025-07-24 16:37:38 -04:00
parent 75622ed03e
commit 2ff211d235
13 changed files with 471 additions and 157 deletions
+11 -10
View File
@@ -1039,6 +1039,15 @@ export const registerRoutes = async (
kmsService kmsService
}); });
const gatewayService = gatewayServiceFactory({
permissionService,
gatewayDAL,
kmsService,
licenseService,
orgGatewayConfigDAL,
keyStore
});
const secretSyncQueue = secretSyncQueueFactory({ const secretSyncQueue = secretSyncQueueFactory({
queueService, queueService,
secretSyncDAL, secretSyncDAL,
@@ -1062,7 +1071,8 @@ export const registerRoutes = async (
secretVersionTagV2BridgeDAL, secretVersionTagV2BridgeDAL,
resourceMetadataDAL, resourceMetadataDAL,
appConnectionDAL, appConnectionDAL,
licenseService licenseService,
gatewayService
}); });
const secretQueueService = secretQueueFactory({ const secretQueueService = secretQueueFactory({
@@ -1481,15 +1491,6 @@ export const registerRoutes = async (
licenseService licenseService
}); });
const gatewayService = gatewayServiceFactory({
permissionService,
gatewayDAL,
kmsService,
licenseService,
orgGatewayConfigDAL,
keyStore
});
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({ const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
identityKubernetesAuthDAL, identityKubernetesAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
@@ -583,7 +583,7 @@ export const appConnectionServiceFactory = ({
deleteAppConnection, deleteAppConnection,
connectAppConnectionById, connectAppConnectionById,
listAvailableAppConnectionsForUser, listAvailableAppConnectionsForUser,
github: githubConnectionService(connectAppConnectionById), github: githubConnectionService(connectAppConnectionById, gatewayService),
githubRadar: githubRadarConnectionService(connectAppConnectionById), githubRadar: githubRadarConnectionService(connectAppConnectionById),
gcp: gcpConnectionService(connectAppConnectionById), gcp: gcpConnectionService(connectAppConnectionById),
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
@@ -1,10 +1,15 @@
import { createAppAuth } from "@octokit/auth-app"; import { createAppAuth } from "@octokit/auth-app";
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import { AxiosResponse } from "axios"; import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
import https from "https";
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request"; import { request as httpRequest } from "@app/lib/config/request";
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { logger } from "@app/lib/logger";
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns"; import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
@@ -24,10 +29,14 @@ export const getGitHubConnectionListItem = () => {
}; };
}; };
export const getGitHubClient = (appConnection: TGitHubConnection) => { export const getGitHubClient = (
appConnection: TGitHubConnection,
octokitOptions: Partial<{ baseUrl: string; request: { agent?: https.Agent } }>
) => {
const appCfg = getConfig(); const appCfg = getConfig();
const { method, credentials } = appConnection; const { method, credentials } = appConnection;
const { baseUrl, request } = octokitOptions;
let client: Octokit; let client: Octokit;
@@ -48,12 +57,16 @@ export const getGitHubClient = (appConnection: TGitHubConnection) => {
appId, appId,
privateKey: appPrivateKey, privateKey: appPrivateKey,
installationId: credentials.installationId installationId: credentials.installationId
} },
baseUrl,
request
}); });
break; break;
case GitHubConnectionMethod.OAuth: case GitHubConnectionMethod.OAuth:
client = new Octokit({ client = new Octokit({
auth: credentials.accessToken auth: credentials.accessToken,
baseUrl,
request
}); });
break; break;
default: default:
@@ -65,6 +78,139 @@ export const getGitHubClient = (appConnection: TGitHubConnection) => {
return client; return client;
}; };
export const executeWithGitHubGateway = async <T>(
appConnection: TGitHubConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
operation: (client: Octokit) => Promise<T>
): Promise<T> => {
const {
gatewayId,
credentials: { host: hostParam }
} = appConnection;
const host = hostParam || "api.github.com";
if (gatewayId && gatewayService) {
const [targetHost] = await verifyHostInputValidity(host, true);
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
return withGatewayProxy(
async (proxyPort) => {
const agent = new https.Agent({
servername: targetHost,
rejectUnauthorized: true
});
const client = getGitHubClient(appConnection, {
baseUrl: `https://localhost:${proxyPort}`,
request: { agent }
});
return operation(client);
},
{
protocol: GatewayProxyProtocol.Tcp,
targetHost,
targetPort: 443,
relayHost,
relayPort: Number(relayPort),
identityId: relayDetails.identityId,
orgId: relayDetails.orgId,
tlsOptions: {
ca: relayDetails.certChain,
cert: relayDetails.certificate,
key: relayDetails.privateKey.toString()
}
}
);
}
// Non-gateway path
const client = getGitHubClient(appConnection, {
baseUrl: `https://${host}`
});
return operation(client);
};
// For non-octokit requests
export const requestWithGitHubGateway = async <T>(
appConnection: TGitHubConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
requestConfig: AxiosRequestConfig
): Promise<AxiosResponse<T>> => {
const {
gatewayId,
credentials: { host: hostParam }
} = appConnection;
const url = new URL(requestConfig.url as string);
const host = hostParam || url.host || "github.com";
if (gatewayId && gatewayService) {
const [targetHost] = await verifyHostInputValidity(host, true);
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
return withGatewayProxy(
async (proxyPort) => {
const proxyAgent = new https.Agent({
servername: targetHost,
rejectUnauthorized: true
});
url.protocol = "https:";
url.host = `localhost:${proxyPort}`;
const finalRequestConfig: AxiosRequestConfig = {
...requestConfig,
url: url.toString(),
httpsAgent: proxyAgent,
headers: {
...requestConfig.headers,
Host: targetHost
}
};
try {
return await httpRequest.request(finalRequestConfig);
} catch (error) {
const axiosError = error as AxiosError;
logger.error("Error during GitHub gateway request:", axiosError.message, axiosError.response?.data);
throw error;
}
},
{
protocol: GatewayProxyProtocol.Tcp,
targetHost,
targetPort: 443,
relayHost,
relayPort: Number(relayPort),
identityId: relayDetails.identityId,
orgId: relayDetails.orgId,
tlsOptions: {
ca: relayDetails.certChain,
cert: relayDetails.certificate,
key: relayDetails.privateKey.toString()
}
}
);
}
if (!url.host) {
url.protocol = "https:";
url.host = host;
}
const finalRequestConfig: AxiosRequestConfig = {
...requestConfig,
url: url.toString()
};
return httpRequest.request(finalRequestConfig);
};
type GitHubOrganization = { type GitHubOrganization = {
login: string; login: string;
id: number; id: number;
@@ -76,72 +222,83 @@ type GitHubRepository = {
owner: GitHubOrganization; owner: GitHubOrganization;
}; };
export const getGitHubRepositories = async (appConnection: TGitHubConnection) => { export const getGitHubRepositories = async (
const client = getGitHubClient(appConnection); appConnection: TGitHubConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
return executeWithGitHubGateway(appConnection, gatewayService, async (client) => {
let repositories: GitHubRepository[];
let repositories: GitHubRepository[]; switch (appConnection.method) {
case GitHubConnectionMethod.App:
repositories = await client.paginate("GET /installation/repositories");
break;
case GitHubConnectionMethod.OAuth:
default:
repositories = (await client.paginate("GET /user/repos")).filter((repo) => repo.permissions?.admin);
break;
}
switch (appConnection.method) { return repositories;
case GitHubConnectionMethod.App: });
repositories = await client.paginate("GET /installation/repositories");
break;
case GitHubConnectionMethod.OAuth:
default:
repositories = (await client.paginate("GET /user/repos")).filter((repo) => repo.permissions?.admin);
break;
}
return repositories;
}; };
export const getGitHubOrganizations = async (appConnection: TGitHubConnection) => { export const getGitHubOrganizations = async (
const client = getGitHubClient(appConnection); appConnection: TGitHubConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
return executeWithGitHubGateway(appConnection, gatewayService, async (client) => {
let organizations: GitHubOrganization[];
let organizations: GitHubOrganization[]; switch (appConnection.method) {
case GitHubConnectionMethod.App: {
const installationRepositories = await client.paginate("GET /installation/repositories");
switch (appConnection.method) { const organizationMap: Record<string, GitHubOrganization> = {};
case GitHubConnectionMethod.App: {
const installationRepositories = await client.paginate("GET /installation/repositories");
const organizationMap: Record<string, GitHubOrganization> = {}; installationRepositories.forEach((repo) => {
if (repo.owner.type === "Organization") {
organizationMap[repo.owner.id] = repo.owner;
}
});
installationRepositories.forEach((repo) => { organizations = Object.values(organizationMap);
if (repo.owner.type === "Organization") {
organizationMap[repo.owner.id] = repo.owner; break;
} }
case GitHubConnectionMethod.OAuth:
default:
organizations = await client.paginate("GET /user/orgs");
break;
}
return organizations;
});
};
export const getGitHubEnvironments = async (
appConnection: TGitHubConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
owner: string,
repo: string
) => {
return executeWithGitHubGateway(appConnection, gatewayService, async (client) => {
try {
const environments = await client.paginate("GET /repos/{owner}/{repo}/environments", {
owner,
repo
}); });
organizations = Object.values(organizationMap); return environments;
} catch (e) {
// repo doesn't have envs
if ((e as { status: number }).status === 404) {
return [];
}
break; throw e;
} }
case GitHubConnectionMethod.OAuth: });
default:
organizations = await client.paginate("GET /user/orgs");
break;
}
return organizations;
};
export const getGitHubEnvironments = async (appConnection: TGitHubConnection, owner: string, repo: string) => {
const client = getGitHubClient(appConnection);
try {
const environments = await client.paginate("GET /repos/{owner}/{repo}/environments", {
owner,
repo
});
return environments;
} catch (e) {
// repo doesn't have envs
if ((e as { status: number }).status === 404) {
return [];
}
throw e;
}
}; };
export type GithubTokenRespData = { export type GithubTokenRespData = {
@@ -159,9 +316,11 @@ export function isGithubErrorResponse(data: GithubTokenRespData): data is Github
return "error" in data; return "error" in data;
} }
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => { export const validateGitHubConnectionCredentials = async (
config: TGitHubConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
const { credentials, method } = config; const { credentials, method } = config;
const { const {
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET, INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
@@ -192,10 +351,14 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
} }
let tokenResp: AxiosResponse<GithubTokenRespData>; let tokenResp: AxiosResponse<GithubTokenRespData>;
const host = credentials.host || "github.com";
const apiHost = credentials.host ? `api.${credentials.host}` : "api.github.com";
try { try {
tokenResp = await request.get<GithubTokenRespData>("https://github.com/login/oauth/access_token", { tokenResp = await requestWithGitHubGateway<GithubTokenRespData>(config, gatewayService, {
params: { url: `https://${host}/login/oauth/access_token`,
method: "POST",
data: {
client_id: clientId, client_id: clientId,
client_secret: clientSecret, client_secret: clientSecret,
code: credentials.code, code: credentials.code,
@@ -203,7 +366,7 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
}, },
headers: { headers: {
Accept: "application/json", Accept: "application/json",
"Accept-Encoding": "application/json" "Content-Type": "application/json"
} }
}); });
@@ -233,7 +396,7 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` }); throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
} }
const installationsResp = await request.get<{ const installationsResp = await requestWithGitHubGateway<{
installations: { installations: {
id: number; id: number;
account: { account: {
@@ -242,7 +405,8 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
id: number; id: number;
}; };
}[]; }[];
}>(IntegrationUrls.GITHUB_USER_INSTALLATIONS, { }>(config, gatewayService, {
url: IntegrationUrls.GITHUB_USER_INSTALLATIONS.replace("api.github.com", apiHost),
headers: { headers: {
Accept: "application/json", Accept: "application/json",
Authorization: `Bearer ${tokenResp.data.access_token}`, Authorization: `Bearer ${tokenResp.data.access_token}`,
@@ -11,20 +11,24 @@ import {
import { GitHubConnectionMethod } from "./github-connection-enums"; import { GitHubConnectionMethod } from "./github-connection-enums";
export const GitHubConnectionOAuthInputCredentialsSchema = z.object({ export const GitHubConnectionOAuthInputCredentialsSchema = z.object({
code: z.string().trim().min(1, "OAuth code required") code: z.string().trim().min(1, "OAuth code required"),
host: z.string().trim().optional()
}); });
export const GitHubConnectionAppInputCredentialsSchema = z.object({ export const GitHubConnectionAppInputCredentialsSchema = z.object({
code: z.string().trim().min(1, "GitHub App code required"), code: z.string().trim().min(1, "GitHub App code required"),
installationId: z.string().min(1, "GitHub App Installation ID required") installationId: z.string().min(1, "GitHub App Installation ID required"),
host: z.string().trim().optional()
}); });
export const GitHubConnectionOAuthOutputCredentialsSchema = z.object({ export const GitHubConnectionOAuthOutputCredentialsSchema = z.object({
accessToken: z.string() accessToken: z.string(),
host: z.string().trim().optional()
}); });
export const GitHubConnectionAppOutputCredentialsSchema = z.object({ export const GitHubConnectionAppOutputCredentialsSchema = z.object({
installationId: z.string() installationId: z.string(),
host: z.string().trim().optional()
}); });
export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -43,7 +47,9 @@ export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("m
]); ]);
export const CreateGitHubConnectionSchema = ValidateGitHubConnectionCredentialsSchema.and( export const CreateGitHubConnectionSchema = ValidateGitHubConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.GitHub) GenericCreateAppConnectionFieldsSchema(AppConnection.GitHub, {
supportsGateways: true
})
); );
export const UpdateGitHubConnectionSchema = z export const UpdateGitHubConnectionSchema = z
@@ -53,7 +59,11 @@ export const UpdateGitHubConnectionSchema = z
.optional() .optional()
.describe(AppConnections.UPDATE(AppConnection.GitHub).credentials) .describe(AppConnections.UPDATE(AppConnection.GitHub).credentials)
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GitHub)); .and(
GenericUpdateAppConnectionFieldsSchema(AppConnection.GitHub, {
supportsGateways: true
})
);
const BaseGitHubConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GitHub) }); const BaseGitHubConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GitHub) });
@@ -1,3 +1,4 @@
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { import {
@@ -19,11 +20,14 @@ type TListGitHubEnvironmentsDTO = {
owner: string; owner: string;
}; };
export const githubConnectionService = (getAppConnection: TGetAppConnectionFunc) => { export const githubConnectionService = (
getAppConnection: TGetAppConnectionFunc,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
const listRepositories = async (connectionId: string, actor: OrgServiceActor) => { const listRepositories = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor); const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor);
const repositories = await getGitHubRepositories(appConnection); const repositories = await getGitHubRepositories(appConnection, gatewayService);
return repositories; return repositories;
}; };
@@ -31,7 +35,7 @@ export const githubConnectionService = (getAppConnection: TGetAppConnectionFunc)
const listOrganizations = async (connectionId: string, actor: OrgServiceActor) => { const listOrganizations = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor); const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor);
const organizations = await getGitHubOrganizations(appConnection); const organizations = await getGitHubOrganizations(appConnection, gatewayService);
return organizations; return organizations;
}; };
@@ -42,7 +46,7 @@ export const githubConnectionService = (getAppConnection: TGetAppConnectionFunc)
) => { ) => {
const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor); const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor);
const environments = await getGitHubEnvironments(appConnection, owner, repo); const environments = await getGitHubEnvironments(appConnection, gatewayService, owner, repo);
return environments; return environments;
}; };
@@ -17,4 +17,7 @@ export type TGitHubConnectionInput = z.infer<typeof CreateGitHubConnectionSchema
export type TValidateGitHubConnectionCredentialsSchema = typeof ValidateGitHubConnectionCredentialsSchema; export type TValidateGitHubConnectionCredentialsSchema = typeof ValidateGitHubConnectionCredentialsSchema;
export type TGitHubConnectionConfig = DiscriminativePick<TGitHubConnectionInput, "method" | "app" | "credentials">; export type TGitHubConnectionConfig = DiscriminativePick<
TGitHubConnectionInput,
"method" | "app" | "credentials" | "gatewayId"
>;
@@ -1,7 +1,8 @@
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import sodium from "libsodium-wrappers"; import sodium from "libsodium-wrappers";
import { getGitHubClient } from "@app/services/app-connection/github"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { executeWithGitHubGateway } from "@app/services/app-connection/github";
import { GitHubSyncScope, GitHubSyncVisibility } from "@app/services/secret-sync/github/github-sync-enums"; import { GitHubSyncScope, GitHubSyncVisibility } from "@app/services/secret-sync/github/github-sync-enums";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
@@ -160,7 +161,11 @@ const putSecret = async (client: Octokit, secretSync: TGitHubSyncWithCredentials
}; };
export const GithubSyncFns = { export const GithubSyncFns = {
syncSecrets: async (secretSync: TGitHubSyncWithCredentials, secretMap: TSecretMap) => { syncSecrets: async (
secretSync: TGitHubSyncWithCredentials,
secretMap: TSecretMap,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
switch (secretSync.destinationConfig.scope) { switch (secretSync.destinationConfig.scope) {
case GitHubSyncScope.Organization: case GitHubSyncScope.Organization:
if (Object.values(secretMap).length > 1000) { if (Object.values(secretMap).length > 1000) {
@@ -187,63 +192,67 @@ export const GithubSyncFns = {
); );
} }
const client = getGitHubClient(secretSync.connection); await executeWithGitHubGateway(secretSync.connection, gatewayService, async (client) => {
const encryptedSecrets = await getEncryptedSecrets(client, secretSync);
const encryptedSecrets = await getEncryptedSecrets(client, secretSync); const publicKey = await getPublicKey(client, secretSync);
const publicKey = await getPublicKey(client, secretSync); await sodium.ready.then(async () => {
for await (const key of Object.keys(secretMap)) {
// convert secret & base64 key to Uint8Array.
const binaryKey = sodium.from_base64(publicKey.key, sodium.base64_variants.ORIGINAL);
const binarySecretValue = sodium.from_string(secretMap[key].value);
await sodium.ready.then(async () => { // encrypt secret using libsodium
for await (const key of Object.keys(secretMap)) { const encryptedBytes = sodium.crypto_box_seal(binarySecretValue, binaryKey);
// convert secret & base64 key to Uint8Array.
const binaryKey = sodium.from_base64(publicKey.key, sodium.base64_variants.ORIGINAL);
const binarySecretValue = sodium.from_string(secretMap[key].value);
// encrypt secret using libsodium // convert encrypted Uint8Array to base64
const encryptedBytes = sodium.crypto_box_seal(binarySecretValue, binaryKey); const encryptedSecretValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL);
// convert encrypted Uint8Array to base64 try {
const encryptedSecretValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL); await putSecret(client, secretSync, {
secret_name: key,
encrypted_value: encryptedSecretValue,
key_id: publicKey.key_id
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
});
try { if (secretSync.syncOptions.disableSecretDeletion) return;
await putSecret(client, secretSync, {
secret_name: key, for await (const encryptedSecret of encryptedSecrets) {
encrypted_value: encryptedSecretValue, if (!matchesSchema(encryptedSecret.name, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema))
key_id: publicKey.key_id // eslint-disable-next-line no-continue
}); continue;
} catch (error) {
throw new SecretSyncError({ if (!(encryptedSecret.name in secretMap)) {
error, await deleteSecret(client, secretSync, encryptedSecret);
secretKey: key
});
} }
} }
}); });
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const encryptedSecret of encryptedSecrets) {
if (!matchesSchema(encryptedSecret.name, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema))
// eslint-disable-next-line no-continue
continue;
if (!(encryptedSecret.name in secretMap)) {
await deleteSecret(client, secretSync, encryptedSecret);
}
}
}, },
getSecrets: async (secretSync: TGitHubSyncWithCredentials) => { getSecrets: async (secretSync: TGitHubSyncWithCredentials) => {
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
}, },
removeSecrets: async (secretSync: TGitHubSyncWithCredentials, secretMap: TSecretMap) => { removeSecrets: async (
const client = getGitHubClient(secretSync.connection); secretSync: TGitHubSyncWithCredentials,
secretMap: TSecretMap,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
await executeWithGitHubGateway(secretSync.connection, gatewayService, async (client) => {
const encryptedSecrets = await getEncryptedSecrets(client, secretSync);
const encryptedSecrets = await getEncryptedSecrets(client, secretSync); for await (const encryptedSecret of encryptedSecrets) {
if (encryptedSecret.name in secretMap) {
for await (const encryptedSecret of encryptedSecrets) { await deleteSecret(client, secretSync, encryptedSecret);
if (encryptedSecret.name in secretMap) { }
await deleteSecret(client, secretSync, encryptedSecret);
} }
} });
} }
}; };
@@ -1,6 +1,7 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import handlebars from "handlebars"; import handlebars from "handlebars";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "@app/ee/services/secret-sync/oci-vault"; import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "@app/ee/services/secret-sync/oci-vault";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
@@ -97,6 +98,7 @@ export const listSecretSyncOptions = () => {
type TSyncSecretDeps = { type TSyncSecretDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">; appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
}; };
// Add schema to secret keys // Add schema to secret keys
@@ -191,7 +193,7 @@ export const SecretSyncFns = {
syncSecrets: ( syncSecrets: (
secretSync: TSecretSyncWithCredentials, secretSync: TSecretSyncWithCredentials,
secretMap: TSecretMap, secretMap: TSecretMap,
{ kmsService, appConnectionDAL }: TSyncSecretDeps { kmsService, appConnectionDAL, gatewayService }: TSyncSecretDeps
): Promise<void> => { ): Promise<void> => {
const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema); const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema);
@@ -201,7 +203,7 @@ export const SecretSyncFns = {
case SecretSync.AWSSecretsManager: case SecretSync.AWSSecretsManager:
return AwsSecretsManagerSyncFns.syncSecrets(secretSync, schemaSecretMap); return AwsSecretsManagerSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.GitHub: case SecretSync.GitHub:
return GithubSyncFns.syncSecrets(secretSync, schemaSecretMap); return GithubSyncFns.syncSecrets(secretSync, schemaSecretMap, gatewayService);
case SecretSync.GCPSecretManager: case SecretSync.GCPSecretManager:
return GcpSyncFns.syncSecrets(secretSync, schemaSecretMap); return GcpSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureKeyVault: case SecretSync.AzureKeyVault:
@@ -395,7 +397,7 @@ export const SecretSyncFns = {
removeSecrets: ( removeSecrets: (
secretSync: TSecretSyncWithCredentials, secretSync: TSecretSyncWithCredentials,
secretMap: TSecretMap, secretMap: TSecretMap,
{ kmsService, appConnectionDAL }: TSyncSecretDeps { kmsService, appConnectionDAL, gatewayService }: TSyncSecretDeps
): Promise<void> => { ): Promise<void> => {
const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema); const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema);
@@ -405,7 +407,7 @@ export const SecretSyncFns = {
case SecretSync.AWSSecretsManager: case SecretSync.AWSSecretsManager:
return AwsSecretsManagerSyncFns.removeSecrets(secretSync, schemaSecretMap); return AwsSecretsManagerSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.GitHub: case SecretSync.GitHub:
return GithubSyncFns.removeSecrets(secretSync, schemaSecretMap); return GithubSyncFns.removeSecrets(secretSync, schemaSecretMap, gatewayService);
case SecretSync.GCPSecretManager: case SecretSync.GCPSecretManager:
return GcpSyncFns.removeSecrets(secretSync, schemaSecretMap); return GcpSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureKeyVault: case SecretSync.AzureKeyVault:
@@ -4,6 +4,7 @@ import { Job } from "bullmq";
import { ProjectMembershipRole, SecretType } from "@app/db/schemas"; import { ProjectMembershipRole, SecretType } from "@app/db/schemas";
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
@@ -96,6 +97,7 @@ type TSecretSyncQueueFactoryDep = {
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">; folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
}; };
type SecretSyncActionJob = Job< type SecretSyncActionJob = Job<
@@ -138,7 +140,8 @@ export const secretSyncQueueFactory = ({
secretVersionTagV2BridgeDAL, secretVersionTagV2BridgeDAL,
resourceMetadataDAL, resourceMetadataDAL,
folderCommitService, folderCommitService,
licenseService licenseService,
gatewayService
}: TSecretSyncQueueFactoryDep) => { }: TSecretSyncQueueFactoryDep) => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -353,7 +356,8 @@ export const secretSyncQueueFactory = ({
const importedSecrets = await SecretSyncFns.getSecrets(secretSync, { const importedSecrets = await SecretSyncFns.getSecrets(secretSync, {
appConnectionDAL, appConnectionDAL,
kmsService kmsService,
gatewayService
}); });
if (!Object.keys(importedSecrets).length) return {}; if (!Object.keys(importedSecrets).length) return {};
@@ -481,7 +485,8 @@ export const secretSyncQueueFactory = ({
await SecretSyncFns.syncSecrets(secretSyncWithCredentials, secretMap, { await SecretSyncFns.syncSecrets(secretSyncWithCredentials, secretMap, {
appConnectionDAL, appConnectionDAL,
kmsService kmsService,
gatewayService
}); });
isSynced = true; isSynced = true;
@@ -730,7 +735,8 @@ export const secretSyncQueueFactory = ({
secretMap, secretMap,
{ {
appConnectionDAL, appConnectionDAL,
kmsService kmsService,
gatewayService
} }
); );
@@ -11,6 +11,7 @@ export type TGitHubConnection = TRootAppConnection & { app: AppConnection.GitHub
method: GitHubConnectionMethod.OAuth; method: GitHubConnectionMethod.OAuth;
credentials: { credentials: {
code: string; code: string;
host?: string;
}; };
} }
| { | {
@@ -18,6 +19,7 @@ export type TGitHubConnection = TRootAppConnection & { app: AppConnection.GitHub
credentials: { credentials: {
code: string; code: string;
installationId: string; installationId: string;
host?: string;
}; };
} }
); );
@@ -3,11 +3,30 @@ import crypto from "crypto";
import { useState } from "react"; import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form"; import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { z } from "zod"; import { z } from "zod";
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2"; import { OrgPermissionCan } from "@app/components/permissions";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button,
FormControl,
Input,
ModalClose,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform"; import { isInfisicalCloud } from "@app/helpers/platform";
import { gatewaysQueryKeys } from "@app/hooks/api";
import { import {
GitHubConnectionMethod, GitHubConnectionMethod,
TGitHubConnection, TGitHubConnection,
@@ -26,7 +45,10 @@ type Props = {
const formSchema = genericAppConnectionFieldsSchema.extend({ const formSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.GitHub), app: z.literal(AppConnection.GitHub),
method: z.nativeEnum(GitHubConnectionMethod) method: z.nativeEnum(GitHubConnectionMethod),
credentials: z.object({
host: z.string().optional()
})
}); });
type FormData = z.infer<typeof formSchema>; type FormData = z.infer<typeof formSchema>;
@@ -44,7 +66,8 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: appConnection ?? { defaultValues: appConnection ?? {
app: AppConnection.GitHub, app: AppConnection.GitHub,
method: GitHubConnectionMethod.App method: GitHubConnectionMethod.App,
gatewayId: null
} }
}); });
@@ -55,6 +78,8 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
formState: { isSubmitting, isDirty } formState: { isSubmitting, isDirty }
} = form; } = form;
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
const selectedMethod = watch("method"); const selectedMethod = watch("method");
const onSubmit = (formData: FormData) => { const onSubmit = (formData: FormData) => {
@@ -66,15 +91,20 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
JSON.stringify({ ...formData, connectionId: appConnection?.id }) JSON.stringify({ ...formData, connectionId: appConnection?.id })
); );
const githubHost =
formData.credentials.host && formData.credentials.host.length > 0
? `https://${formData.credentials.host}`
: "https://github.com";
switch (formData.method) { switch (formData.method) {
case GitHubConnectionMethod.App: case GitHubConnectionMethod.App:
window.location.assign( window.location.assign(
`https://github.com/apps/${appClientSlug}/installations/new?state=${state}` `${githubHost}/apps/${appClientSlug}/installations/new?state=${state}`
); );
break; break;
case GitHubConnectionMethod.OAuth: case GitHubConnectionMethod.OAuth:
window.location.assign( window.location.assign(
`https://github.com/login/oauth/authorize?client_id=${oauthClientId}&response_type=code&scope=repo,admin:org&redirect_uri=${window.location.origin}/organization/app-connections/github/oauth/callback&state=${state}` `${githubHost}/login/oauth/authorize?client_id=${oauthClientId}&response_type=code&scope=repo,admin:org&redirect_uri=${window.location.origin}/organization/app-connections/github/oauth/callback&state=${state}`
); );
break; break;
default: default:
@@ -141,6 +171,80 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
</FormControl> </FormControl>
)} )}
/> />
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="enterprise-options" className="data-[state=open]:border-none">
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
<div className="order-1 ml-3">GitHub Enterprise Options</div>
</AccordionTrigger>
<AccordionContent childrenClassName="px-0">
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
<Controller
name="credentials.host"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Hostname"
isOptional
>
<Input {...field} placeholder="github.com" />
</FormControl>
)}
/>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="mt-8 flex items-center"> <div className="mt-8 flex items-center">
<Button <Button
className="mr-4" className="mr-4"
@@ -30,7 +30,8 @@ type BaseFormData = {
isUpdate?: boolean; isUpdate?: boolean;
}; };
type GithubFormData = BaseFormData & Pick<TGitHubConnection, "name" | "method" | "description">; type GithubFormData = BaseFormData &
Pick<TGitHubConnection, "name" | "method" | "description" | "gatewayId" | "credentials">;
type GithubRadarFormData = BaseFormData & type GithubRadarFormData = BaseFormData &
Pick<TGitHubRadarConnection, "name" | "method" | "description">; Pick<TGitHubRadarConnection, "name" | "method" | "description">;
@@ -395,7 +396,7 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.GitHub); clearState(AppConnection.GitHub);
const { connectionId, name, description, returnUrl } = formData; const { connectionId, name, description, returnUrl, gatewayId, credentials } = formData;
try { try {
if (connectionId) { if (connectionId) {
@@ -406,14 +407,18 @@ export const OAuthCallbackPage = () => {
connectionId, connectionId,
credentials: { credentials: {
code: code as string, code: code as string,
installationId: installationId as string installationId: installationId as string,
} host: credentials.host
},
gatewayId
} }
: { : {
connectionId, connectionId,
credentials: { credentials: {
code: code as string code: code as string,
} host: credentials.host
},
gatewayId
}) })
}); });
} else { } else {
@@ -426,14 +431,18 @@ export const OAuthCallbackPage = () => {
method: GitHubConnectionMethod.App, method: GitHubConnectionMethod.App,
credentials: { credentials: {
code: code as string, code: code as string,
installationId: installationId as string installationId: installationId as string,
} host: credentials.host
},
gatewayId
} }
: { : {
method: GitHubConnectionMethod.OAuth, method: GitHubConnectionMethod.OAuth,
credentials: { credentials: {
code: code as string code: code as string,
} host: credentials.host
},
gatewayId
}) })
}); });
} }
@@ -52,7 +52,7 @@ export const ProjectsPage = () => {
<title>{t("common.head-title", { title: t("settings.members.title") })}</title> <title>{t("common.head-title", { title: t("settings.members.title") })}</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
{!isLoading && !serverDetails?.redisConfigured && ( {!isLoading && !serverDetails?.redisConfigured && (
<div className="mb-4 flex flex-col items-start justify-start text-3xl"> <div className="mb-4 flex flex-col items-start justify-start text-3xl">
<p className="mb-4 mr-4 font-semibold text-white">Announcements</p> <p className="mb-4 mr-4 font-semibold text-white">Announcements</p>