diff --git a/.env.example b/.env.example index 1f121b77f..23a0b8be0 100644 --- a/.env.example +++ b/.env.example @@ -106,3 +106,6 @@ INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET= INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY= INF_APP_CONNECTION_GITHUB_APP_SLUG= INF_APP_CONNECTION_GITHUB_APP_ID= + +#gcp app +INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL= diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 762ca298d..627ccb053 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -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( diff --git a/backend/src/lib/crypto/encryption.ts b/backend/src/lib/crypto/encryption.ts index 258a6d285..f495681f1 100644 --- a/backend/src/lib/crypto/encryption.ts +++ b/backend/src/lib/crypto/encryption.ts @@ -116,7 +116,7 @@ export const decryptAsymmetric = ({ ciphertext, nonce, publicKey, privateKey }: export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64"); -export const generateHash = (value: string) => crypto.createHash("sha256").update(value).digest("hex"); +export const generateHash = (value: string | Buffer) => crypto.createHash("sha256").update(value).digest("hex"); export const generateAsymmetricKeyPair = () => { const pair = nacl.box.keyPair(); diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index d18639786..d9a5b0ee2 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -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) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/gcp-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/gcp-connection-router.ts new file mode 100644 index 000000000..f92d5e668 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/gcp-connection-router.ts @@ -0,0 +1,48 @@ +import z from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateGcpConnectionSchema, + SanitizedGcpConnectionSchema, + UpdateGcpConnectionSchema +} from "@app/services/app-connection/gcp"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerGcpConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.GCP, + server, + sanitizedResponseSchema: SanitizedGcpConnectionSchema, + createSchema: CreateGcpConnectionSchema, + updateSchema: UpdateGcpConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + server.route({ + method: "GET", + url: `/:connectionId/secret-manager-projects`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ id: z.string(), name: z.string() }).array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const projects = await server.services.appConnection.gcp.listSecretManagerProjects(connectionId, req.permission); + + return projects; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index 2570cb3ad..4551a0fbb 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -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 Promise> = { [AppConnection.AWS]: registerAwsConnectionRouter, - [AppConnection.GitHub]: registerGitHubConnectionRouter + [AppConnection.GitHub]: registerGitHubConnectionRouter, + [AppConnection.GCP]: registerGcpConnectionRouter }; diff --git a/backend/src/server/routes/v1/secret-sync-routers/gcp-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/gcp-sync-router.ts new file mode 100644 index 000000000..8e4266556 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/gcp-sync-router.ts @@ -0,0 +1,13 @@ +import { CreateGcpSyncSchema, GcpSyncSchema, UpdateGcpSyncSchema } from "@app/services/secret-sync/gcp"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerGcpSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.GCPSecretManager, + server, + responseSchema: GcpSyncSchema, + createSchema: CreateGcpSyncSchema, + updateSchema: UpdateGcpSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index ecc21b776..6b3d2f15f 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -1,11 +1,13 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router"; +import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; export * from "./secret-sync-router"; export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record Promise> = { [SecretSync.AWSParameterStore]: registerAwsParameterStoreSyncRouter, - [SecretSync.GitHub]: registerGitHubSyncRouter + [SecretSync.GitHub]: registerGitHubSyncRouter, + [SecretSync.GCPSecretManager]: registerGcpSyncRouter }; diff --git a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts index 5736767dd..2703f8e44 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts @@ -9,13 +9,19 @@ import { AwsParameterStoreSyncListItemSchema, AwsParameterStoreSyncSchema } from "@app/services/secret-sync/aws-parameter-store"; +import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp"; import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github"; -const SecretSyncSchema = z.discriminatedUnion("destination", [AwsParameterStoreSyncSchema, GitHubSyncSchema]); +const SecretSyncSchema = z.discriminatedUnion("destination", [ + AwsParameterStoreSyncSchema, + GitHubSyncSchema, + GcpSyncSchema +]); const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ AwsParameterStoreSyncListItemSchema, - GitHubSyncListItemSchema + GitHubSyncListItemSchema, + GcpSyncListItemSchema ]); export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index e96886e9f..61787b47c 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -1,6 +1,7 @@ export enum AppConnection { GitHub = "github", - AWS = "aws" + AWS = "aws", + GCP = "gcp" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 3f52b1285..d4c9f97ad 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -1,4 +1,5 @@ import { TAppConnections } from "@app/db/schemas/app-connections"; +import { generateHash } from "@app/lib/crypto/encryption"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service"; import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types"; @@ -7,6 +8,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 +21,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 +77,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 +95,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}`); @@ -101,6 +113,7 @@ export const decryptAppConnection = async ( encryptedCredentials: appConnection.encryptedCredentials, orgId: appConnection.orgId, kmsService - }) + }), + credentialsHash: generateHash(appConnection.encryptedCredentials) } as TAppConnection; }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index f473b1e38..abff5cf3b 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -2,5 +2,6 @@ import { AppConnection } from "./app-connection-enums"; export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.AWS]: "AWS", - [AppConnection.GitHub]: "GitHub" + [AppConnection.GitHub]: "GitHub", + [AppConnection.GCP]: "GCP" }; diff --git a/backend/src/services/app-connection/app-connection-schemas.ts b/backend/src/services/app-connection/app-connection-schemas.ts index ce5e877fd..ef3c16cf8 100644 --- a/backend/src/services/app-connection/app-connection-schemas.ts +++ b/backend/src/services/app-connection/app-connection-schemas.ts @@ -10,6 +10,8 @@ export const BaseAppConnectionSchema = AppConnectionsSchema.omit({ encryptedCredentials: true, app: true, method: true +}).extend({ + credentialsHash: z.string().optional() }); export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) => diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 91e7d9dc4..8e51caf30 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -2,6 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { generateHash } from "@app/lib/crypto/encryption"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { DiscriminativePick, OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; @@ -26,6 +27,8 @@ 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"; +import { gcpConnectionService } from "./gcp/gcp-connection-service"; export type TAppConnectionServiceFactoryDep = { appConnectionDAL: TAppConnectionDALFactory; @@ -37,7 +40,8 @@ export type TAppConnectionServiceFactory = ReturnType = { [AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema, - [AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema + [AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema, + [AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema }; export const appConnectionServiceFactory = ({ @@ -182,6 +186,7 @@ export const appConnectionServiceFactory = ({ return { ...connection, + credentialsHash: generateHash(connection.encryptedCredentials), credentials: validatedCredentials }; }); @@ -382,6 +387,7 @@ export const appConnectionServiceFactory = ({ deleteAppConnection, connectAppConnectionById, listAvailableAppConnectionsForUser, - github: githubConnectionService(connectAppConnectionById) + github: githubConnectionService(connectAppConnectionById), + gcp: gcpConnectionService(connectAppConnectionById) }; }; diff --git a/backend/src/services/app-connection/app-connection-types.ts b/backend/src/services/app-connection/app-connection-types.ts index e3983cf91..dfe2d1c64 100644 --- a/backend/src/services/app-connection/app-connection-types.ts +++ b/backend/src/services/app-connection/app-connection-types.ts @@ -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 { + return { + name: "GCP" as const, + app: AppConnection.GCP as const, + methods: Object.values(GcpConnectionMethod) as [GcpConnectionMethod.ServiceAccountImpersonation] + }; +}; + +export const getGcpConnectionAuthToken = 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 tokenResponse.token; +}; + +export const getGcpSecretManagerProjects = async (appConnection: TGcpConnection) => { + const accessToken = await getGcpConnectionAuthToken(appConnection); + + let gcpApps: GCPApp[] = []; + + const pageSize = 100; + let pageToken: string | undefined; + let hasMorePages = true; + + const projects: { + name: string; + id: string; + }[] = []; + + while (hasMorePages) { + const params = new URLSearchParams({ + pageSize: String(pageSize), + ...(pageToken ? { pageToken } : {}) + }); + + // eslint-disable-next-line no-await-in-loop + const { data } = await request.get(`${IntegrationUrls.GCP_API_URL}/v1/projects`, { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + }); + + gcpApps = gcpApps.concat(data.projects); + + if (!data.nextPageToken) { + hasMorePages = false; + } + + pageToken = data.nextPageToken; + } + + // eslint-disable-next-line + for await (const gcpApp of gcpApps) { + try { + const res = ( + await request.get( + `${IntegrationUrls.GCP_SERVICE_USAGE_URL}/v1/projects/${gcpApp.projectId}/services/${IntegrationUrls.GCP_SECRET_MANAGER_SERVICE_NAME}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ) + ).data; + + if (res.state === "ENABLED") { + projects.push({ + name: gcpApp.name, + id: gcpApp.projectId + }); + } + } catch { + // eslint-disable-next-line + continue; + } + } + + return projects; +}; + +export const validateGcpConnectionCredentials = async (appConnection: TGcpConnectionConfig) => { + // Check if provided service account email suffix matches organization ID. + // We do this to mitigate confused deputy attacks in multi-tenant instances + if (appConnection.credentials.serviceAccountEmail) { + const expectedAccountIdSuffix = appConnection.orgId.split("-").slice(0, 2).join("-"); + const serviceAccountId = appConnection.credentials.serviceAccountEmail.split("@")[0]; + if (!serviceAccountId.endsWith(expectedAccountIdSuffix)) { + throw new BadRequestError({ + message: `GCP service account ID (the part of the email before '@') must have a suffix of "${expectedAccountIdSuffix}"` + }); + } + } + + await getGcpConnectionAuthToken(appConnection); + + return appConnection.credentials; +}; diff --git a/backend/src/services/app-connection/gcp/gcp-connection-schemas.ts b/backend/src/services/app-connection/gcp/gcp-connection-schemas.ts new file mode 100644 index 000000000..3c313f205 --- /dev/null +++ b/backend/src/services/app-connection/gcp/gcp-connection-schemas.ts @@ -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().email().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() +}); diff --git a/backend/src/services/app-connection/gcp/gcp-connection-service.ts b/backend/src/services/app-connection/gcp/gcp-connection-service.ts new file mode 100644 index 000000000..96b795a8f --- /dev/null +++ b/backend/src/services/app-connection/gcp/gcp-connection-service.ts @@ -0,0 +1,29 @@ +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { getGcpSecretManagerProjects } from "./gcp-connection-fns"; +import { TGcpConnection } from "./gcp-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const gcpConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listSecretManagerProjects = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.GCP, connectionId, actor); + + try { + const projects = await getGcpSecretManagerProjects(appConnection); + + return projects; + } catch (error) { + return []; + } + }; + + return { + listSecretManagerProjects + }; +}; diff --git a/backend/src/services/app-connection/gcp/gcp-connection-types.ts b/backend/src/services/app-connection/gcp/gcp-connection-types.ts new file mode 100644 index 000000000..097bed1ef --- /dev/null +++ b/backend/src/services/app-connection/gcp/gcp-connection-types.ts @@ -0,0 +1,45 @@ +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; + +export type TGcpConnectionInput = z.infer & { + app: AppConnection.GCP; +}; + +export type TValidateGcpConnectionCredentials = typeof ValidateGcpConnectionCredentialsSchema; + +export type TGcpConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type GCPApp = { + projectNumber: string; + projectId: string; + lifecycleState: "ACTIVE" | "LIFECYCLE_STATE_UNSPECIFIED" | "DELETE_REQUESTED" | "DELETE_IN_PROGRESS"; + name: string; + createTime: string; + parent: { + type: "organization" | "folder" | "project"; + id: string; + }; +}; + +export type GCPGetProjectsRes = { + projects: GCPApp[]; + nextPageToken?: string; +}; + +export type GCPGetServiceRes = { + name: string; + parent: string; + state: "ENABLED" | "DISABLED" | "STATE_UNSPECIFIED"; +}; diff --git a/backend/src/services/app-connection/gcp/index.ts b/backend/src/services/app-connection/gcp/index.ts new file mode 100644 index 000000000..60ebf13e7 --- /dev/null +++ b/backend/src/services/app-connection/gcp/index.ts @@ -0,0 +1,4 @@ +export * from "./gcp-connection-enums"; +export * from "./gcp-connection-fns"; +export * from "./gcp-connection-schemas"; +export * from "./gcp-connection-types"; diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-constants.ts b/backend/src/services/secret-sync/gcp/gcp-sync-constants.ts new file mode 100644 index 000000000..39ae0a9a4 --- /dev/null +++ b/backend/src/services/secret-sync/gcp/gcp-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const GCP_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "GCP Secret Manager", + destination: SecretSync.GCPSecretManager, + connection: AppConnection.GCP, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-enums.ts b/backend/src/services/secret-sync/gcp/gcp-sync-enums.ts new file mode 100644 index 000000000..348d2bfa5 --- /dev/null +++ b/backend/src/services/secret-sync/gcp/gcp-sync-enums.ts @@ -0,0 +1,3 @@ +export enum GcpSyncScope { + Global = "global" +} diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts new file mode 100644 index 000000000..f0eb3ce88 --- /dev/null +++ b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts @@ -0,0 +1,218 @@ +import { AxiosError } from "axios"; + +import { request } from "@app/lib/config/request"; +import { logger } from "@app/lib/logger"; +import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { SecretSyncError } from "../secret-sync-errors"; +import { TSecretMap } from "../secret-sync-types"; +import { + GCPLatestSecretVersionAccess, + GCPSecret, + GCPSMListSecretsRes, + TGcpSyncWithCredentials +} from "./gcp-sync-types"; + +const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => { + const { destinationConfig } = secretSync; + + let gcpSecrets: GCPSecret[] = []; + + const pageSize = 100; + let pageToken: string | undefined; + let hasMorePages = true; + + while (hasMorePages) { + const params = new URLSearchParams({ + pageSize: String(pageSize), + ...(pageToken ? { pageToken } : {}) + }); + + // eslint-disable-next-line no-await-in-loop + const { data: secretsRes } = await request.get( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${secretSync.destinationConfig.projectId}/secrets`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + if (secretsRes.secrets) { + gcpSecrets = gcpSecrets.concat(secretsRes.secrets); + } + + if (!secretsRes.nextPageToken) { + hasMorePages = false; + } + + pageToken = secretsRes.nextPageToken; + } + + const res: { [key: string]: string } = {}; + + for await (const gcpSecret of gcpSecrets) { + const arr = gcpSecret.name.split("/"); + const key = arr[arr.length - 1]; + + try { + const { data: secretLatest } = await request.get( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}/versions/latest:access`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8"); + } catch (error) { + // when a secret in GCP has no versions, we treat it as if it's a blank value + if (error instanceof AxiosError && error.response?.status === 404) { + res[key] = ""; + } else { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + } + + return res; +}; + +export const GcpSyncFns = { + syncSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => { + const { destinationConfig, connection } = secretSync; + const accessToken = await getGcpConnectionAuthToken(connection); + + const gcpSecrets = await getGcpSecrets(accessToken, secretSync); + + for await (const key of Object.keys(secretMap)) { + try { + // we do not process secrets with no value because GCP secret manager does not allow it + if (!secretMap[key].value) { + // eslint-disable-next-line no-continue + continue; + } + + if (!(key in gcpSecrets)) { + // case: create secret + await request.post( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets`, + { + replication: { + automatic: {} + } + }, + { + params: { + secretId: key + }, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + await request.post( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`, + { + payload: { + data: Buffer.from(secretMap[key].value).toString("base64") + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + for await (const key of Object.keys(gcpSecrets)) { + try { + if (!(key in secretMap) || !secretMap[key].value) { + // case: delete secret + await request.delete( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } else if (secretMap[key].value !== gcpSecrets[key]) { + if (!secretMap[key].value) { + logger.warn( + `syncSecretsGcpsecretManager: update secret value in gcp where [key=${key}] and [projectId=${destinationConfig.projectId}]` + ); + } + + await request.post( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`, + { + payload: { + data: Buffer.from(secretMap[key].value).toString("base64") + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + }, + + getSecrets: async (secretSync: TGcpSyncWithCredentials): Promise => { + const { connection } = secretSync; + const accessToken = await getGcpConnectionAuthToken(connection); + + const gcpSecrets = await getGcpSecrets(accessToken, secretSync); + return Object.fromEntries(Object.entries(gcpSecrets).map(([key, value]) => [key, { value: value ?? "" }])); + }, + + removeSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => { + const { destinationConfig, connection } = secretSync; + const accessToken = await getGcpConnectionAuthToken(connection); + + const gcpSecrets = await getGcpSecrets(accessToken, secretSync); + for await (const [key] of Object.entries(gcpSecrets)) { + if (key in secretMap) { + await request.delete( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } + } +}; diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts b/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts new file mode 100644 index 000000000..b0516d166 --- /dev/null +++ b/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts @@ -0,0 +1,45 @@ +import z from "zod"; + +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +import { SecretSync } from "../secret-sync-enums"; +import { GcpSyncScope } from "./gcp-sync-enums"; + +const GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +const GcpSyncDestinationConfigSchema = z.object({ + scope: z.literal(GcpSyncScope.Global), + projectId: z.string().min(1, "Project ID is required") +}); + +export const GcpSyncSchema = BaseSecretSyncSchema(SecretSync.GCPSecretManager, GcpSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.GCPSecretManager), + destinationConfig: GcpSyncDestinationConfigSchema +}); + +export const CreateGcpSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.GCPSecretManager, + GcpSyncOptionsConfig +).extend({ + destinationConfig: GcpSyncDestinationConfigSchema +}); + +export const UpdateGcpSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.GCPSecretManager, + GcpSyncOptionsConfig +).extend({ + destinationConfig: GcpSyncDestinationConfigSchema.optional() +}); + +export const GcpSyncListItemSchema = z.object({ + name: z.literal("GCP Secret Manager"), + connection: z.literal(AppConnection.GCP), + destination: z.literal(SecretSync.GCPSecretManager), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-types.ts b/backend/src/services/secret-sync/gcp/gcp-sync-types.ts new file mode 100644 index 000000000..1bf6820be --- /dev/null +++ b/backend/src/services/secret-sync/gcp/gcp-sync-types.ts @@ -0,0 +1,33 @@ +import z from "zod"; + +import { TGcpConnection } from "@app/services/app-connection/gcp"; + +import { CreateGcpSyncSchema, GcpSyncListItemSchema, GcpSyncSchema } from "./gcp-sync-schemas"; + +export type TGcpSyncListItem = z.infer; + +export type TGcpSync = z.infer; + +export type TGcpSyncInput = z.infer; + +export type TGcpSyncWithCredentials = TGcpSync & { + connection: TGcpConnection; +}; + +export type GCPSecret = { + name: string; + createTime: string; +}; + +export type GCPSMListSecretsRes = { + secrets?: GCPSecret[]; + totalSize?: number; + nextPageToken?: string; +}; + +export type GCPLatestSecretVersionAccess = { + name: string; + payload: { + data: string; + }; +}; diff --git a/backend/src/services/secret-sync/gcp/index.ts b/backend/src/services/secret-sync/gcp/index.ts new file mode 100644 index 000000000..c92ecc890 --- /dev/null +++ b/backend/src/services/secret-sync/gcp/index.ts @@ -0,0 +1,4 @@ +export * from "./gcp-sync-constants"; +export * from "./gcp-sync-enums"; +export * from "./gcp-sync-schemas"; +export * from "./gcp-sync-types"; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 406a3a161..58d68ebb1 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -1,6 +1,7 @@ export enum SecretSync { AWSParameterStore = "aws-parameter-store", - GitHub = "github" + GitHub = "github", + GCPSecretManager = "gcp-secret-manager" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index de39fef02..c02b8599d 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -13,9 +13,13 @@ import { TSecretSyncWithCredentials } from "@app/services/secret-sync/secret-sync-types"; +import { GCP_SYNC_LIST_OPTION } from "./gcp"; +import { GcpSyncFns } from "./gcp/gcp-sync-fns"; + const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION, - [SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION + [SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION, + [SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -71,6 +75,8 @@ export const SecretSyncFns = { return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.GitHub: return GithubSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.GCPSecretManager: + return GcpSyncFns.syncSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -86,6 +92,9 @@ export const SecretSyncFns = { case SecretSync.GitHub: secretMap = await GithubSyncFns.getSecrets(secretSync); break; + case SecretSync.GCPSecretManager: + secretMap = await GcpSyncFns.getSecrets(secretSync); + break; default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -103,6 +112,8 @@ export const SecretSyncFns = { return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.GitHub: return GithubSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.GCPSecretManager: + return GcpSyncFns.removeSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -115,7 +126,7 @@ export const parseSyncErrorMessage = (err: unknown): string => { if (err instanceof SecretSyncError) { return JSON.stringify({ secretKey: err.secretKey, - error: err.message ?? parseSyncErrorMessage(err.error) + error: err.message || parseSyncErrorMessage(err.error) }); } diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 67ba7b690..2df7d11e6 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -3,10 +3,12 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.AWSParameterStore]: "AWS Parameter Store", - [SecretSync.GitHub]: "GitHub" + [SecretSync.GitHub]: "GitHub", + [SecretSync.GCPSecretManager]: "GCP Secret Manager" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.AWSParameterStore]: AppConnection.AWS, - [SecretSync.GitHub]: AppConnection.GitHub + [SecretSync.GitHub]: AppConnection.GitHub, + [SecretSync.GCPSecretManager]: AppConnection.GCP }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index eade6671d..fb622ee9a 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -17,14 +17,18 @@ import { TAwsParameterStoreSyncListItem, TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store"; +import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; -export type TSecretSync = TAwsParameterStoreSync | TGitHubSync; +export type TSecretSync = TAwsParameterStoreSync | TGitHubSync | TGcpSync; -export type TSecretSyncWithCredentials = TAwsParameterStoreSyncWithCredentials | TGitHubSyncWithCredentials; +export type TSecretSyncWithCredentials = + | TAwsParameterStoreSyncWithCredentials + | TGitHubSyncWithCredentials + | TGcpSyncWithCredentials; -export type TSecretSyncInput = TAwsParameterStoreSyncInput | TGitHubSyncInput; +export type TSecretSyncInput = TAwsParameterStoreSyncInput | TGitHubSyncInput | TGcpSyncInput; -export type TSecretSyncListItem = TAwsParameterStoreSyncListItem | TGitHubSyncListItem; +export type TSecretSyncListItem = TAwsParameterStoreSyncListItem | TGitHubSyncListItem | TGcpSyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/docs/api-reference/endpoints/app-connections/gcp/available.mdx b/docs/api-reference/endpoints/app-connections/gcp/available.mdx new file mode 100644 index 000000000..edf5e84f6 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gcp/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/gcp/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/gcp/create.mdx b/docs/api-reference/endpoints/app-connections/gcp/create.mdx new file mode 100644 index 000000000..ebe7f1295 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gcp/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/gcp" +--- + + + Check out the configuration docs for [GCP + Connections](/integrations/app-connections/gcp) to learn how to obtain the + required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/gcp/delete.mdx b/docs/api-reference/endpoints/app-connections/gcp/delete.mdx new file mode 100644 index 000000000..7cfbc10ba --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gcp/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/gcp/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/gcp/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/gcp/get-by-id.mdx new file mode 100644 index 000000000..33a6009af --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gcp/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/gcp/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/gcp/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/gcp/get-by-name.mdx new file mode 100644 index 000000000..ae2bb42a4 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gcp/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/gcp/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/gcp/list.mdx b/docs/api-reference/endpoints/app-connections/gcp/list.mdx new file mode 100644 index 000000000..177af6ed9 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gcp/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/gcp" +--- diff --git a/docs/api-reference/endpoints/app-connections/gcp/update.mdx b/docs/api-reference/endpoints/app-connections/gcp/update.mdx new file mode 100644 index 000000000..0c711fd8e --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gcp/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/gcp/{connectionId}" +--- + + + Check out the configuration docs for [GCP + Connections](/integrations/app-connections/gcp) to learn how to obtain the + required credentials. + diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/create.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/create.mdx new file mode 100644 index 000000000..f877d1e1b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/gcp-secret-manager" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/delete.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/delete.mdx new file mode 100644 index 000000000..edb765728 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/gcp-secret-manager/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-id.mdx new file mode 100644 index 000000000..51ab1019e --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/gcp-secret-manager/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-name.mdx new file mode 100644 index 000000000..3a09872af --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/gcp-secret-manager/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets.mdx new file mode 100644 index 000000000..a975d83bf --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/list.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/list.mdx new file mode 100644 index 000000000..ca2e59be8 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/gcp-secret-manager" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets.mdx new file mode 100644 index 000000000..a2a67ae93 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/sync-secrets.mdx new file mode 100644 index 000000000..899e72d7c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/update.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/update.mdx new file mode 100644 index 000000000..fce03c90a --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/gcp-secret-manager/{syncId}" +--- diff --git a/docs/images/app-connections/gcp/create-gcp-impersonation-method.png b/docs/images/app-connections/gcp/create-gcp-impersonation-method.png new file mode 100644 index 000000000..141e24430 Binary files /dev/null and b/docs/images/app-connections/gcp/create-gcp-impersonation-method.png differ diff --git a/docs/images/app-connections/gcp/create-instance-service-account.png b/docs/images/app-connections/gcp/create-instance-service-account.png new file mode 100644 index 000000000..4a18e2ad2 Binary files /dev/null and b/docs/images/app-connections/gcp/create-instance-service-account.png differ diff --git a/docs/images/app-connections/gcp/create-service-account-credential.png b/docs/images/app-connections/gcp/create-service-account-credential.png new file mode 100644 index 000000000..acab54c31 Binary files /dev/null and b/docs/images/app-connections/gcp/create-service-account-credential.png differ diff --git a/docs/images/app-connections/gcp/create-service-account.png b/docs/images/app-connections/gcp/create-service-account.png new file mode 100644 index 000000000..c0d86b681 Binary files /dev/null and b/docs/images/app-connections/gcp/create-service-account.png differ diff --git a/docs/images/app-connections/gcp/gcp-app-impersonation-connection.png b/docs/images/app-connections/gcp/gcp-app-impersonation-connection.png new file mode 100644 index 000000000..8f67478b9 Binary files /dev/null and b/docs/images/app-connections/gcp/gcp-app-impersonation-connection.png differ diff --git a/docs/images/app-connections/gcp/select-gcp-connection.png b/docs/images/app-connections/gcp/select-gcp-connection.png new file mode 100644 index 000000000..3b28869f9 Binary files /dev/null and b/docs/images/app-connections/gcp/select-gcp-connection.png differ diff --git a/docs/images/app-connections/gcp/service-account-grant-access.png b/docs/images/app-connections/gcp/service-account-grant-access.png new file mode 100644 index 000000000..d0e0df52b Binary files /dev/null and b/docs/images/app-connections/gcp/service-account-grant-access.png differ diff --git a/docs/images/app-connections/gcp/service-account-overview.png b/docs/images/app-connections/gcp/service-account-overview.png new file mode 100644 index 000000000..4e94d31f1 Binary files /dev/null and b/docs/images/app-connections/gcp/service-account-overview.png differ diff --git a/docs/images/app-connections/gcp/service-account-permission-overview.png b/docs/images/app-connections/gcp/service-account-permission-overview.png new file mode 100644 index 000000000..789085cfe Binary files /dev/null and b/docs/images/app-connections/gcp/service-account-permission-overview.png differ diff --git a/docs/images/app-connections/gcp/service-account-secret-sync-permission.png b/docs/images/app-connections/gcp/service-account-secret-sync-permission.png new file mode 100644 index 000000000..f3bf0d28c Binary files /dev/null and b/docs/images/app-connections/gcp/service-account-secret-sync-permission.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/enable-resource-manager-api.png b/docs/images/secret-syncs/gcp-secret-manager/enable-resource-manager-api.png new file mode 100644 index 000000000..a3154e7ec Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/enable-resource-manager-api.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/enable-secret-manager-api.png b/docs/images/secret-syncs/gcp-secret-manager/enable-secret-manager-api.png new file mode 100644 index 000000000..50358699a Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/enable-secret-manager-api.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-created.png b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-created.png new file mode 100644 index 000000000..502365ff7 Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-created.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-destination.png b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-destination.png new file mode 100644 index 000000000..c50b6232a Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-destination.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-details.png b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-details.png new file mode 100644 index 000000000..dbb47371f Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-details.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-options.png b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-options.png new file mode 100644 index 000000000..d3eddfab9 Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-options.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-review.png b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-review.png new file mode 100644 index 000000000..4d710b3af Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-review.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-source.png b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-source.png new file mode 100644 index 000000000..9074d45ac Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-source.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/select-gcp-secret-manager-option.png b/docs/images/secret-syncs/gcp-secret-manager/select-gcp-secret-manager-option.png new file mode 100644 index 000000000..24e6ff95d Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/select-gcp-secret-manager-option.png differ diff --git a/docs/integrations/app-connections/gcp.mdx b/docs/integrations/app-connections/gcp.mdx new file mode 100644 index 000000000..3a23535ad --- /dev/null +++ b/docs/integrations/app-connections/gcp.mdx @@ -0,0 +1,88 @@ +--- +title: "GCP Connection" +description: "Learn how to configure a GCP Connection for Infisical." +--- + +Infisical supports [service account impersonation](https://cloud.google.com/iam/docs/service-account-impersonation) to connect with your GCP projects. + + + Using the GCP integration on a self-hosted instance of Infisical requires configuring a service account on GCP and + configuring your instance to use it. + + + + ![Service Account Page](/images/app-connections/gcp/service-account-overview.png) + + + Create a new service account that will be used to impersonate other GCP service accounts for your app connections. + ![Service Account Page](/images/app-connections/gcp/create-instance-service-account.png) + + + Download the JSON key file for your service account. This will be used to authenticate your instance with GCP. + ![Service Account Page](/images/app-connections/gcp/create-service-account-credential.png) + + + 1. Copy the entire contents of the downloaded JSON key file. + 2. Set it as a string value for the `INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL` environment variable. + 3. Restart your Infisical instance to apply the changes. + 4. You can now use GCP integration with service account impersonation. + + + + + +## Configure Service Account for Infisical + + + + ![Service Account Page](/images/app-connections/gcp/service-account-overview.png) + + + Create a new service account with an ID that follows this requirement: + + Your service account ID must end with the first two sections of your Infisical organization ID. + + Example: + - Infisical organization ID: `df92581a-0fe9-42b5-b526-0a1e88ec8085` + - Required service account ID suffix: `df92581a-0fe9` + + ![Create Service Account](/images/app-connections/gcp/create-service-account.png) + + + + + Add the required permissions for secret syncs: + ![Assign Service Account Permission](/images/app-connections/gcp/service-account-secret-sync-permission.png) + + + + + On the new service account, assign the `Service Account Token Creator` role to the Infisical instance's service account. This allows Infisical to impersonate the new service account. + ![Service Account Page](/images/app-connections/gcp/service-account-grant-access.png) + + + + +## Setup GCP Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** + page. ![App Connections + Tab](/images/app-connections/general/add-connection.png) + + + Select the **GCP Connection** option from the connection options modal. + ![Select GCP + Connection](/images/app-connections/gcp/select-gcp-connection.png) + + + Select the **Service Account Impersonation** method and click **Connect to + GCP**. ![Connect via GCP + impersonation](/images/app-connections/gcp/create-gcp-impersonation-method.png) + + + Your **GCP Connection** is now available for use. ![Impersonation GCP + Connection](/images/app-connections/gcp/gcp-app-impersonation-connection.png) + + diff --git a/docs/integrations/secret-syncs/gcp-secret-manager.mdx b/docs/integrations/secret-syncs/gcp-secret-manager.mdx new file mode 100644 index 000000000..ca20d8f79 --- /dev/null +++ b/docs/integrations/secret-syncs/gcp-secret-manager.mdx @@ -0,0 +1,141 @@ +--- +title: "GCP Secret Manager Sync" +description: "Learn how to configure a GCP Secret Manager Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [GCP Connection](/integrations/app-connections/gcp) with the required **Secret Sync** permissions + - Enable **Cloud Resource Manager API** and **Secret Manager API** on your GCP project + ![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-resource-manager-api.png) + ![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-secret-manager-api.png) + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **GCP Secret Manager** option. + ![Select GCP Secret Manager](/images/secret-syncs/gcp-secret-manager/select-gcp-secret-manager-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-destination.png) + + - **GCP Connection**: The GCP Connection to authenticate with. + - **Project**: The GCP project to sync with. + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Infisical if secrets conflict. + - **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in GCP secret manager if secrets conflict. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + + 6. Configure the **Details** of your GCP Secret Manager Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Secret Manager Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-review.png) + + 8. If enabled, your GCP Secret Manager Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-created.png) + + + + To create a **GCP Secret Manager Sync**, make an API request to the [Create GCP + Secret Manager Sync](/api-reference/endpoints/secret-syncs/gcp-secret-manager/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/gcp-secret-manager \ + --header 'Content-Type: application/json' \ + --data '{ + "destinationConfig": { + "scope": "global", + "projectId": "infisical-test-playground" + }, + "name": "my-gcp-sync", + "description": "this is an example secret sync", + "secretPath": "/", + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "isAutoSyncEnabled": true, + "connectionId": "eec83609-5eb4-4d8d-9f6e-ded016984f0d", + "environment": "dev", + "projectId": "09eda1f8-85a3-47a9-8a6f-e27f133b2a36" + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "aee02c4a-4a5f-488c-82dd-0b3164772871", + "name": "my-gcp-sync", + "description": "this is an example secret sync", + "isAutoSyncEnabled": true, + "version": 1, + "projectId": "09eda1f8-85a3-47a9-8a6f-e27f133b2a36", + "folderId": "1447389e-16fb-49ba-96fd-361b5a2522af", + "connectionId": "eec83609-5eb4-4d8d-9f6e-ded016984f0d", + "createdAt": "2025-01-27T12:28:59.408Z", + "updatedAt": "2025-01-27T12:28:59.408Z", + "syncStatus": "pending", + "lastSyncJobId": null, + "lastSyncMessage": null, + "lastSyncedAt": null, + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "connection": { + "app": "gcp", + "name": "my-gcp-connection", + "id": "eec83609-5eb4-4d8d-9f6e-ded016984f0d" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "124e0392-4070-4b1c-900e-ced30cd55bf3" + }, + "folder": { + "id": "1447389e-16fb-49ba-96fd-361b5a2522af", + "path": "/" + }, + "destination": "gcp-secret-manager", + "destinationConfig": { + "projectId": "infisical-test-playground" + } + } + } + ``` + + + diff --git a/docs/mint.json b/docs/mint.json index 7e4491523..7a0952c2c 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -352,7 +352,8 @@ "group": "Connections", "pages": [ "integrations/app-connections/aws", - "integrations/app-connections/github" + "integrations/app-connections/github", + "integrations/app-connections/gcp" ] } ] @@ -365,7 +366,8 @@ "group": "Syncs", "pages": [ "integrations/secret-syncs/aws-parameter-store", - "integrations/secret-syncs/github" + "integrations/secret-syncs/github", + "integrations/secret-syncs/gcp-secret-manager" ] } ] @@ -799,7 +801,8 @@ "pages": [ "api-reference/endpoints/app-connections/list", "api-reference/endpoints/app-connections/options", - { "group": "AWS", + { + "group": "AWS", "pages": [ "api-reference/endpoints/app-connections/aws/list", "api-reference/endpoints/app-connections/aws/available", @@ -810,7 +813,8 @@ "api-reference/endpoints/app-connections/aws/delete" ] }, - { "group": "GitHub", + { + "group": "GitHub", "pages": [ "api-reference/endpoints/app-connections/github/list", "api-reference/endpoints/app-connections/github/available", @@ -820,6 +824,18 @@ "api-reference/endpoints/app-connections/github/update", "api-reference/endpoints/app-connections/github/delete" ] + }, + { + "group": "GCP", + "pages": [ + "api-reference/endpoints/app-connections/gcp/list", + "api-reference/endpoints/app-connections/gcp/available", + "api-reference/endpoints/app-connections/gcp/get-by-id", + "api-reference/endpoints/app-connections/gcp/get-by-name", + "api-reference/endpoints/app-connections/gcp/create", + "api-reference/endpoints/app-connections/gcp/update", + "api-reference/endpoints/app-connections/gcp/delete" + ] } ] }, @@ -828,7 +844,8 @@ "pages": [ "api-reference/endpoints/secret-syncs/list", "api-reference/endpoints/secret-syncs/options", - { "group": "AWS Parameter Store", + { + "group": "AWS Parameter Store", "pages": [ "api-reference/endpoints/secret-syncs/aws-parameter-store/list", "api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-id", @@ -841,7 +858,8 @@ "api-reference/endpoints/secret-syncs/aws-parameter-store/remove-secrets" ] }, - { "group": "GitHub", + { + "group": "GitHub", "pages": [ "api-reference/endpoints/secret-syncs/github/list", "api-reference/endpoints/secret-syncs/github/get-by-id", @@ -852,6 +870,20 @@ "api-reference/endpoints/secret-syncs/github/sync-secrets", "api-reference/endpoints/secret-syncs/github/remove-secrets" ] + }, + { + "group": "GCP Secret Manager", + "pages": [ + "api-reference/endpoints/secret-syncs/gcp-secret-manager/list", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-id", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-name", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/create", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/update", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/delete", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/sync-secrets", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets" + ] } ] }, diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/GcpSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/GcpSyncFields.tsx new file mode 100644 index 000000000..2af03b203 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/GcpSyncFields.tsx @@ -0,0 +1,76 @@ +import { useEffect } from "react"; +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; +import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2"; +import { useGcpConnectionListProjects } from "@app/hooks/api/appConnections/gcp/queries"; +import { TGitHubConnectionEnvironment } from "@app/hooks/api/appConnections/github"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; + +import { TSecretSyncForm } from "../schemas"; + +export const GcpSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.GCPSecretManager } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: projects, isPending } = useGcpConnectionListProjects(connectionId, { + enabled: Boolean(connectionId) + }); + + useEffect(() => { + setValue("destinationConfig.scope", GcpSyncScope.Global); + }, []); + + return ( + <> + { + setValue("destinationConfig.projectId", ""); + }} + /> + ( + +
+ Don't see the project you're looking for?{" "} + +
+ + } + > + project.id === value) ?? null} + onChange={(option) => + onChange((option as SingleValue)?.id ?? null) + } + options={projects} + placeholder="Select a GCP project..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id.toString()} + /> +
+ )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 8edca89fb..19c3d7f7c 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -4,6 +4,7 @@ import { SecretSync } from "@app/hooks/api/secretSyncs"; import { TSecretSyncForm } from "../schemas"; import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields"; +import { GcpSyncFields } from "./GcpSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields"; export const SecretSyncDestinationFields = () => { @@ -16,6 +17,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.GitHub: return ; + case SecretSync.GCPSecretManager: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/GcpSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/GcpSyncReviewFields.tsx new file mode 100644 index 000000000..52776c0e9 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/GcpSyncReviewFields.tsx @@ -0,0 +1,14 @@ +import { useFormContext } from "react-hook-form"; + +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const GcpSyncReviewFields = () => { + const { watch } = useFormContext< + TSecretSyncForm & { destination: SecretSync.GCPSecretManager } + >(); + const projectId = watch("destinationConfig.projectId"); + + return {projectId}; +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 4b198eb7e..6a0ddc280 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -8,6 +8,7 @@ import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/hel import { SecretSync } from "@app/hooks/api/secretSyncs"; import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields"; +import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; export const SecretSyncReviewFields = () => { @@ -38,6 +39,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.GitHub: DestinationFieldsComponent = ; break; + case SecretSync.GCPSecretManager: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/schemas/gcp-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/gcp-sync-destination-schema.ts new file mode 100644 index 000000000..6ffa3ca86 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/gcp-sync-destination-schema.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; + +export const GcpSyncDestinationSchema = z.object({ + destination: z.literal(SecretSync.GCPSecretManager), + destinationConfig: z.object({ + scope: z.literal(GcpSyncScope.Global), + projectId: z.string().min(1, "Project ID required") + }) +}); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index 2ae2bc370..c3d517f82 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -5,6 +5,7 @@ import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs"; import { slugSchema } from "@app/lib/schemas"; import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema"; +import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; const BaseSecretSyncSchema = z.object({ name: slugSchema({ field: "Name" }), @@ -31,7 +32,8 @@ const BaseSecretSyncSchema = z.object({ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ AwsParameterStoreSyncDestinationSchema, - GitHubSyncDestinationSchema + GitHubSyncDestinationSchema, + GcpSyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema); diff --git a/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx b/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx index c28b17927..8e82c2915 100644 --- a/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx +++ b/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx @@ -141,7 +141,7 @@ export const SecretPathInput = ({ maxHeight: "var(--radix-select-content-available-height)" }} > -
+
{suggestions.map((suggestion, i) => (
= { [AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" }, - [AppConnection.GitHub]: { name: "GitHub", image: "GitHub.png" } + [AppConnection.GitHub]: { name: "GitHub", image: "GitHub.png" }, + [AppConnection.GCP]: { + name: "GCP", + image: "Google Cloud Platform.png" + } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { @@ -23,6 +28,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) return { name: "Access Key", icon: faKey }; case AwsConnectionMethod.AssumeRole: return { name: "Assume Role", icon: faUser }; + case GcpConnectionMethod.ServiceAccountImpersonation: + return { name: "Service Account Impersonation", icon: faUser }; default: throw new Error(`Unhandled App Connection Method: ${method}`); } diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index f46f0f230..91005f114 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -7,12 +7,14 @@ import { export const SECRET_SYNC_MAP: Record = { [SecretSync.AWSParameterStore]: { name: "Parameter Store", image: "Amazon Web Services.png" }, - [SecretSync.GitHub]: { name: "GitHub", image: "GitHub.png" } + [SecretSync.GitHub]: { name: "GitHub", image: "GitHub.png" }, + [SecretSync.GCPSecretManager]: { name: "GCP Secret Manager", image: "Google Cloud Platform.png" } }; export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.AWSParameterStore]: AppConnection.AWS, - [SecretSync.GitHub]: AppConnection.GitHub + [SecretSync.GitHub]: AppConnection.GitHub, + [SecretSync.GCPSecretManager]: AppConnection.GCP }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 3c1a409a4..ba29a3781 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -1,4 +1,5 @@ export enum AppConnection { AWS = "aws", - GitHub = "github" + GitHub = "github", + GCP = "gcp" } diff --git a/frontend/src/hooks/api/appConnections/gcp/queries.tsx b/frontend/src/hooks/api/appConnections/gcp/queries.tsx new file mode 100644 index 000000000..a88860006 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/gcp/queries.tsx @@ -0,0 +1,37 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { TGcpProject } from "./types"; + +const gcpConnectionKeys = { + all: [...appConnectionKeys.all, "gcp"] as const, + listProjects: (connectionId: string) => + [...gcpConnectionKeys.all, "projects", connectionId] as const +}; + +export const useGcpConnectionListProjects = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TGcpProject[], + unknown, + TGcpProject[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: gcpConnectionKeys.listProjects(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/gcp/${connectionId}/secret-manager-projects` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/gcp/types.ts b/frontend/src/hooks/api/appConnections/gcp/types.ts new file mode 100644 index 000000000..2af4eee9d --- /dev/null +++ b/frontend/src/hooks/api/appConnections/gcp/types.ts @@ -0,0 +1,4 @@ +export type TGcpProject = { + id: string; + name: string; +}; diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index bfc9e5903..91fc25cc2 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -16,9 +16,14 @@ export type TGitHubConnectionOption = TAppConnectionOptionBase & { appClientSlug?: string; }; +export type TGcpConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.GCP; +}; + export type TAppConnectionOption = TAwsConnectionOption | TGitHubConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; [AppConnection.GitHub]: TGitHubConnectionOption; + [AppConnection.GCP]: TGcpConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/gcp-connection.ts b/frontend/src/hooks/api/appConnections/types/gcp-connection.ts new file mode 100644 index 000000000..44a73d24a --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/gcp-connection.ts @@ -0,0 +1,13 @@ +import { AppConnection } from "../enums"; +import { TRootAppConnection } from "./root-connection"; + +export enum GcpConnectionMethod { + ServiceAccountImpersonation = "service-account-impersonation" +} + +export type TGcpConnection = TRootAppConnection & { app: AppConnection.GCP } & { + method: GcpConnectionMethod.ServiceAccountImpersonation; + credentials: { + serviceAccountEmail: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 64bd41015..283a6d1a0 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -3,10 +3,13 @@ import { TAppConnectionOption } from "@app/hooks/api/appConnections/types/app-op import { TAwsConnection } from "@app/hooks/api/appConnections/types/aws-connection"; import { TGitHubConnection } from "@app/hooks/api/appConnections/types/github-connection"; +import { TGcpConnection } from "./gcp-connection"; + export * from "./aws-connection"; +export * from "./gcp-connection"; export * from "./github-connection"; -export type TAppConnection = TAwsConnection | TGitHubConnection; +export type TAppConnection = TAwsConnection | TGitHubConnection | TGcpConnection; export type TAvailableAppConnection = Pick; @@ -36,4 +39,5 @@ export type TDeleteAppConnectionDTO = { export type TAppConnectionMap = { [AppConnection.AWS]: TAwsConnection; [AppConnection.GitHub]: TGitHubConnection; + [AppConnection.GCP]: TGcpConnection; }; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index b5211eedc..11f902388 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -1,6 +1,7 @@ export enum SecretSync { AWSParameterStore = "aws-parameter-store", - GitHub = "github" + GitHub = "github", + GCPSecretManager = "gcp-secret-manager" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/types/gcp-sync.ts b/frontend/src/hooks/api/secretSyncs/types/gcp-sync.ts new file mode 100644 index 000000000..bda7da6be --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/gcp-sync.ts @@ -0,0 +1,20 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export enum GcpSyncScope { + Global = "global" +} + +export type TGcpSync = TRootSecretSync & { + destination: SecretSync.GCPSecretManager; + destinationConfig: { + scope: GcpSyncScope.Global; + projectId: string; + }; + connection: { + app: AppConnection.GCP; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index c0b714321..88c2a35da 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -3,13 +3,15 @@ import { TAwsParameterStoreSync } from "@app/hooks/api/secretSyncs/types/aws-par import { TGitHubSync } from "@app/hooks/api/secretSyncs/types/github-sync"; import { DiscriminativePick } from "@app/types"; +import { TGcpSync } from "./gcp-sync"; + export type TSecretSyncOption = { name: string; destination: SecretSync; canImportSecrets: boolean; }; -export type TSecretSync = TAwsParameterStoreSync | TGitHubSync; +export type TSecretSync = TAwsParameterStoreSync | TGitHubSync | TGcpSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx index 1032f9958..99a18a6fc 100644 --- a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx @@ -10,6 +10,7 @@ import { DiscriminativePick } from "@app/types"; import { AppConnectionHeader } from "../AppConnectionHeader"; import { AwsConnectionForm } from "./AwsConnectionForm"; +import { GcpConnectionForm } from "./GcpConnectionForm"; import { GitHubConnectionForm } from "./GitHubConnectionForm"; type FormProps = { @@ -50,6 +51,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.GitHub: return ; + case AppConnection.GCP: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -87,6 +90,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.GitHub: return ; + case AppConnection.GCP: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GcpConnectionForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GcpConnectionForm.tsx new file mode 100644 index 000000000..1a392063b --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GcpConnectionForm.tsx @@ -0,0 +1,178 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + IconButton, + ModalClose, + SecretInput, + Select, + SelectItem, + Tooltip +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { useToggle } from "@app/hooks"; +import { GcpConnectionMethod, TGcpConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TGcpConnection; + onSubmit: (formData: FormData) => void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.GCP) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(GcpConnectionMethod.ServiceAccountImpersonation), + credentials: z.object({ + serviceAccountEmail: z.string().email().trim().min(1, "Service account email required") + }) + }) +]); + +type FormData = z.infer; + +export const GcpConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.GCP, + method: GcpConnectionMethod.ServiceAccountImpersonation + } + }); + const { currentOrg } = useOrganization(); + + const [isCopied, { timedToggle: toggleIsCopied }] = useToggle(false); + const expectedAccountIdSuffix = currentOrg.id.split("-").slice(0, 2).join("-"); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + + {`Service account ID (the part of the email before '@') must be suffixed with "${expectedAccountIdSuffix}"`} + + + { + if (isCopied) { + return; + } + + navigator.clipboard.writeText(expectedAccountIdSuffix); + + createNotification({ + text: "Copied to clipboard", + type: "info" + }); + + toggleIsCopied(2000); + }} + className="hover:bg-bunker-100/10" + > + + + + + } + > + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/GcpSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/GcpSyncDestinationCol.tsx new file mode 100644 index 000000000..109586779 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/GcpSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TGcpSync } from "@app/hooks/api/secretSyncs/types/gcp-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TGcpSync; +}; + +export const GcpSyncDestinationCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index fcbe00cfe..673e2aced 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -1,6 +1,7 @@ import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; import { AwsParameterStoreSyncDestinationCol } from "./AwsParameterStoreSyncDestinationCol"; +import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; type Props = { @@ -13,6 +14,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.GitHub: return ; + case SecretSync.GCPSecretManager: + return ; default: throw new Error( `Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}` diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index 2b79015a0..94147aa26 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -39,6 +39,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { throw new Error(`Unhandled GitHub Scope Destination Col Values ${destination}`); } break; + case SecretSync.GCPSecretManager: + primaryText = destinationConfig.projectId; + secondaryText = "Global"; + break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GcpSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GcpSyncDestinationSection.tsx new file mode 100644 index 000000000..69821ddfd --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GcpSyncDestinationSection.tsx @@ -0,0 +1,14 @@ +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { TGcpSync } from "@app/hooks/api/secretSyncs/types/gcp-sync"; + +type Props = { + secretSync: TGcpSync; +}; + +export const GcpSyncDestinationSection = ({ secretSync }: Props) => { + const { + destinationConfig: { projectId } + } = secretSync; + + return {projectId}; +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index fdc87c97c..4bfbd492a 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -12,6 +12,8 @@ import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; import { AwsParameterStoreSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsParameterStoreSyncDestinationSection"; import { GitHubSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GitHubSyncDestinationSection"; +import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection"; + type Props = { secretSync: TSecretSync; onEditDestination: VoidFunction; @@ -30,6 +32,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.GitHub: DestinationComponents = ; break; + case SecretSync.GCPSecretManager: + DestinationComponents = ; + break; default: throw new Error(`Unhandled Destination Section components: ${destination}`); }