mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 06:26:27 +00:00
Swap away from using octokit due to gateway compatibility issues
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user