mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 09:26:14 +00:00
Merge pull request #3738 from Infisical/gcp-sync-location
feature(gcp-sync): Add support for syncing to locations
This commit is contained in:
@@ -2276,7 +2276,8 @@ export const SecretSyncs = {
|
|||||||
},
|
},
|
||||||
GCP: {
|
GCP: {
|
||||||
scope: "The Google project scope that secrets should be synced to.",
|
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: {
|
DATABRICKS: {
|
||||||
scope: "The Databricks secret scope that secrets should be synced to."
|
scope: "The Databricks secret scope that secrets should be synced to."
|
||||||
|
|||||||
@@ -45,4 +45,37 @@ export const registerGcpConnectionRouter = async (server: FastifyZodProvider) =>
|
|||||||
return projects;
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import { AppConnection } from "../app-connection-enums";
|
|||||||
import { GcpConnectionMethod } from "./gcp-connection-enums";
|
import { GcpConnectionMethod } from "./gcp-connection-enums";
|
||||||
import {
|
import {
|
||||||
GCPApp,
|
GCPApp,
|
||||||
|
GCPGetProjectLocationsRes,
|
||||||
GCPGetProjectsRes,
|
GCPGetProjectsRes,
|
||||||
GCPGetServiceRes,
|
GCPGetServiceRes,
|
||||||
|
GCPLocation,
|
||||||
TGcpConnection,
|
TGcpConnection,
|
||||||
TGcpConnectionConfig
|
TGcpConnectionConfig
|
||||||
} from "./gcp-connection-types";
|
} from "./gcp-connection-types";
|
||||||
@@ -145,6 +147,45 @@ export const getGcpSecretManagerProjects = async (appConnection: TGcpConnection)
|
|||||||
return projects;
|
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<GCPGetProjectLocationsRes>(
|
||||||
|
`${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) => {
|
export const validateGcpConnectionCredentials = async (appConnection: TGcpConnectionConfig) => {
|
||||||
// Check if provided service account email suffix matches organization ID.
|
// Check if provided service account email suffix matches organization ID.
|
||||||
// We do this to mitigate confused deputy attacks in multi-tenant instances
|
// We do this to mitigate confused deputy attacks in multi-tenant instances
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
import { AppConnection } from "../app-connection-enums";
|
import { AppConnection } from "../app-connection-enums";
|
||||||
import { getGcpSecretManagerProjects } from "./gcp-connection-fns";
|
import { getGcpSecretManagerProjectLocations, getGcpSecretManagerProjects } from "./gcp-connection-fns";
|
||||||
import { TGcpConnection } from "./gcp-connection-types";
|
import { TGcpConnection, TGetGCPProjectLocationsDTO } from "./gcp-connection-types";
|
||||||
|
|
||||||
type TGetAppConnectionFunc = (
|
type TGetAppConnectionFunc = (
|
||||||
app: AppConnection,
|
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 {
|
return {
|
||||||
listSecretManagerProjects
|
listSecretManagerProjects,
|
||||||
|
listSecretManagerProjectLocations
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,6 +38,22 @@ export type GCPGetProjectsRes = {
|
|||||||
nextPageToken?: string;
|
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 = {
|
export type GCPGetServiceRes = {
|
||||||
name: string;
|
name: string;
|
||||||
parent: string;
|
parent: string;
|
||||||
|
|||||||
@@ -1,3 +1,63 @@
|
|||||||
export enum GcpSyncScope {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { request } from "@app/lib/config/request";
|
|||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp";
|
import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp";
|
||||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
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 { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||||
|
|
||||||
import { SecretSyncError } from "../secret-sync-errors";
|
import { SecretSyncError } from "../secret-sync-errors";
|
||||||
@@ -15,9 +16,17 @@ import {
|
|||||||
TGcpSyncWithCredentials
|
TGcpSyncWithCredentials
|
||||||
} from "./gcp-sync-types";
|
} from "./gcp-sync-types";
|
||||||
|
|
||||||
const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => {
|
const getProjectUrl = (secretSync: TGcpSyncWithCredentials) => {
|
||||||
const { destinationConfig } = secretSync;
|
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[] = [];
|
let gcpSecrets: GCPSecret[] = [];
|
||||||
|
|
||||||
const pageSize = 100;
|
const pageSize = 100;
|
||||||
@@ -31,16 +40,13 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden
|
|||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const { data: secretsRes } = await request.get<GCPSMListSecretsRes>(
|
const { data: secretsRes } = await request.get<GCPSMListSecretsRes>(`${getProjectUrl(secretSync)}/secrets`, {
|
||||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${secretSync.destinationConfig.projectId}/secrets`,
|
params,
|
||||||
{
|
headers: {
|
||||||
params,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
headers: {
|
"Accept-Encoding": "application/json"
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
"Accept-Encoding": "application/json"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
if (secretsRes.secrets) {
|
if (secretsRes.secrets) {
|
||||||
gcpSecrets = gcpSecrets.concat(secretsRes.secrets);
|
gcpSecrets = gcpSecrets.concat(secretsRes.secrets);
|
||||||
@@ -61,7 +67,7 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: secretLatest } = await request.get<GCPLatestSecretVersionAccess>(
|
const { data: secretLatest } = await request.get<GCPLatestSecretVersionAccess>(
|
||||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}/versions/latest:access`,
|
`${getProjectUrl(secretSync)}/secrets/${key}/versions/latest:access`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
@@ -113,11 +119,14 @@ export const GcpSyncFns = {
|
|||||||
if (!(key in gcpSecrets)) {
|
if (!(key in gcpSecrets)) {
|
||||||
// case: create secret
|
// case: create secret
|
||||||
await request.post(
|
await request.post(
|
||||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets`,
|
`${getProjectUrl(secretSync)}/secrets`,
|
||||||
{
|
{
|
||||||
replication: {
|
replication:
|
||||||
automatic: {}
|
destinationConfig.scope === GcpSyncScope.Global
|
||||||
}
|
? {
|
||||||
|
automatic: {}
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
@@ -131,7 +140,7 @@ export const GcpSyncFns = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await request.post(
|
await request.post(
|
||||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
|
`${getProjectUrl(secretSync)}/secrets/${key}:addVersion`,
|
||||||
{
|
{
|
||||||
payload: {
|
payload: {
|
||||||
data: Buffer.from(secretMap[key].value).toString("base64")
|
data: Buffer.from(secretMap[key].value).toString("base64")
|
||||||
@@ -163,15 +172,12 @@ export const GcpSyncFns = {
|
|||||||
if (secretSync.syncOptions.disableSecretDeletion) continue;
|
if (secretSync.syncOptions.disableSecretDeletion) continue;
|
||||||
|
|
||||||
// case: delete secret
|
// case: delete secret
|
||||||
await request.delete(
|
await request.delete(`${getProjectUrl(secretSync)}/secrets/${key}`, {
|
||||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
|
headers: {
|
||||||
{
|
Authorization: `Bearer ${accessToken}`,
|
||||||
headers: {
|
"Accept-Encoding": "application/json"
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
"Accept-Encoding": "application/json"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
} else if (secretMap[key].value !== gcpSecrets[key]) {
|
} else if (secretMap[key].value !== gcpSecrets[key]) {
|
||||||
if (!secretMap[key].value) {
|
if (!secretMap[key].value) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -180,7 +186,7 @@ export const GcpSyncFns = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await request.post(
|
await request.post(
|
||||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
|
`${getProjectUrl(secretSync)}/secrets/${key}:addVersion`,
|
||||||
{
|
{
|
||||||
payload: {
|
payload: {
|
||||||
data: Buffer.from(secretMap[key].value).toString("base64")
|
data: Buffer.from(secretMap[key].value).toString("base64")
|
||||||
@@ -212,21 +218,18 @@ export const GcpSyncFns = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
removeSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => {
|
removeSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
const { destinationConfig, connection } = secretSync;
|
const { connection } = secretSync;
|
||||||
const accessToken = await getGcpConnectionAuthToken(connection);
|
const accessToken = await getGcpConnectionAuthToken(connection);
|
||||||
|
|
||||||
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
|
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
|
||||||
for await (const [key] of Object.entries(gcpSecrets)) {
|
for await (const [key] of Object.entries(gcpSecrets)) {
|
||||||
if (key in secretMap) {
|
if (key in secretMap) {
|
||||||
await request.delete(
|
await request.delete(`${getProjectUrl(secretSync)}/secrets/${key}`, {
|
||||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
|
headers: {
|
||||||
{
|
Authorization: `Bearer ${accessToken}`,
|
||||||
headers: {
|
"Accept-Encoding": "application/json"
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
"Accept-Encoding": "application/json"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,33 @@ import {
|
|||||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
import { SecretSync } from "../secret-sync-enums";
|
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 GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||||
|
|
||||||
const GcpSyncDestinationConfigSchema = z.object({
|
const GcpSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
|
||||||
scope: z.literal(GcpSyncScope.Global).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope),
|
z
|
||||||
projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId)
|
.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({
|
export const GcpSyncSchema = BaseSecretSyncSchema(SecretSync.GCPSecretManager, GcpSyncOptionsConfig).extend({
|
||||||
destination: z.literal(SecretSync.GCPSecretManager),
|
destination: z.literal(SecretSync.GCPSecretManager),
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 624 KiB After Width: | Height: | Size: 793 KiB |
@@ -34,6 +34,9 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
|
|||||||
|
|
||||||
- **GCP Connection**: The GCP Connection to authenticate with.
|
- **GCP Connection**: The GCP Connection to authenticate with.
|
||||||
- **Project**: The GCP project to sync 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**.
|
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
|
||||||

|

|
||||||
|
|||||||
+108
-7
@@ -5,27 +5,56 @@ import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||||
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
|
import {
|
||||||
import { useGcpConnectionListProjects } from "@app/hooks/api/appConnections/gcp/queries";
|
Badge,
|
||||||
import { TGitHubConnectionEnvironment } from "@app/hooks/api/appConnections/github";
|
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 { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||||
|
|
||||||
import { TSecretSyncForm } from "../schemas";
|
import { TSecretSyncForm } from "../schemas";
|
||||||
|
|
||||||
|
const formatOptionLabel = ({ displayName, locationId }: TGcpLocation) => (
|
||||||
|
<div className="flex w-full flex-row items-center gap-1">
|
||||||
|
<span>{displayName}</span>{" "}
|
||||||
|
<Badge className="h-5 leading-5" variant="success">
|
||||||
|
{locationId}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export const GcpSyncFields = () => {
|
export const GcpSyncFields = () => {
|
||||||
const { control, setValue } = useFormContext<
|
const { control, setValue } = useFormContext<
|
||||||
TSecretSyncForm & { destination: SecretSync.GCPSecretManager }
|
TSecretSyncForm & { destination: SecretSync.GCPSecretManager }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const connectionId = useWatch({ name: "connection.id", control });
|
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, {
|
const { data: projects, isPending } = useGcpConnectionListProjects(connectionId, {
|
||||||
enabled: Boolean(connectionId)
|
enabled: Boolean(connectionId)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: locations, isPending: areLocationsPending } = useGcpConnectionListProjectLocations(
|
||||||
|
{ connectionId, projectId },
|
||||||
|
{
|
||||||
|
enabled: Boolean(connectionId) && Boolean(projectId)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue("destinationConfig.scope", GcpSyncScope.Global);
|
if (!selectedScope) setValue("destinationConfig.scope", GcpSyncScope.Global);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -33,6 +62,7 @@ export const GcpSyncFields = () => {
|
|||||||
<SecretSyncConnectionField
|
<SecretSyncConnectionField
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setValue("destinationConfig.projectId", "");
|
setValue("destinationConfig.projectId", "");
|
||||||
|
setValue("destinationConfig.locationId", "");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -60,9 +90,10 @@ export const GcpSyncFields = () => {
|
|||||||
isLoading={isPending && Boolean(connectionId)}
|
isLoading={isPending && Boolean(connectionId)}
|
||||||
isDisabled={!connectionId}
|
isDisabled={!connectionId}
|
||||||
value={projects?.find((project) => project.id === value) ?? null}
|
value={projects?.find((project) => project.id === value) ?? null}
|
||||||
onChange={(option) =>
|
onChange={(option) => {
|
||||||
onChange((option as SingleValue<TGitHubConnectionEnvironment>)?.id ?? null)
|
setValue("destinationConfig.locationId", "");
|
||||||
}
|
onChange((option as SingleValue<TGcpProject>)?.id ?? null);
|
||||||
|
}}
|
||||||
options={projects}
|
options={projects}
|
||||||
placeholder="Select a GCP project..."
|
placeholder="Select a GCP project..."
|
||||||
getOptionLabel={(option) => option.name}
|
getOptionLabel={(option) => option.name}
|
||||||
@@ -71,6 +102,76 @@ export const GcpSyncFields = () => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
name="destinationConfig.scope"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
tooltipText={
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
Specify how Infisical should sync secrets to GCP. The following options are
|
||||||
|
available:
|
||||||
|
</p>
|
||||||
|
<ul className="flex list-disc flex-col gap-3 pl-4">
|
||||||
|
{Object.values(GCP_SYNC_SCOPES).map(({ name, description }) => {
|
||||||
|
return (
|
||||||
|
<li key={name}>
|
||||||
|
<p className="text-mineshaft-300">
|
||||||
|
<span className="font-medium text-bunker-200">{name}</span>: {description}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
tooltipClassName="max-w-lg"
|
||||||
|
label="Scope"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
onValueChange={(val) => onChange(val)}
|
||||||
|
className="w-full border border-mineshaft-500 capitalize"
|
||||||
|
position="popper"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
isDisabled={!projectId}
|
||||||
|
>
|
||||||
|
{Object.values(GcpSyncScope).map((scope) => {
|
||||||
|
return (
|
||||||
|
<SelectItem className="capitalize" value={scope} key={scope}>
|
||||||
|
{scope}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{selectedScope === GcpSyncScope.Region && (
|
||||||
|
<Controller
|
||||||
|
name="destinationConfig.locationId"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
|
||||||
|
<FilterableSelect
|
||||||
|
menuPlacement="top"
|
||||||
|
isLoading={areLocationsPending && Boolean(projectId)}
|
||||||
|
isDisabled={!projectId}
|
||||||
|
value={locations?.find((option) => option.locationId === value) ?? null}
|
||||||
|
onChange={(option) =>
|
||||||
|
onChange((option as SingleValue<TGcpLocation>)?.locationId ?? null)
|
||||||
|
}
|
||||||
|
options={locations}
|
||||||
|
placeholder="Select a region..."
|
||||||
|
getOptionValue={(option) => option.locationId}
|
||||||
|
formatOptionLabel={formatOptionLabel}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+13
-2
@@ -3,12 +3,23 @@ import { useFormContext } from "react-hook-form";
|
|||||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||||
|
|
||||||
export const GcpSyncReviewFields = () => {
|
export const GcpSyncReviewFields = () => {
|
||||||
const { watch } = useFormContext<
|
const { watch } = useFormContext<
|
||||||
TSecretSyncForm & { destination: SecretSync.GCPSecretManager }
|
TSecretSyncForm & { destination: SecretSync.GCPSecretManager }
|
||||||
>();
|
>();
|
||||||
const projectId = watch("destinationConfig.projectId");
|
const destinationConfig = watch("destinationConfig");
|
||||||
|
|
||||||
return <GenericFieldLabel label="Project ID">{projectId}</GenericFieldLabel>;
|
return (
|
||||||
|
<>
|
||||||
|
<GenericFieldLabel label="Project ID">{destinationConfig.projectId}</GenericFieldLabel>
|
||||||
|
<GenericFieldLabel label="Scope" className="capitalize">
|
||||||
|
{destinationConfig.scope}
|
||||||
|
</GenericFieldLabel>
|
||||||
|
{destinationConfig.scope === GcpSyncScope.Region && (
|
||||||
|
<GenericFieldLabel label="Region">{destinationConfig.locationId}</GenericFieldLabel>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,9 +7,16 @@ import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
|||||||
export const GcpSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
export const GcpSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||||
z.object({
|
z.object({
|
||||||
destination: z.literal(SecretSync.GCPSecretManager),
|
destination: z.literal(SecretSync.GCPSecretManager),
|
||||||
destinationConfig: z.object({
|
destinationConfig: z.discriminatedUnion("scope", [
|
||||||
scope: z.literal(GcpSyncScope.Global),
|
z.object({
|
||||||
projectId: z.string().min(1, "Project ID required")
|
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")
|
||||||
|
})
|
||||||
|
])
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
SecretSyncImportBehavior,
|
SecretSyncImportBehavior,
|
||||||
SecretSyncInitialSyncBehavior
|
SecretSyncInitialSyncBehavior
|
||||||
} from "@app/hooks/api/secretSyncs";
|
} 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";
|
import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
|
||||||
|
|
||||||
export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = {
|
export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = {
|
||||||
@@ -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."
|
"Infisical will sync secrets as environment level shared values to the specified Humanitec application environment."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const GCP_SYNC_SCOPES: Record<GcpSyncScope, { name: string; description: string }> = {
|
||||||
|
[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."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { appConnectionKeys } from "../queries";
|
import { appConnectionKeys } from "../queries";
|
||||||
import { TGcpProject } from "./types";
|
import { TGcpLocation, TGcpProject, TListProjectLocations } from "./types";
|
||||||
|
|
||||||
const gcpConnectionKeys = {
|
const gcpConnectionKeys = {
|
||||||
all: [...appConnectionKeys.all, "gcp"] as const,
|
all: [...appConnectionKeys.all, "gcp"] as const,
|
||||||
listProjects: (connectionId: string) =>
|
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 = (
|
export const useGcpConnectionListProjects = (
|
||||||
@@ -35,3 +37,29 @@ export const useGcpConnectionListProjects = (
|
|||||||
...options
|
...options
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGcpConnectionListProjectLocations = (
|
||||||
|
{ connectionId, projectId }: TListProjectLocations,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
TGcpLocation[],
|
||||||
|
unknown,
|
||||||
|
TGcpLocation[],
|
||||||
|
ReturnType<typeof gcpConnectionKeys.listProjectLocations>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: gcpConnectionKeys.listProjectLocations({ connectionId, projectId }),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TGcpLocation[]>(
|
||||||
|
`/api/v1/app-connections/gcp/${connectionId}/secret-manager-project-locations`,
|
||||||
|
{ params: { projectId } }
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,3 +2,13 @@ export type TGcpProject = {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TListProjectLocations = {
|
||||||
|
connectionId: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGcpLocation = {
|
||||||
|
displayName: string;
|
||||||
|
locationId: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,15 +3,22 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
|
|||||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||||
|
|
||||||
export enum GcpSyncScope {
|
export enum GcpSyncScope {
|
||||||
Global = "global"
|
Global = "global",
|
||||||
|
Region = "region"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TGcpSync = TRootSecretSync & {
|
export type TGcpSync = TRootSecretSync & {
|
||||||
destination: SecretSync.GCPSecretManager;
|
destination: SecretSync.GCPSecretManager;
|
||||||
destinationConfig: {
|
destinationConfig:
|
||||||
scope: GcpSyncScope.Global;
|
| {
|
||||||
projectId: string;
|
scope: GcpSyncScope.Global;
|
||||||
};
|
projectId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
scope: GcpSyncScope.Region;
|
||||||
|
projectId: string;
|
||||||
|
locationId: string;
|
||||||
|
};
|
||||||
connection: {
|
connection: {
|
||||||
app: AppConnection.GCP;
|
app: AppConnection.GCP;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,6 @@
|
|||||||
import { TerraformCloudSyncScope } from "@app/hooks/api/appConnections/terraform-cloud";
|
import { TerraformCloudSyncScope } from "@app/hooks/api/appConnections/terraform-cloud";
|
||||||
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||||
import {
|
import {
|
||||||
GitHubSyncScope,
|
GitHubSyncScope,
|
||||||
GitHubSyncVisibility
|
GitHubSyncVisibility
|
||||||
@@ -47,7 +48,8 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
|
|||||||
break;
|
break;
|
||||||
case SecretSync.GCPSecretManager:
|
case SecretSync.GCPSecretManager:
|
||||||
primaryText = destinationConfig.projectId;
|
primaryText = destinationConfig.projectId;
|
||||||
secondaryText = "Global";
|
secondaryText =
|
||||||
|
destinationConfig.scope === GcpSyncScope.Global ? "Global" : destinationConfig.locationId;
|
||||||
break;
|
break;
|
||||||
case SecretSync.AzureKeyVault:
|
case SecretSync.AzureKeyVault:
|
||||||
primaryText = destinationConfig.vaultBaseUrl;
|
primaryText = destinationConfig.vaultBaseUrl;
|
||||||
|
|||||||
+13
-5
@@ -1,14 +1,22 @@
|
|||||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
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 = {
|
type Props = {
|
||||||
secretSync: TGcpSync;
|
secretSync: TGcpSync;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GcpSyncDestinationSection = ({ secretSync }: Props) => {
|
export const GcpSyncDestinationSection = ({ secretSync }: Props) => {
|
||||||
const {
|
const { destinationConfig } = secretSync;
|
||||||
destinationConfig: { projectId }
|
|
||||||
} = secretSync;
|
|
||||||
|
|
||||||
return <GenericFieldLabel label="Project ID">{projectId}</GenericFieldLabel>;
|
return (
|
||||||
|
<>
|
||||||
|
<GenericFieldLabel label="Project ID">{destinationConfig.projectId}</GenericFieldLabel>
|
||||||
|
<GenericFieldLabel label="Scope" className="capitalize">
|
||||||
|
{destinationConfig.scope}
|
||||||
|
</GenericFieldLabel>
|
||||||
|
{destinationConfig.scope === GcpSyncScope.Region && (
|
||||||
|
<GenericFieldLabel label="Region">{destinationConfig.locationId}</GenericFieldLabel>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user