mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 16:40:30 +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: {
|
||||
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."
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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<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) => {
|
||||
// Check if provided service account email suffix matches organization ID.
|
||||
// We do this to mitigate confused deputy attacks in multi-tenant instances
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<GCPSMListSecretsRes>(
|
||||
`${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<GCPSMListSecretsRes>(`${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<GCPLatestSecretVersionAccess>(
|
||||
`${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"
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user