feat: added support for app octokit

This commit is contained in:
Sheen Capadngan
2024-10-14 16:17:39 +08:00
parent 059c552307
commit b8f3814df0
10 changed files with 676 additions and 101 deletions
@@ -1,9 +1,12 @@
/* eslint-disable no-await-in-loop */
import { createAppAuth } from "@octokit/auth-app";
import { Octokit } from "@octokit/rest";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { NotFoundError } from "@app/lib/errors";
import { TIntegrationAuthMetadata } from "./integration-auth-schema";
import { Integrations, IntegrationUrls } from "./integration-list";
// akhilmhdh: check this part later. Copied from old base
@@ -230,7 +233,13 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
/**
* Return list of repositories for Github integration
*/
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
const getAppsGithub = async ({
accessToken,
authMetadata
}: {
accessToken: string;
authMetadata?: TIntegrationAuthMetadata;
}) => {
interface GitHubApp {
id: string;
name: string;
@@ -242,6 +251,29 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
};
}
if (authMetadata?.installationId) {
const appCfg = getConfig();
const octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: appCfg.GITHUB_APP_ID,
privateKey: appCfg.GITHUB_APP_PRIVATE_KEY,
installationId: authMetadata.installationId
}
});
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const repos = await octokit.paginate("GET /installation/repositories", {
per_page: 100
});
return repos.map((a) => ({
appId: String(a.id),
name: a.name,
owner: a.owner.login
}));
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const repos = (await new Octokit({
auth: accessToken
@@ -1061,11 +1093,13 @@ export const getApps = async ({
teamId,
azureDevOpsOrgName,
workspaceSlug,
authMetadata,
url
}: {
integration: string;
accessToken: string;
accessId?: string;
authMetadata: TIntegrationAuthMetadata;
teamId?: string | null;
azureDevOpsOrgName?: string | null;
workspaceSlug?: string;
@@ -1099,7 +1133,8 @@ export const getApps = async ({
case Integrations.GITHUB:
return getAppsGithub({
accessToken
accessToken,
authMetadata
});
case Integrations.GITLAB:
@@ -0,0 +1,7 @@
import { z } from "zod";
export const IntegrationAuthMetadataSchema = z.object({
installationId: z.string().optional()
});
export type TIntegrationAuthMetadata = z.infer<typeof IntegrationAuthMetadataSchema>;
@@ -1,10 +1,12 @@
import { ForbiddenError } from "@casl/ability";
import { createAppAuth } from "@octokit/auth-app";
import { Octokit } from "@octokit/rest";
import AWS from "aws-sdk";
import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -16,6 +18,7 @@ import { KmsDataKey } from "../kms/kms-types";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { getApps } from "./integration-app-list";
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
import { IntegrationAuthMetadataSchema } from "./integration-auth-schema";
import {
TBitbucketWorkspace,
TChecklyGroups,
@@ -345,6 +348,13 @@ export const integrationAuthServiceFactory = ({
) {
return { accessToken: "", accessId: "" };
}
if (
integrationAuth.integration === Integrations.GITHUB &&
IntegrationAuthMetadataSchema.parse(integrationAuth.metadata).installationId
) {
return { accessToken: "", accessId: "" };
}
if (shouldUseSecretV2Bridge) {
const { decryptor: secretManagerDecryptor, encryptor: secretManagerEncryptor } =
await kmsService.createCipherPairWithDataKey({
@@ -471,6 +481,7 @@ export const integrationAuthServiceFactory = ({
const { accessToken, accessId } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const apps = await getApps({
integration: integrationAuth.integration,
authMetadata: IntegrationAuthMetadataSchema.parse(integrationAuth.metadata),
accessToken,
accessId,
teamId,
@@ -586,6 +597,7 @@ export const integrationAuthServiceFactory = ({
};
const getGithubOrgs = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TIntegrationAuthGithubOrgsDTO) => {
const appCfg = getConfig();
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new NotFoundError({ message: "Failed to find integration" });
@@ -598,9 +610,44 @@ export const integrationAuthServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const octokit = new Octokit({
let octokit: Octokit;
const { installationId } = integrationAuth.metadata as { installationId: string };
if (installationId) {
octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: appCfg.GITHUB_APP_ID,
privateKey: appCfg.GITHUB_APP_PRIVATE_KEY,
installationId
}
});
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const repos = await octokit.paginate("GET /installation/repositories", {
per_page: 100
});
const orgSet: Set<string> = new Set();
return repos
.filter((repo) => repo.owner.type === "Organization")
.map((repo) => ({
name: repo.owner.login,
orgId: String(repo.owner.id)
}))
.filter((org) => {
const isOrgProcessed = !orgSet.has(org.orgId);
if (!isOrgProcessed) {
orgSet.add(org.orgId);
}
return isOrgProcessed;
});
}
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
octokit = new Octokit({
auth: accessToken
});
@@ -609,7 +656,9 @@ export const integrationAuthServiceFactory = ({
"X-GitHub-Api-Version": "2022-11-28"
}
});
if (!data) return [];
if (!data) {
return [];
}
return data.map(({ login: name, id: orgId }) => ({ name, orgId: String(orgId) }));
};
@@ -637,9 +686,24 @@ export const integrationAuthServiceFactory = ({
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const octokit = new Octokit({
auth: accessToken
});
let octokit: Octokit;
const appCfg = getConfig();
const authMetadata = IntegrationAuthMetadataSchema.parse(integrationAuth.metadata);
if (authMetadata.installationId) {
octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: appCfg.GITHUB_APP_ID,
privateKey: appCfg.GITHUB_APP_PRIVATE_KEY,
installationId: authMetadata.installationId
}
});
} else {
octokit = new Octokit({
auth: accessToken
});
}
const {
data: { environments }
@@ -1,7 +1,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { createAppAuth } from "@octokit/auth-app";
import { retry } from "@octokit/plugin-retry";
import { Octokit } from "@octokit/rest";
import { TIntegrationAuths, TIntegrations } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
@@ -15,6 +18,7 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
import { TIntegrationAuthServiceFactory } from "./integration-auth-service";
import { Integrations } from "./integration-list";
@@ -154,10 +158,12 @@ const getIntegrationSecretsV1 = async (
export const deleteGithubSecrets = async ({
integration,
authMetadata,
secrets,
accessToken
}: {
integration: Omit<TIntegrations, "envId">;
authMetadata: TIntegrationAuthMetadata;
secrets: Record<string, boolean>;
accessToken: string;
}) => {
@@ -170,9 +176,23 @@ export const deleteGithubSecrets = async ({
}
const OctokitWithRetry = Octokit.plugin(retry);
const octokit = new OctokitWithRetry({
auth: accessToken
});
let octokit: Octokit;
const appCfg = getConfig();
if (authMetadata.installationId) {
octokit = new OctokitWithRetry({
authStrategy: createAppAuth,
auth: {
appId: appCfg.GITHUB_APP_ID,
privateKey: appCfg.GITHUB_APP_PRIVATE_KEY,
installationId: authMetadata.installationId
}
});
} else {
octokit = new OctokitWithRetry({
auth: accessToken
});
}
enum GithubScope {
Repo = "github-repo",
@@ -192,6 +212,7 @@ export const deleteGithubSecrets = async ({
break;
}
case GithubScope.Env: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
encryptedGithubSecrets = (
await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", {
repository_id: Number(integration.appId),
@@ -346,6 +367,7 @@ export const deleteIntegrationSecrets = async ({
case Integrations.GITHUB: {
await deleteGithubSecrets({
integration,
authMetadata: IntegrationAuthMetadataSchema.parse(integrationAuth.metadata),
accessToken,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets
});
@@ -18,6 +18,7 @@ import {
UpdateSecretCommand
} from "@aws-sdk/client-secrets-manager";
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
import { createAppAuth } from "@octokit/auth-app";
import { Octokit } from "@octokit/rest";
import AWS, { AWSError } from "aws-sdk";
import { AxiosError } from "axios";
@@ -35,6 +36,7 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/
import { TIntegrationDALFactory } from "../integration/integration-dal";
import { IntegrationMetadataSchema } from "../integration/integration-schema";
import { IntegrationAuthMetadataSchema } from "./integration-auth-schema";
import { TIntegrationsWithEnvironment } from "./integration-auth-types";
import {
IntegrationInitialSyncBehavior,
@@ -1527,11 +1529,13 @@ const syncSecretsNetlify = async ({
*/
const syncSecretsGitHub = async ({
integration,
integrationAuth,
secrets,
accessToken,
appendices
}: {
integration: TIntegrations;
integrationAuth: TIntegrationAuths;
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
appendices?: { prefix: string; suffix: string };
@@ -1553,9 +1557,24 @@ const syncSecretsGitHub = async ({
selected_repositories_url?: string | undefined;
}
const octokit = new Octokit({
auth: accessToken
});
const authMetadata = IntegrationAuthMetadataSchema.parse(integrationAuth.metadata);
let octokit: Octokit;
const appCfg = getConfig();
if (authMetadata.installationId) {
octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: appCfg.GITHUB_APP_ID,
privateKey: appCfg.GITHUB_APP_PRIVATE_KEY,
installationId: authMetadata.installationId
}
});
} else {
octokit = new Octokit({
auth: accessToken
});
}
enum GithubScope {
Repo = "github-repo",
@@ -4054,6 +4073,7 @@ export const syncIntegrationSecrets = async ({
case Integrations.GITHUB:
await syncSecretsGitHub({
integration,
integrationAuth,
secrets,
accessToken,
appendices
@@ -258,27 +258,7 @@ export const integrationServiceFactory = ({
});
}
const deletedIntegration = await integrationDAL.transaction(async (tx) => {
// delete integration
const deletedIntegrationResult = await integrationDAL.deleteById(id, tx);
// check if there are other integrations that share the same integration auth
const integrations = await integrationDAL.find(
{
integrationAuthId: integration.integrationAuthId
},
tx
);
if (integrations.length === 0) {
// no other integration shares the same integration auth
// -> delete the integration auth
await integrationAuthDAL.deleteById(integration.integrationAuthId, tx);
}
return deletedIntegrationResult;
});
const deletedIntegration = await integrationDAL.deleteById(id);
return { ...integration, ...deletedIntegration };
};