From 753c28a2d3e6c22fcd4c196e9c9320b768449732 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sat, 25 Jan 2025 03:01:10 +0800 Subject: [PATCH] feat: gcp secret sync management --- .../gcp-connection-router.ts | 33 ++++++- .../v1/secret-sync-routers/gcp-sync-router.ts | 13 +++ .../routes/v1/secret-sync-routers/index.ts | 4 +- .../secret-sync-routers/secret-sync-router.ts | 10 ++- .../app-connection/app-connection-maps.ts | 3 +- .../app-connection/app-connection-service.ts | 4 +- .../app-connection/gcp/gcp-connection-fns.ts | 87 ++++++++++++++++++- .../gcp/gcp-connection-service.ts | 29 +++++++ .../gcp/gcp-connection-types.ts | 23 +++++ .../secret-sync/gcp/gcp-sync-constants.ts | 10 +++ .../secret-sync/gcp/gcp-sync-schemas.ts | 37 ++++++++ .../secret-sync/gcp/gcp-sync-types.ts | 15 ++++ backend/src/services/secret-sync/gcp/index.ts | 3 + .../services/secret-sync/secret-sync-enums.ts | 3 +- .../services/secret-sync/secret-sync-fns.ts | 5 +- .../services/secret-sync/secret-sync-maps.ts | 6 +- .../services/secret-sync/secret-sync-types.ts | 12 ++- .../GcpSyncFields.tsx | 51 +++++++++++ .../SecretSyncDestinationFields.tsx | 3 + .../GcpSyncReviewFIelds.tsx | 12 +++ .../SecretSyncReviewFields.tsx | 4 + .../schemas/gcp-sync-destination-schema.ts | 10 +++ .../forms/schemas/secret-sync-schema.ts | 4 +- frontend/src/helpers/secretSyncs.ts | 6 +- .../hooks/api/appConnections/gcp/queries.tsx | 37 ++++++++ .../src/hooks/api/appConnections/gcp/types.ts | 4 + frontend/src/hooks/api/secretSyncs/enums.ts | 3 +- .../hooks/api/secretSyncs/types/gcp-sync.ts | 15 ++++ .../src/hooks/api/secretSyncs/types/index.ts | 4 +- .../GcpSyncDestinationCol.tsx | 14 +++ .../SecretSyncDestinationCol.tsx | 3 + .../SecretSyncTable/helpers/index.ts | 3 + .../GcpSyncDestinationSection.tsx | 14 +++ .../SecretSyncDestinatonSection.tsx | 5 ++ 34 files changed, 467 insertions(+), 22 deletions(-) create mode 100644 backend/src/server/routes/v1/secret-sync-routers/gcp-sync-router.ts create mode 100644 backend/src/services/app-connection/gcp/gcp-connection-service.ts create mode 100644 backend/src/services/secret-sync/gcp/gcp-sync-constants.ts create mode 100644 backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts create mode 100644 backend/src/services/secret-sync/gcp/gcp-sync-types.ts create mode 100644 backend/src/services/secret-sync/gcp/index.ts create mode 100644 frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/GcpSyncFields.tsx create mode 100644 frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/GcpSyncReviewFIelds.tsx create mode 100644 frontend/src/components/secret-syncs/forms/schemas/gcp-sync-destination-schema.ts create mode 100644 frontend/src/hooks/api/appConnections/gcp/queries.tsx create mode 100644 frontend/src/hooks/api/appConnections/gcp/types.ts create mode 100644 frontend/src/hooks/api/secretSyncs/types/gcp-sync.ts create mode 100644 frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/GcpSyncDestinationCol.tsx create mode 100644 frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GcpSyncDestinationSection.tsx 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 index 5c76df160..f92d5e668 100644 --- 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 @@ -1,13 +1,18 @@ +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) => +export const registerGcpConnectionRouter = async (server: FastifyZodProvider) => { registerAppConnectionEndpoints({ app: AppConnection.GCP, server, @@ -15,3 +20,29 @@ export const registerGcpConnectionRouter = async (server: FastifyZodProvider) => 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/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..02020db02 --- /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.GCP, + 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..212e27fda 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.GCP]: 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-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-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 5befc0168..00006a71c 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -27,6 +27,7 @@ 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; @@ -384,6 +385,7 @@ export const appConnectionServiceFactory = ({ deleteAppConnection, connectAppConnectionById, listAvailableAppConnectionsForUser, - github: githubConnectionService(connectAppConnectionById) + github: githubConnectionService(connectAppConnectionById), + gcp: gcpConnectionService(connectAppConnectionById) }; }; diff --git a/backend/src/services/app-connection/gcp/gcp-connection-fns.ts b/backend/src/services/app-connection/gcp/gcp-connection-fns.ts index 3039e814f..39e56e055 100644 --- a/backend/src/services/app-connection/gcp/gcp-connection-fns.ts +++ b/backend/src/services/app-connection/gcp/gcp-connection-fns.ts @@ -2,12 +2,20 @@ 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 { request } from "@app/lib/config/request"; import { BadRequestError, InternalServerError } from "@app/lib/errors"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { AppConnection } from "../app-connection-enums"; import { getAppConnectionMethodName } from "../app-connection-fns"; import { GcpConnectionMethod } from "./gcp-connection-enums"; -import { TGcpConnectionConfig } from "./gcp-connection-types"; +import { + GCPApp, + GCPGetProjectsRes, + GCPGetServiceRes, + TGcpConnection, + TGcpConnectionConfig +} from "./gcp-connection-types"; export const getGcpAppConnectionListItem = () => { return { @@ -17,9 +25,8 @@ export const getGcpAppConnectionListItem = () => { }; }; -export const validateGcpConnectionCredentials = async (appConnection: TGcpConnectionConfig) => { +export const getAuthToken = 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( @@ -67,5 +74,79 @@ export const validateGcpConnectionCredentials = async (appConnection: TGcpConnec }); } + return tokenResponse.token; +}; + +export const getGcpSecretManagerProjects = async (appConnection: TGcpConnection) => { + const accessToken = await getAuthToken(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) => { + await getAuthToken(appConnection); + return appConnection.credentials; }; 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 index 30b1ff8f3..36d2dc74a 100644 --- a/backend/src/services/app-connection/gcp/gcp-connection-types.ts +++ b/backend/src/services/app-connection/gcp/gcp-connection-types.ts @@ -20,3 +20,26 @@ export type TValidateGcpConnectionCredentials = typeof ValidateGcpConnectionCred export type TGcpConnectionConfig = DiscriminativePick & { orgId: string; }; + +export interface 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 interface GCPGetProjectsRes { + projects: GCPApp[]; + nextPageToken?: string; +} + +export interface GCPGetServiceRes { + name: string; + parent: string; + state: "ENABLED" | "DISABLED" | "STATE_UNSPECIFIED"; +} 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..111ed615b --- /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", + destination: SecretSync.GCP, + connection: AppConnection.GCP, + canImportSecrets: false +}; 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..994965af9 --- /dev/null +++ b/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts @@ -0,0 +1,37 @@ +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"; + +const GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +const GcpSyncDestinationConfigSchema = z.object({ + projectId: z.string().min(1, "Project ID is required") +}); + +export const GcpSyncSchema = BaseSecretSyncSchema(SecretSync.GCP, GcpSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.GCP), + destinationConfig: GcpSyncDestinationConfigSchema +}); + +export const CreateGcpSyncSchema = GenericCreateSecretSyncFieldsSchema(SecretSync.GCP, GcpSyncOptionsConfig).extend({ + destinationConfig: GcpSyncDestinationConfigSchema +}); + +export const UpdateGcpSyncSchema = GenericUpdateSecretSyncFieldsSchema(SecretSync.GCP, GcpSyncOptionsConfig).extend({ + destinationConfig: GcpSyncDestinationConfigSchema.optional() +}); + +export const GcpSyncListItemSchema = z.object({ + name: z.literal("GCP"), + connection: z.literal(AppConnection.GCP), + destination: z.literal(SecretSync.GCP), + canImportSecrets: z.literal(false) +}); 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..4cda9618b --- /dev/null +++ b/backend/src/services/secret-sync/gcp/gcp-sync-types.ts @@ -0,0 +1,15 @@ +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; +}; 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..296c6867c --- /dev/null +++ b/backend/src/services/secret-sync/gcp/index.ts @@ -0,0 +1,3 @@ +export * from "./gcp-sync-constants"; +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..61280b93f 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", + GCP = "gcp" } 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..e6ee5931f 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -13,9 +13,12 @@ import { TSecretSyncWithCredentials } from "@app/services/secret-sync/secret-sync-types"; +import { GCP_SYNC_LIST_OPTION } from "./gcp"; + 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.GCP]: GCP_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 67ba7b690..a6d62f233 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.GCP]: "GCP Secret Manager" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.AWSParameterStore]: AppConnection.AWS, - [SecretSync.GitHub]: AppConnection.GitHub + [SecretSync.GitHub]: AppConnection.GitHub, + [SecretSync.GCP]: 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/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..f9637411d --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/GcpSyncFields.tsx @@ -0,0 +1,51 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl } 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 { TSecretSyncForm } from "../schemas"; + +export const GcpSyncFields = () => { + const { control, setValue } = useFormContext(); + + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: projects, isPending } = useGcpConnectionListProjects(connectionId, { + enabled: Boolean(connectionId) + }); + + return ( + <> + { + setValue("destinationConfig.projectId", ""); + }} + /> + ( + + 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..77f0712e6 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.GCP: + 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..28986c317 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/GcpSyncReviewFIelds.tsx @@ -0,0 +1,12 @@ +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(); + 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..2a8ed4a0a 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.GCP: + 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..faf2fd8d1 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/gcp-sync-destination-schema.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const GcpSyncDestinationSchema = z.object({ + destination: z.literal(SecretSync.GCP), + destinationConfig: z.object({ + 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/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index f46f0f230..98299c8a4 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.GCP]: { 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.GCP]: AppConnection.GCP }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< 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/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index b5211eedc..18d388b06 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", + GCP = "gcp" } 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..d7332bf9e --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/gcp-sync.ts @@ -0,0 +1,15 @@ +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 type TGcpSync = TRootSecretSync & { + destination: SecretSync.GCP; + destinationConfig: { + 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/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..43ac55508 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.GCP: + 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..2efe74b9f 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,9 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { throw new Error(`Unhandled GitHub Scope Destination Col Values ${destination}`); } break; + case SecretSync.GCP: + primaryText = destinationConfig.projectId; + 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..abf8539ba 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.GCP: + DestinationComponents = ; + break; default: throw new Error(`Unhandled Destination Section components: ${destination}`); }