feat: gcp secret sync

This commit is contained in:
Sheen Capadngan
2025-01-24 22:33:56 +08:00
parent c3970d1ea2
commit 58f51411c0
20 changed files with 386 additions and 14 deletions

View File

@@ -201,6 +201,9 @@ const envSchema = z
INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()),
INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional()),
// gcp app
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL: zpStr(z.string().optional()),
/* CORS ----------------------------------------------------------------------------- */
CORS_ALLOWED_ORIGINS: zpStr(

View File

@@ -4,18 +4,21 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
import { AuthMode } from "@app/services/auth/auth-type";
// can't use discriminated due to multiple schemas for certain apps
const SanitizedAppConnectionSchema = z.union([
...SanitizedAwsConnectionSchema.options,
...SanitizedGitHubConnectionSchema.options
...SanitizedGitHubConnectionSchema.options,
...SanitizedGcpConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
AwsConnectionListItemSchema,
GitHubConnectionListItemSchema
GitHubConnectionListItemSchema,
GcpConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@@ -0,0 +1,17 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateGcpConnectionSchema,
SanitizedGcpConnectionSchema,
UpdateGcpConnectionSchema
} from "@app/services/app-connection/gcp";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerGcpConnectionRouter = async (server: FastifyZodProvider) =>
registerAppConnectionEndpoints({
app: AppConnection.GCP,
server,
sanitizedResponseSchema: SanitizedGcpConnectionSchema,
createSchema: CreateGcpConnectionSchema,
updateSchema: UpdateGcpConnectionSchema
});

View File

@@ -1,6 +1,7 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { registerAwsConnectionRouter } from "./aws-connection-router";
import { registerGcpConnectionRouter } from "./gcp-connection-router";
import { registerGitHubConnectionRouter } from "./github-connection-router";
export * from "./app-connection-router";
@@ -8,5 +9,6 @@ export * from "./app-connection-router";
export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> =
{
[AppConnection.AWS]: registerAwsConnectionRouter,
[AppConnection.GitHub]: registerGitHubConnectionRouter
[AppConnection.GitHub]: registerGitHubConnectionRouter,
[AppConnection.GCP]: registerGcpConnectionRouter
};

View File

@@ -1,6 +1,7 @@
export enum AppConnection {
GitHub = "github",
AWS = "aws"
AWS = "aws",
GCP = "gcp"
}
export enum AWSRegion {

View File

@@ -7,6 +7,11 @@ import {
getAwsAppConnectionListItem,
validateAwsConnectionCredentials
} from "@app/services/app-connection/aws";
import {
GcpConnectionMethod,
getGcpAppConnectionListItem,
validateGcpConnectionCredentials
} from "@app/services/app-connection/gcp";
import {
getGitHubConnectionListItem,
GitHubConnectionMethod,
@@ -15,7 +20,9 @@ import {
import { KmsDataKey } from "@app/services/kms/kms-types";
export const listAppConnectionOptions = () => {
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem()].sort((a, b) => a.name.localeCompare(b.name));
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem(), getGcpAppConnectionListItem()].sort((a, b) =>
a.name.localeCompare(b.name)
);
};
export const encryptAppConnectionCredentials = async ({
@@ -69,6 +76,8 @@ export const validateAppConnectionCredentials = async (
return validateAwsConnectionCredentials(appConnection);
case AppConnection.GitHub:
return validateGitHubConnectionCredentials(appConnection);
case AppConnection.GCP:
return validateGcpConnectionCredentials(appConnection);
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection ${app}`);
@@ -85,6 +94,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
return "Access Key";
case AwsConnectionMethod.AssumeRole:
return "Assume Role";
case GcpConnectionMethod.ServiceAccountImpersonation:
return "Service Account Impersonation";
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`);

View File

@@ -26,6 +26,7 @@ import { githubConnectionService } from "@app/services/app-connection/github/git
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TAppConnectionDALFactory } from "./app-connection-dal";
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
export type TAppConnectionServiceFactoryDep = {
appConnectionDAL: TAppConnectionDALFactory;
@@ -37,7 +38,8 @@ export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServic
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema,
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({

View File

@@ -11,9 +11,11 @@ import {
TValidateGitHubConnectionCredentials
} from "@app/services/app-connection/github";
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection);
import { TGcpConnection, TGcpConnectionConfig, TGcpConnectionInput, TValidateGcpConnectionCredentials } from "./gcp";
export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput);
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection | TGcpConnection);
export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput | TGcpConnectionInput);
export type TCreateAppConnectionDTO = Pick<
TAppConnectionInput,
@@ -24,8 +26,9 @@ export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "met
connectionId: string;
};
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig;
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig | TGcpConnectionConfig;
export type TValidateAppConnectionCredentials =
| TValidateAwsConnectionCredentials
| TValidateGitHubConnectionCredentials;
| TValidateGitHubConnectionCredentials
| TValidateGcpConnectionCredentials;

View File

@@ -0,0 +1,3 @@
export enum GcpConnectionMethod {
ServiceAccountImpersonation = "service-account-impersonation"
}

View File

@@ -0,0 +1,71 @@
import { gaxios, Impersonated, JWT } from "google-auth-library";
import { GetAccessTokenResponse } from "google-auth-library/build/src/auth/oauth2client";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { AppConnection } from "../app-connection-enums";
import { getAppConnectionMethodName } from "../app-connection-fns";
import { GcpConnectionMethod } from "./gcp-connection-enums";
import { TGcpConnectionConfig } from "./gcp-connection-types";
export const getGcpAppConnectionListItem = () => {
return {
name: "GCP" as const,
app: AppConnection.GCP as const,
methods: Object.values(GcpConnectionMethod) as [GcpConnectionMethod.ServiceAccountImpersonation]
};
};
export const validateGcpConnectionCredentials = async (appConnection: TGcpConnectionConfig) => {
const appCfg = getConfig();
if (!appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) {
throw new InternalServerError({
message: `Environment variables have not been configured for GCP ${getAppConnectionMethodName(
GcpConnectionMethod.ServiceAccountImpersonation
)}`
});
}
const credJson = JSON.parse(appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) as {
client_email: string;
private_key: string;
};
const sourceClient = new JWT({
email: credJson.client_email,
key: credJson.private_key,
scopes: ["https://www.googleapis.com/auth/cloud-platform"]
});
const impersonatedCredentials = new Impersonated({
sourceClient,
targetPrincipal: appConnection.credentials.serviceAccountEmail,
lifetime: 3600,
delegates: [],
targetScopes: ["https://www.googleapis.com/auth/cloud-platform"]
});
let tokenResponse: GetAccessTokenResponse | undefined;
try {
tokenResponse = await impersonatedCredentials.getAccessToken();
} catch (error) {
let message = "Unable to validate connection";
if (error instanceof gaxios.GaxiosError) {
message = error.message;
}
throw new BadRequestError({
message
});
}
if (!tokenResponse || !tokenResponse.token) {
throw new BadRequestError({
message: `Unable to validate connection`
});
}
return appConnection.credentials;
};

View File

@@ -0,0 +1,65 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { GcpConnectionMethod } from "./gcp-connection-enums";
export const GcpConnectionServiceAccountImpersonationCredentialsSchema = z.object({
serviceAccountEmail: z.string().trim().min(1, "Service account email required")
});
const BaseGcpConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GCP) });
export const GcpConnectionSchema = z.intersection(
BaseGcpConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(GcpConnectionMethod.ServiceAccountImpersonation),
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema
})
])
);
export const SanitizedGcpConnectionSchema = z.discriminatedUnion("method", [
BaseGcpConnectionSchema.extend({
method: z.literal(GcpConnectionMethod.ServiceAccountImpersonation),
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.pick({})
})
]);
export const ValidateGcpConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(GcpConnectionMethod.ServiceAccountImpersonation)
.describe(AppConnections?.CREATE(AppConnection.GCP).method),
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.GCP).credentials
)
})
]);
export const CreateGcpConnectionSchema = ValidateGcpConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.GCP)
);
export const UpdateGcpConnectionSchema = z
.object({
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.GCP).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GCP));
export const GcpConnectionListItemSchema = z.object({
name: z.literal("GCP"),
app: z.literal(AppConnection.GCP),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]),
methods: z.nativeEnum(GcpConnectionMethod).array()
});

View File

@@ -0,0 +1,22 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateGcpConnectionSchema,
GcpConnectionSchema,
ValidateGcpConnectionCredentialsSchema
} from "./gcp-connection-schemas";
export type TGcpConnection = z.infer<typeof GcpConnectionSchema>;
export type TGcpConnectionInput = z.infer<typeof CreateGcpConnectionSchema> & {
app: AppConnection.GCP;
};
export type TValidateGcpConnectionCredentials = typeof ValidateGcpConnectionCredentialsSchema;
export type TGcpConnectionConfig = DiscriminativePick<TGcpConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};

View File

@@ -0,0 +1,4 @@
export * from "./gcp-connection-enums";
export * from "./gcp-connection-fns";
export * from "./gcp-connection-schemas";
export * from "./gcp-connection-types";