diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 3734cbf21..779707ed1 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2276,7 +2276,8 @@ export const SecretSyncs = { }, GCP: { scope: "The Google project scope that secrets should be synced to.", - projectId: "The ID of the Google project secrets should be synced to." + projectId: "The ID of the Google project secrets should be synced to.", + locationId: 'The ID of the Google project location secrets should be synced to (ie "us-west4").' }, DATABRICKS: { scope: "The Databricks secret scope that secrets should be synced to." 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 f92d5e668..c88308c64 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 @@ -45,4 +45,37 @@ export const registerGcpConnectionRouter = async (server: FastifyZodProvider) => return projects; } }); + + server.route({ + method: "GET", + url: `/:connectionId/secret-manager-project-locations`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + querystring: z.object({ + projectId: z.string() + }), + response: { + 200: z.object({ displayName: z.string(), locationId: z.string() }).array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { + params: { connectionId }, + query: { projectId } + } = req; + + const locations = await server.services.appConnection.gcp.listSecretManagerProjectLocations( + { connectionId, projectId }, + req.permission + ); + + return locations; + } + }); }; 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 8bde74062..d1533b696 100644 --- a/backend/src/services/app-connection/gcp/gcp-connection-fns.ts +++ b/backend/src/services/app-connection/gcp/gcp-connection-fns.ts @@ -11,8 +11,10 @@ import { AppConnection } from "../app-connection-enums"; import { GcpConnectionMethod } from "./gcp-connection-enums"; import { GCPApp, + GCPGetProjectLocationsRes, GCPGetProjectsRes, GCPGetServiceRes, + GCPLocation, TGcpConnection, TGcpConnectionConfig } from "./gcp-connection-types"; @@ -145,6 +147,45 @@ export const getGcpSecretManagerProjects = async (appConnection: TGcpConnection) return projects; }; +export const getGcpSecretManagerProjectLocations = async (projectId: string, appConnection: TGcpConnection) => { + const accessToken = await getGcpConnectionAuthToken(appConnection); + + let gcpLocations: GCPLocation[] = []; + + 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 } = await request.get( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${projectId}/locations`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + gcpLocations = gcpLocations.concat(data.locations); + + if (!data.nextPageToken) { + hasMorePages = false; + } + + pageToken = data.nextPageToken; + } + + return gcpLocations.sort((a, b) => a.displayName.localeCompare(b.displayName)); +}; + 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 diff --git a/backend/src/services/app-connection/gcp/gcp-connection-service.ts b/backend/src/services/app-connection/gcp/gcp-connection-service.ts index 96b795a8f..74f2ab2c4 100644 --- a/backend/src/services/app-connection/gcp/gcp-connection-service.ts +++ b/backend/src/services/app-connection/gcp/gcp-connection-service.ts @@ -1,8 +1,8 @@ import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "../app-connection-enums"; -import { getGcpSecretManagerProjects } from "./gcp-connection-fns"; -import { TGcpConnection } from "./gcp-connection-types"; +import { getGcpSecretManagerProjectLocations, getGcpSecretManagerProjects } from "./gcp-connection-fns"; +import { TGcpConnection, TGetGCPProjectLocationsDTO } from "./gcp-connection-types"; type TGetAppConnectionFunc = ( app: AppConnection, @@ -23,7 +23,23 @@ export const gcpConnectionService = (getAppConnection: TGetAppConnectionFunc) => } }; + const listSecretManagerProjectLocations = async ( + { connectionId, projectId }: TGetGCPProjectLocationsDTO, + actor: OrgServiceActor + ) => { + const appConnection = await getAppConnection(AppConnection.GCP, connectionId, actor); + + try { + const locations = await getGcpSecretManagerProjectLocations(projectId, appConnection); + + return locations; + } catch (error) { + return []; + } + }; + return { - listSecretManagerProjects + listSecretManagerProjects, + listSecretManagerProjectLocations }; }; 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 2bb518820..4dc4bd131 100644 --- a/backend/src/services/app-connection/gcp/gcp-connection-types.ts +++ b/backend/src/services/app-connection/gcp/gcp-connection-types.ts @@ -38,6 +38,22 @@ export type GCPGetProjectsRes = { nextPageToken?: string; }; +export type GCPLocation = { + name: string; + locationId: string; + displayName: string; +}; + +export type GCPGetProjectLocationsRes = { + locations: GCPLocation[]; + nextPageToken?: string; +}; + +export type TGetGCPProjectLocationsDTO = { + projectId: string; + connectionId: string; +}; + export type GCPGetServiceRes = { name: string; parent: string; diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-enums.ts b/backend/src/services/secret-sync/gcp/gcp-sync-enums.ts index 348d2bfa5..389070a9a 100644 --- a/backend/src/services/secret-sync/gcp/gcp-sync-enums.ts +++ b/backend/src/services/secret-sync/gcp/gcp-sync-enums.ts @@ -1,3 +1,63 @@ export enum GcpSyncScope { - Global = "global" + Global = "global", + Region = "region" +} + +export enum GCPSecretManagerLocation { + // Asia Pacific + ASIA_SOUTHEAST3 = "asia-southeast3", // Bangkok + ASIA_SOUTH2 = "asia-south2", // Delhi + ASIA_EAST2 = "asia-east2", // Hong Kong + ASIA_SOUTHEAST2 = "asia-southeast2", // Jakarta + AUSTRALIA_SOUTHEAST2 = "australia-southeast2", // Melbourne + ASIA_SOUTH1 = "asia-south1", // Mumbai + ASIA_NORTHEAST2 = "asia-northeast2", // Osaka + ASIA_NORTHEAST3 = "asia-northeast3", // Seoul + ASIA_SOUTHEAST1 = "asia-southeast1", // Singapore + AUSTRALIA_SOUTHEAST1 = "australia-southeast1", // Sydney + ASIA_EAST1 = "asia-east1", // Taiwan + ASIA_NORTHEAST1 = "asia-northeast1", // Tokyo + + // Europe + EUROPE_WEST1 = "europe-west1", // Belgium + EUROPE_WEST10 = "europe-west10", // Berlin + EUROPE_NORTH1 = "europe-north1", // Finland + EUROPE_NORTH2 = "europe-north2", // Stockholm + EUROPE_WEST3 = "europe-west3", // Frankfurt + EUROPE_WEST2 = "europe-west2", // London + EUROPE_SOUTHWEST1 = "europe-southwest1", // Madrid + EUROPE_WEST8 = "europe-west8", // Milan + EUROPE_WEST4 = "europe-west4", // Netherlands + EUROPE_WEST12 = "europe-west12", // Turin + EUROPE_WEST9 = "europe-west9", // Paris + EUROPE_CENTRAL2 = "europe-central2", // Warsaw + EUROPE_WEST6 = "europe-west6", // Zurich + + // North America + US_CENTRAL1 = "us-central1", // Iowa + US_WEST4 = "us-west4", // Las Vegas + US_WEST2 = "us-west2", // Los Angeles + NORTHAMERICA_SOUTH1 = "northamerica-south1", // Mexico + NORTHAMERICA_NORTHEAST1 = "northamerica-northeast1", // Montréal + US_EAST4 = "us-east4", // Northern Virginia + US_CENTRAL2 = "us-central2", // Oklahoma + US_WEST1 = "us-west1", // Oregon + US_WEST3 = "us-west3", // Salt Lake City + US_EAST1 = "us-east1", // South Carolina + NORTHAMERICA_NORTHEAST2 = "northamerica-northeast2", // Toronto + US_EAST5 = "us-east5", // Columbus + US_SOUTH1 = "us-south1", // Dallas + US_WEST8 = "us-west8", // Phoenix + + // South America + SOUTHAMERICA_EAST1 = "southamerica-east1", // São Paulo + SOUTHAMERICA_WEST1 = "southamerica-west1", // Santiago + + // Middle East + ME_CENTRAL2 = "me-central2", // Dammam + ME_CENTRAL1 = "me-central1", // Doha + ME_WEST1 = "me-west1", // Tel Aviv + + // Africa + AFRICA_SOUTH1 = "africa-south1" // Johannesburg } diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts index 6a45aab31..d51383fef 100644 --- a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts +++ b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts @@ -4,6 +4,7 @@ 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 { GcpSyncScope } from "@app/services/secret-sync/gcp/gcp-sync-enums"; import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { SecretSyncError } from "../secret-sync-errors"; @@ -15,9 +16,17 @@ import { TGcpSyncWithCredentials } from "./gcp-sync-types"; -const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => { +const getProjectUrl = (secretSync: TGcpSyncWithCredentials) => { const { destinationConfig } = secretSync; + if (destinationConfig.scope === GcpSyncScope.Global) { + return `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}`; + } + + return `https://secretmanager.${destinationConfig.locationId}.rep.googleapis.com/v1/projects/${destinationConfig.projectId}/locations/${destinationConfig.locationId}`; +}; + +const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => { let gcpSecrets: GCPSecret[] = []; const pageSize = 100; @@ -31,16 +40,13 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden }); // 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" - } + const { data: secretsRes } = await request.get(`${getProjectUrl(secretSync)}/secrets`, { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); + }); if (secretsRes.secrets) { gcpSecrets = gcpSecrets.concat(secretsRes.secrets); @@ -61,7 +67,7 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden try { const { data: secretLatest } = await request.get( - `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}/versions/latest:access`, + `${getProjectUrl(secretSync)}/secrets/${key}/versions/latest:access`, { headers: { Authorization: `Bearer ${accessToken}`, @@ -113,11 +119,14 @@ export const GcpSyncFns = { if (!(key in gcpSecrets)) { // case: create secret await request.post( - `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets`, + `${getProjectUrl(secretSync)}/secrets`, { - replication: { - automatic: {} - } + replication: + destinationConfig.scope === GcpSyncScope.Global + ? { + automatic: {} + } + : undefined }, { params: { @@ -131,7 +140,7 @@ export const GcpSyncFns = { ); await request.post( - `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`, + `${getProjectUrl(secretSync)}/secrets/${key}:addVersion`, { payload: { data: Buffer.from(secretMap[key].value).toString("base64") @@ -163,15 +172,12 @@ export const GcpSyncFns = { if (secretSync.syncOptions.disableSecretDeletion) continue; // 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" - } + await request.delete(`${getProjectUrl(secretSync)}/secrets/${key}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); + }); } else if (secretMap[key].value !== gcpSecrets[key]) { if (!secretMap[key].value) { logger.warn( @@ -180,7 +186,7 @@ export const GcpSyncFns = { } await request.post( - `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`, + `${getProjectUrl(secretSync)}/secrets/${key}:addVersion`, { payload: { data: Buffer.from(secretMap[key].value).toString("base64") @@ -212,21 +218,18 @@ export const GcpSyncFns = { }, removeSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => { - const { destinationConfig, connection } = secretSync; + const { 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" - } + await request.delete(`${getProjectUrl(secretSync)}/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 index 0643c431a..875ceaf70 100644 --- a/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts +++ b/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts @@ -10,14 +10,33 @@ import { import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; import { SecretSync } from "../secret-sync-enums"; -import { GcpSyncScope } from "./gcp-sync-enums"; +import { GCPSecretManagerLocation, GcpSyncScope } from "./gcp-sync-enums"; const GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; -const GcpSyncDestinationConfigSchema = z.object({ - scope: z.literal(GcpSyncScope.Global).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope), - projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId) -}); +const GcpSyncDestinationConfigSchema = z.discriminatedUnion("scope", [ + z + .object({ + scope: z.literal(GcpSyncScope.Global).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope), + projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId) + }) + .describe( + JSON.stringify({ + title: "Global" + }) + ), + z + .object({ + scope: z.literal(GcpSyncScope.Region).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope), + projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId), + locationId: z.nativeEnum(GCPSecretManagerLocation).describe(SecretSyncs.DESTINATION_CONFIG.GCP.locationId) + }) + .describe( + JSON.stringify({ + title: "Region" + }) + ) +]); export const GcpSyncSchema = BaseSecretSyncSchema(SecretSync.GCPSecretManager, GcpSyncOptionsConfig).extend({ destination: z.literal(SecretSync.GCPSecretManager), 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 index c50b6232a..1841b4b6d 100644 Binary files a/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-destination.png and b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-destination.png differ diff --git a/docs/integrations/secret-syncs/gcp-secret-manager.mdx b/docs/integrations/secret-syncs/gcp-secret-manager.mdx index df490c6b5..0be08a9a9 100644 --- a/docs/integrations/secret-syncs/gcp-secret-manager.mdx +++ b/docs/integrations/secret-syncs/gcp-secret-manager.mdx @@ -34,6 +34,9 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical." - **GCP Connection**: The GCP Connection to authenticate with. - **Project**: The GCP project to sync with. + - **Scope**: The GCP project scope that secrets should be synced to: + - **Global**: Secrets will be synced globally; available to all project regions. + - **Region**: Secrets will be synced to the specified region. 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) diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/GcpSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/GcpSyncFields.tsx index 2af03b203..3361b8384 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/GcpSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/GcpSyncFields.tsx @@ -5,27 +5,56 @@ 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 { + Badge, + FilterableSelect, + FormControl, + Select, + SelectItem, + Tooltip +} from "@app/components/v2"; +import { GCP_SYNC_SCOPES } from "@app/helpers/secretSyncs"; +import { + useGcpConnectionListProjectLocations, + useGcpConnectionListProjects +} from "@app/hooks/api/appConnections/gcp/queries"; +import { TGcpLocation, TGcpProject } from "@app/hooks/api/appConnections/gcp/types"; import { SecretSync } from "@app/hooks/api/secretSyncs"; import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; import { TSecretSyncForm } from "../schemas"; +const formatOptionLabel = ({ displayName, locationId }: TGcpLocation) => ( +
+ {displayName}{" "} + + {locationId} + +
+); + export const GcpSyncFields = () => { const { control, setValue } = useFormContext< TSecretSyncForm & { destination: SecretSync.GCPSecretManager } >(); const connectionId = useWatch({ name: "connection.id", control }); + const projectId = useWatch({ name: "destinationConfig.projectId", control }); + const selectedScope = useWatch({ name: "destinationConfig.scope", control }); const { data: projects, isPending } = useGcpConnectionListProjects(connectionId, { enabled: Boolean(connectionId) }); + const { data: locations, isPending: areLocationsPending } = useGcpConnectionListProjectLocations( + { connectionId, projectId }, + { + enabled: Boolean(connectionId) && Boolean(projectId) + } + ); + useEffect(() => { - setValue("destinationConfig.scope", GcpSyncScope.Global); + if (!selectedScope) setValue("destinationConfig.scope", GcpSyncScope.Global); }, []); return ( @@ -33,6 +62,7 @@ export const GcpSyncFields = () => { { setValue("destinationConfig.projectId", ""); + setValue("destinationConfig.locationId", ""); }} /> { isLoading={isPending && Boolean(connectionId)} isDisabled={!connectionId} value={projects?.find((project) => project.id === value) ?? null} - onChange={(option) => - onChange((option as SingleValue)?.id ?? null) - } + onChange={(option) => { + setValue("destinationConfig.locationId", ""); + onChange((option as SingleValue)?.id ?? null); + }} options={projects} placeholder="Select a GCP project..." getOptionLabel={(option) => option.name} @@ -71,6 +102,76 @@ export const GcpSyncFields = () => { )} /> + ( + +

+ Specify how Infisical should sync secrets to GCP. The following options are + available: +

+
    + {Object.values(GCP_SYNC_SCOPES).map(({ name, description }) => { + return ( +
  • +

    + {name}: {description} +

    +
  • + ); + })} +
+ + } + tooltipClassName="max-w-lg" + label="Scope" + > + +
+ )} + /> + {selectedScope === GcpSyncScope.Region && ( + ( + + option.locationId === value) ?? null} + onChange={(option) => + onChange((option as SingleValue)?.locationId ?? null) + } + options={locations} + placeholder="Select a region..." + getOptionValue={(option) => option.locationId} + formatOptionLabel={formatOptionLabel} + /> + + )} + /> + )} ); }; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/GcpSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/GcpSyncReviewFields.tsx index 000478f5e..bebb1ffe4 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/GcpSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/GcpSyncReviewFields.tsx @@ -3,12 +3,23 @@ import { useFormContext } from "react-hook-form"; import { GenericFieldLabel } from "@app/components/secret-syncs"; import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; export const GcpSyncReviewFields = () => { const { watch } = useFormContext< TSecretSyncForm & { destination: SecretSync.GCPSecretManager } >(); - const projectId = watch("destinationConfig.projectId"); + const destinationConfig = watch("destinationConfig"); - return {projectId}; + return ( + <> + {destinationConfig.projectId} + + {destinationConfig.scope} + + {destinationConfig.scope === GcpSyncScope.Region && ( + {destinationConfig.locationId} + )} + + ); }; 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 index 4225c6619..fffae153e 100644 --- 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 @@ -7,9 +7,16 @@ import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; export const GcpSyncDestinationSchema = BaseSecretSyncSchema().merge( z.object({ destination: z.literal(SecretSync.GCPSecretManager), - destinationConfig: z.object({ - scope: z.literal(GcpSyncScope.Global), - projectId: z.string().min(1, "Project ID required") - }) + destinationConfig: z.discriminatedUnion("scope", [ + z.object({ + scope: z.literal(GcpSyncScope.Global), + projectId: z.string().min(1, "Project ID required") + }), + z.object({ + scope: z.literal(GcpSyncScope.Region), + projectId: z.string().min(1, "Project ID required"), + locationId: z.string().min(1, "Region required") + }) + ]) }) ); diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 88a0f7517..fbfc001e9 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -4,6 +4,7 @@ import { SecretSyncImportBehavior, SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs"; +import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync"; export const SECRET_SYNC_MAP: Record = { @@ -124,3 +125,14 @@ export const HUMANITEC_SYNC_SCOPES: Record< "Infisical will sync secrets as environment level shared values to the specified Humanitec application environment." } }; + +export const GCP_SYNC_SCOPES: Record = { + [GcpSyncScope.Global]: { + name: "Global", + description: "Secrets will be synced globally; being available in all project regions." + }, + [GcpSyncScope.Region]: { + name: "Region", + description: "Secrets will be synced to the specified region." + } +}; diff --git a/frontend/src/hooks/api/appConnections/gcp/queries.tsx b/frontend/src/hooks/api/appConnections/gcp/queries.tsx index a88860006..c8535aae1 100644 --- a/frontend/src/hooks/api/appConnections/gcp/queries.tsx +++ b/frontend/src/hooks/api/appConnections/gcp/queries.tsx @@ -3,12 +3,14 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { appConnectionKeys } from "../queries"; -import { TGcpProject } from "./types"; +import { TGcpLocation, TGcpProject, TListProjectLocations } from "./types"; const gcpConnectionKeys = { all: [...appConnectionKeys.all, "gcp"] as const, listProjects: (connectionId: string) => - [...gcpConnectionKeys.all, "projects", connectionId] as const + [...gcpConnectionKeys.all, "projects", connectionId] as const, + listProjectLocations: ({ projectId, connectionId }: TListProjectLocations) => + [...gcpConnectionKeys.all, "project-locations", connectionId, projectId] as const }; export const useGcpConnectionListProjects = ( @@ -35,3 +37,29 @@ export const useGcpConnectionListProjects = ( ...options }); }; + +export const useGcpConnectionListProjectLocations = ( + { connectionId, projectId }: TListProjectLocations, + options?: Omit< + UseQueryOptions< + TGcpLocation[], + unknown, + TGcpLocation[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: gcpConnectionKeys.listProjectLocations({ connectionId, projectId }), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/gcp/${connectionId}/secret-manager-project-locations`, + { params: { projectId } } + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/gcp/types.ts b/frontend/src/hooks/api/appConnections/gcp/types.ts index 2af4eee9d..c5b594446 100644 --- a/frontend/src/hooks/api/appConnections/gcp/types.ts +++ b/frontend/src/hooks/api/appConnections/gcp/types.ts @@ -2,3 +2,13 @@ export type TGcpProject = { id: string; name: string; }; + +export type TListProjectLocations = { + connectionId: string; + projectId: string; +}; + +export type TGcpLocation = { + displayName: string; + locationId: string; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/gcp-sync.ts b/frontend/src/hooks/api/secretSyncs/types/gcp-sync.ts index bda7da6be..b7b9c1db8 100644 --- a/frontend/src/hooks/api/secretSyncs/types/gcp-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/types/gcp-sync.ts @@ -3,15 +3,22 @@ import { SecretSync } from "@app/hooks/api/secretSyncs"; import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; export enum GcpSyncScope { - Global = "global" + Global = "global", + Region = "region" } export type TGcpSync = TRootSecretSync & { destination: SecretSync.GCPSecretManager; - destinationConfig: { - scope: GcpSyncScope.Global; - projectId: string; - }; + destinationConfig: + | { + scope: GcpSyncScope.Global; + projectId: string; + } + | { + scope: GcpSyncScope.Region; + projectId: string; + locationId: string; + }; connection: { app: AppConnection.GCP; name: string; 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 4943df471..0390f387a 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 @@ -1,5 +1,6 @@ import { TerraformCloudSyncScope } from "@app/hooks/api/appConnections/terraform-cloud"; import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; +import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; import { GitHubSyncScope, GitHubSyncVisibility @@ -47,7 +48,8 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { break; case SecretSync.GCPSecretManager: primaryText = destinationConfig.projectId; - secondaryText = "Global"; + secondaryText = + destinationConfig.scope === GcpSyncScope.Global ? "Global" : destinationConfig.locationId; break; case SecretSync.AzureKeyVault: primaryText = destinationConfig.vaultBaseUrl; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GcpSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GcpSyncDestinationSection.tsx index cffefcfd3..88e146c48 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GcpSyncDestinationSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GcpSyncDestinationSection.tsx @@ -1,14 +1,22 @@ import { GenericFieldLabel } from "@app/components/secret-syncs"; -import { TGcpSync } from "@app/hooks/api/secretSyncs/types/gcp-sync"; +import { GcpSyncScope, TGcpSync } from "@app/hooks/api/secretSyncs/types/gcp-sync"; type Props = { secretSync: TGcpSync; }; export const GcpSyncDestinationSection = ({ secretSync }: Props) => { - const { - destinationConfig: { projectId } - } = secretSync; + const { destinationConfig } = secretSync; - return {projectId}; + return ( + <> + {destinationConfig.projectId} + + {destinationConfig.scope} + + {destinationConfig.scope === GcpSyncScope.Region && ( + {destinationConfig.locationId} + )} + + ); };