Swap away from using octokit due to gateway compatibility issues

This commit is contained in:
x032205
2025-07-24 21:43:18 -04:00
parent 2ff211d235
commit 5df7539f65
2 changed files with 296 additions and 343 deletions
@@ -1,7 +1,7 @@
import { createAppAuth } from "@octokit/auth-app"; import { createAppAuth } from "@octokit/auth-app";
import { Octokit } from "@octokit/rest";
import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios"; import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
import https from "https"; import https from "https";
import RE2 from "re2";
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns"; import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
@@ -29,251 +29,196 @@ export const getGitHubConnectionListItem = () => {
}; };
}; };
export const getGitHubClient = (
appConnection: TGitHubConnection,
octokitOptions: Partial<{ baseUrl: string; request: { agent?: https.Agent } }>
) => {
const appCfg = getConfig();
const { method, credentials } = appConnection;
const { baseUrl, request } = octokitOptions;
let client: Octokit;
const appId = appCfg.INF_APP_CONNECTION_GITHUB_APP_ID;
const appPrivateKey = appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY;
switch (method) {
case GitHubConnectionMethod.App:
if (!appId || !appPrivateKey) {
throw new InternalServerError({
message: `GitHub ${getAppConnectionMethodName(method).replace("GitHub", "")} has not been configured`
});
}
client = new Octokit({
authStrategy: createAppAuth,
auth: {
appId,
privateKey: appPrivateKey,
installationId: credentials.installationId
},
baseUrl,
request
});
break;
case GitHubConnectionMethod.OAuth:
client = new Octokit({
auth: credentials.accessToken,
baseUrl,
request
});
break;
default:
throw new InternalServerError({
message: `Unhandled GitHub connection method: ${method as GitHubConnectionMethod}`
});
}
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>( export const requestWithGitHubGateway = async <T>(
appConnection: TGitHubConnectionConfig, appConnection: { gatewayId?: string | null; credentials: { host?: string } },
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">, gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
requestConfig: AxiosRequestConfig requestConfig: AxiosRequestConfig
): Promise<AxiosResponse<T>> => { ): Promise<AxiosResponse<T>> => {
const { const { gatewayId } = appConnection;
gatewayId,
credentials: { host: hostParam } // If gateway isn't set up, don't proxy request
} = appConnection; if (!gatewayId) {
return httpRequest.request(requestConfig);
}
const url = new URL(requestConfig.url as string); const url = new URL(requestConfig.url as string);
const host = hostParam || url.host || "github.com";
if (gatewayId && gatewayService) { const [targetHost] = await verifyHostInputValidity(url.host, true);
const [targetHost] = await verifyHostInputValidity(host, true); const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId);
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId); const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
return withGatewayProxy( return withGatewayProxy(
async (proxyPort) => { async (proxyPort) => {
const proxyAgent = new https.Agent({ const httpsAgent = new https.Agent({
servername: targetHost, servername: targetHost
rejectUnauthorized: true });
});
url.protocol = "https:"; url.protocol = "https:";
url.host = `localhost:${proxyPort}`; url.host = `localhost:${proxyPort}`;
const finalRequestConfig: AxiosRequestConfig = { const finalRequestConfig: AxiosRequestConfig = {
...requestConfig, ...requestConfig,
url: url.toString(), url: url.toString(),
httpsAgent: proxyAgent, httpsAgent,
headers: { headers: {
...requestConfig.headers, ...requestConfig.headers,
Host: targetHost 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()
} }
};
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()
}
}
);
};
export const getGitHubAppAuthToken = async (appConnection: TGitHubConnection) => {
const appCfg = getConfig();
const appId = appCfg.INF_APP_CONNECTION_GITHUB_APP_ID;
const appPrivateKey = appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY;
if (!appId || !appPrivateKey) {
throw new InternalServerError({
message: `GitHub App keys are not configured.`
});
} }
if (!url.host) { if (appConnection.method !== GitHubConnectionMethod.App) {
url.protocol = "https:"; throw new InternalServerError({ message: "Cannot generate GitHub App token for non-app connection" });
url.host = host;
} }
const finalRequestConfig: AxiosRequestConfig = { const appAuth = createAppAuth({
...requestConfig, appId,
url: url.toString() privateKey: appPrivateKey,
}; installationId: appConnection.credentials.installationId
});
return httpRequest.request(finalRequestConfig); const { token } = await appAuth({ type: "installation" });
return token;
};
export const makePaginatedGitHubRequest = async <T, R = T[]>(
appConnection: TGitHubConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
path: string,
dataMapper?: (data: R) => T[]
): Promise<T[]> => {
const { credentials, method } = appConnection;
const token =
method === GitHubConnectionMethod.OAuth ? credentials.accessToken : await getGitHubAppAuthToken(appConnection);
let url: string | null = `https://api.${credentials.host || "github.com"}${path}`;
let results: T[] = [];
while (url) {
// eslint-disable-next-line no-await-in-loop
const response: AxiosResponse<R> = await requestWithGitHubGateway<R>(appConnection, gatewayService, {
url,
method: "GET",
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28"
}
});
const items = dataMapper ? dataMapper(response.data) : (response.data as unknown as T[]);
results = results.concat(items);
const linkHeader = response.headers.link as string | undefined;
const nextLink =
typeof linkHeader === "string" ? linkHeader.split(",").find((s) => s.includes('rel="next"')) : undefined;
if (nextLink) {
url = new RE2(/<(.+)>/).exec(nextLink)?.[1] || null;
} else {
url = null;
}
}
return results;
}; };
type GitHubOrganization = { type GitHubOrganization = {
login: string; login: string;
id: number; id: number;
type: string;
}; };
type GitHubRepository = { type GitHubRepository = {
id: number; id: number;
name: string; name: string;
owner: GitHubOrganization; owner: GitHubOrganization;
permissions?: {
admin: boolean;
maintain: boolean;
push: boolean;
triage: boolean;
pull: boolean;
};
};
type GitHubEnvironment = {
id: number;
name: string;
}; };
export const getGitHubRepositories = async ( export const getGitHubRepositories = async (
appConnection: TGitHubConnection, appConnection: TGitHubConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId"> gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => { ) => {
return executeWithGitHubGateway(appConnection, gatewayService, async (client) => { if (appConnection.method === GitHubConnectionMethod.App) {
let repositories: GitHubRepository[]; return makePaginatedGitHubRequest<GitHubRepository, { repositories: GitHubRepository[] }>(
appConnection,
gatewayService,
"/installation/repositories",
(data) => data.repositories
);
}
switch (appConnection.method) { const repos = await makePaginatedGitHubRequest<GitHubRepository>(appConnection, gatewayService, "/user/repos");
case GitHubConnectionMethod.App: return repos.filter((repo) => repo.permissions?.admin);
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 ( export const getGitHubOrganizations = async (
appConnection: TGitHubConnection, appConnection: TGitHubConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId"> gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => { ) => {
return executeWithGitHubGateway(appConnection, gatewayService, async (client) => { if (appConnection.method === GitHubConnectionMethod.App) {
let organizations: GitHubOrganization[]; const installationRepositories = await makePaginatedGitHubRequest<
GitHubRepository,
{ repositories: GitHubRepository[] }
>(appConnection, gatewayService, "/installation/repositories", (data) => data.repositories);
switch (appConnection.method) { const organizationMap: Record<string, GitHubOrganization> = {};
case GitHubConnectionMethod.App: { installationRepositories.forEach((repo) => {
const installationRepositories = await client.paginate("GET /installation/repositories"); if (repo.owner.type === "Organization") {
organizationMap[repo.owner.id] = repo.owner;
const organizationMap: Record<string, GitHubOrganization> = {};
installationRepositories.forEach((repo) => {
if (repo.owner.type === "Organization") {
organizationMap[repo.owner.id] = repo.owner;
}
});
organizations = Object.values(organizationMap);
break;
} }
case GitHubConnectionMethod.OAuth: });
default:
organizations = await client.paginate("GET /user/orgs");
break;
}
return organizations; return Object.values(organizationMap);
}); }
return makePaginatedGitHubRequest<GitHubOrganization>(appConnection, gatewayService, "/user/orgs");
}; };
export const getGitHubEnvironments = async ( export const getGitHubEnvironments = async (
@@ -282,23 +227,18 @@ export const getGitHubEnvironments = async (
owner: string, owner: string,
repo: string repo: string
) => { ) => {
return executeWithGitHubGateway(appConnection, gatewayService, async (client) => { try {
try { return await makePaginatedGitHubRequest<GitHubEnvironment, { environments: GitHubEnvironment[] }>(
const environments = await client.paginate("GET /repos/{owner}/{repo}/environments", { appConnection,
owner, gatewayService,
repo `/repos/${owner}/${repo}/environments`,
}); (data) => data.environments
);
return environments; } catch (error) {
} catch (e) { const axiosError = error as AxiosError;
// repo doesn't have envs if (axiosError.response?.status === 404) return [];
if ((e as { status: number }).status === 404) { throw error;
return []; }
}
throw e;
}
});
}; };
export type GithubTokenRespData = { export type GithubTokenRespData = {
@@ -352,7 +292,6 @@ export const validateGitHubConnectionCredentials = async (
let tokenResp: AxiosResponse<GithubTokenRespData>; let tokenResp: AxiosResponse<GithubTokenRespData>;
const host = credentials.host || "github.com"; const host = credentials.host || "github.com";
const apiHost = credentials.host ? `api.${credentials.host}` : "api.github.com";
try { try {
tokenResp = await requestWithGitHubGateway<GithubTokenRespData>(config, gatewayService, { tokenResp = await requestWithGitHubGateway<GithubTokenRespData>(config, gatewayService, {
@@ -406,7 +345,7 @@ export const validateGitHubConnectionCredentials = async (
}; };
}[]; }[];
}>(config, gatewayService, { }>(config, gatewayService, {
url: IntegrationUrls.GITHUB_USER_INSTALLATIONS.replace("api.github.com", apiHost), url: IntegrationUrls.GITHUB_USER_INSTALLATIONS.replace("api.github.com", `api.${host}`),
headers: { headers: {
Accept: "application/json", Accept: "application/json",
Authorization: `Bearer ${tokenResp.data.access_token}`, Authorization: `Bearer ${tokenResp.data.access_token}`,
@@ -1,8 +1,12 @@
import { Octokit } from "@octokit/rest";
import sodium from "libsodium-wrappers"; import sodium from "libsodium-wrappers";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { executeWithGitHubGateway } from "@app/services/app-connection/github"; import {
getGitHubAppAuthToken,
GitHubConnectionMethod,
makePaginatedGitHubRequest,
requestWithGitHubGateway
} 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";
@@ -13,151 +17,155 @@ import { TGitHubPublicKey, TGitHubSecret, TGitHubSecretPayload, TGitHubSyncWithC
// TODO: rate limit handling // TODO: rate limit handling
const getEncryptedSecrets = async (client: Octokit, secretSync: TGitHubSyncWithCredentials) => { const getEncryptedSecrets = async (
let encryptedSecrets: TGitHubSecret[]; secretSync: TGitHubSyncWithCredentials,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
const { destinationConfig } = secretSync; ) => {
const { destinationConfig, connection } = secretSync;
let path: string;
switch (destinationConfig.scope) { switch (destinationConfig.scope) {
case GitHubSyncScope.Organization: { case GitHubSyncScope.Organization: {
encryptedSecrets = await client.paginate("GET /orgs/{org}/actions/secrets", { path = `/orgs/${destinationConfig.org}/actions/secrets`;
org: destinationConfig.org
});
break; break;
} }
case GitHubSyncScope.Repository: { case GitHubSyncScope.Repository: {
encryptedSecrets = await client.paginate("GET /repos/{owner}/{repo}/actions/secrets", { path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/actions/secrets`;
owner: destinationConfig.owner,
repo: destinationConfig.repo
});
break; break;
} }
case GitHubSyncScope.RepositoryEnvironment: case GitHubSyncScope.RepositoryEnvironment:
default: { default: {
encryptedSecrets = await client.paginate("GET /repos/{owner}/{repo}/environments/{environment_name}/secrets", { path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/environments/${destinationConfig.env}/secrets`;
owner: destinationConfig.owner,
repo: destinationConfig.repo,
environment_name: destinationConfig.env
});
break; break;
} }
} }
return encryptedSecrets; return makePaginatedGitHubRequest<TGitHubSecret, { secrets: TGitHubSecret[] }>(
connection,
gatewayService,
path,
(data) => data.secrets
);
}; };
const getPublicKey = async (client: Octokit, secretSync: TGitHubSyncWithCredentials) => { const getPublicKey = async (
let publicKey: TGitHubPublicKey; secretSync: TGitHubSyncWithCredentials,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
const { destinationConfig } = secretSync; token: string
) => {
const { destinationConfig, connection } = secretSync;
let path: string;
switch (destinationConfig.scope) { switch (destinationConfig.scope) {
case GitHubSyncScope.Organization: { case GitHubSyncScope.Organization: {
publicKey = ( path = `/orgs/${destinationConfig.org}/actions/secrets/public-key`;
await client.request("GET /orgs/{org}/actions/secrets/public-key", {
org: destinationConfig.org
})
).data;
break; break;
} }
case GitHubSyncScope.Repository: { case GitHubSyncScope.Repository: {
publicKey = ( path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/actions/secrets/public-key`;
await client.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", {
owner: destinationConfig.owner,
repo: destinationConfig.repo
})
).data;
break; break;
} }
case GitHubSyncScope.RepositoryEnvironment: case GitHubSyncScope.RepositoryEnvironment:
default: { default: {
publicKey = ( path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/environments/${destinationConfig.env}/secrets/public-key`;
await client.request("GET /repos/{owner}/{repo}/environments/{environment_name}/secrets/public-key", {
owner: destinationConfig.owner,
repo: destinationConfig.repo,
environment_name: destinationConfig.env
})
).data;
break; break;
} }
} }
return publicKey; const response = await requestWithGitHubGateway<TGitHubPublicKey>(connection, gatewayService, {
url: `https://api.${connection.credentials.host || "github.com"}${path}`,
method: "GET",
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28"
}
});
return response.data;
}; };
const deleteSecret = async ( const deleteSecret = async (
client: Octokit,
secretSync: TGitHubSyncWithCredentials, secretSync: TGitHubSyncWithCredentials,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
token: string,
encryptedSecret: TGitHubSecret encryptedSecret: TGitHubSecret
) => { ) => {
const { destinationConfig } = secretSync; const { destinationConfig, connection } = secretSync;
let path: string;
switch (destinationConfig.scope) { switch (destinationConfig.scope) {
case GitHubSyncScope.Organization: { case GitHubSyncScope.Organization: {
await client.request(`DELETE /orgs/{org}/actions/secrets/{secret_name}`, { path = `/orgs/${destinationConfig.org}/actions/secrets/${encryptedSecret.name}`;
org: destinationConfig.org,
secret_name: encryptedSecret.name
});
break; break;
} }
case GitHubSyncScope.Repository: { case GitHubSyncScope.Repository: {
await client.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/actions/secrets/${encryptedSecret.name}`;
owner: destinationConfig.owner,
repo: destinationConfig.repo,
secret_name: encryptedSecret.name
});
break; break;
} }
case GitHubSyncScope.RepositoryEnvironment: case GitHubSyncScope.RepositoryEnvironment:
default: { default: {
await client.request("DELETE /repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}", { path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/environments/${destinationConfig.env}/secrets/${encryptedSecret.name}`;
owner: destinationConfig.owner,
repo: destinationConfig.repo,
environment_name: destinationConfig.env,
secret_name: encryptedSecret.name
});
break; break;
} }
} }
await requestWithGitHubGateway(connection, gatewayService, {
url: `https://api.${connection.credentials.host || "github.com"}${path}`,
method: "DELETE",
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28"
}
});
}; };
const putSecret = async (client: Octokit, secretSync: TGitHubSyncWithCredentials, payload: TGitHubSecretPayload) => { const putSecret = async (
const { destinationConfig } = secretSync; secretSync: TGitHubSyncWithCredentials,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
token: string,
payload: TGitHubSecretPayload
) => {
const { destinationConfig, connection } = secretSync;
let path: string;
let body: Record<string, string | number[]> = payload;
switch (destinationConfig.scope) { switch (destinationConfig.scope) {
case GitHubSyncScope.Organization: { case GitHubSyncScope.Organization: {
const { visibility, selectedRepositoryIds } = destinationConfig; const { visibility, selectedRepositoryIds } = destinationConfig;
path = `/orgs/${destinationConfig.org}/actions/secrets/${payload.secret_name}`;
await client.request(`PUT /orgs/{org}/actions/secrets/{secret_name}`, { body = {
org: destinationConfig.org,
...payload, ...payload,
visibility, visibility,
...(visibility === GitHubSyncVisibility.Selected && { ...(visibility === GitHubSyncVisibility.Selected && {
selected_repository_ids: selectedRepositoryIds selected_repository_ids: selectedRepositoryIds
}) })
}); };
break; break;
} }
case GitHubSyncScope.Repository: { case GitHubSyncScope.Repository: {
await client.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", { path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/actions/secrets/${payload.secret_name}`;
owner: destinationConfig.owner,
repo: destinationConfig.repo,
...payload
});
break; break;
} }
case GitHubSyncScope.RepositoryEnvironment: case GitHubSyncScope.RepositoryEnvironment:
default: { default: {
await client.request("PUT /repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}", { path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/environments/${destinationConfig.env}/secrets/${payload.secret_name}`;
owner: destinationConfig.owner,
repo: destinationConfig.repo,
environment_name: destinationConfig.env,
...payload
});
break; break;
} }
} }
await requestWithGitHubGateway(connection, gatewayService, {
url: `https://api.${connection.credentials.host || "github.com"}${path}`,
method: "PUT",
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28"
},
data: body
});
}; };
export const GithubSyncFns = { export const GithubSyncFns = {
@@ -192,50 +200,52 @@ export const GithubSyncFns = {
); );
} }
await executeWithGitHubGateway(secretSync.connection, gatewayService, async (client) => { const { connection } = secretSync;
const encryptedSecrets = await getEncryptedSecrets(client, secretSync); const token =
connection.method === GitHubConnectionMethod.OAuth
? connection.credentials.accessToken
: await getGitHubAppAuthToken(connection);
const publicKey = await getPublicKey(client, secretSync); const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService);
const publicKey = await getPublicKey(secretSync, gatewayService, token);
await sodium.ready.then(async () => { await sodium.ready;
for await (const key of Object.keys(secretMap)) { for await (const key of Object.keys(secretMap)) {
// convert secret & base64 key to Uint8Array. // convert secret & base64 key to Uint8Array.
const binaryKey = sodium.from_base64(publicKey.key, sodium.base64_variants.ORIGINAL); const binaryKey = sodium.from_base64(publicKey.key, sodium.base64_variants.ORIGINAL);
const binarySecretValue = sodium.from_string(secretMap[key].value); const binarySecretValue = sodium.from_string(secretMap[key].value);
// encrypt secret using libsodium // encrypt secret using libsodium
const encryptedBytes = sodium.crypto_box_seal(binarySecretValue, binaryKey); const encryptedBytes = sodium.crypto_box_seal(binarySecretValue, binaryKey);
// convert encrypted Uint8Array to base64 // convert encrypted Uint8Array to base64
const encryptedSecretValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL); const encryptedSecretValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL);
try { try {
await putSecret(client, secretSync, { await putSecret(secretSync, gatewayService, token, {
secret_name: key, secret_name: key,
encrypted_value: encryptedSecretValue, encrypted_value: encryptedSecretValue,
key_id: publicKey.key_id key_id: publicKey.key_id
}); });
} catch (error) { } catch (error) {
throw new SecretSyncError({ throw new SecretSyncError({
error, error,
secretKey: key 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);
}
} }
}); }
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(secretSync, gatewayService, token, 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.`);
@@ -245,14 +255,18 @@ export const GithubSyncFns = {
secretMap: TSecretMap, secretMap: TSecretMap,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId"> gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => { ) => {
await executeWithGitHubGateway(secretSync.connection, gatewayService, async (client) => { const { connection } = secretSync;
const encryptedSecrets = await getEncryptedSecrets(client, secretSync); const token =
connection.method === GitHubConnectionMethod.OAuth
? connection.credentials.accessToken
: await getGitHubAppAuthToken(connection);
for await (const encryptedSecret of encryptedSecrets) { const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService);
if (encryptedSecret.name in secretMap) {
await deleteSecret(client, secretSync, encryptedSecret); for await (const encryptedSecret of encryptedSecrets) {
} if (encryptedSecret.name in secretMap) {
await deleteSecret(secretSync, gatewayService, token, encryptedSecret);
} }
}); }
} }
}; };