diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index c9ddfc54a..af16e29bc 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -1,6 +1,4 @@ import { - INTEGRATION_GCP_SECRET_MANAGER, - INTEGRATION_GCP_API_URL, INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_SECRET_MANAGER, INTEGRATION_AZURE_KEY_VAULT, @@ -20,6 +18,10 @@ import { INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, INTEGRATION_FLYIO_API_URL, + INTEGRATION_GCP_API_URL, + INTEGRATION_GCP_SECRET_MANAGER, + INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME, + INTEGRATION_GCP_SERVICE_USAGE_URL, INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_GITLAB_API_URL, @@ -218,17 +220,14 @@ const getApps = async ({ }; /** - * Return list of apps for Heroku integration + * Return list of apps for GCP secret manager integration * @param {Object} obj - * @param {String} obj.accessToken - access token for Heroku API - * @returns {Object[]} apps - names of Heroku apps - * @returns {String} apps.name - name of Heroku app + * @param {String} obj.accessToken - access token for GCP API + * @returns {Object[]} apps - list of GCP projects + * @returns {String} apps.name - name of GCP project + * @returns {String} apps.appId - id of GCP project */ const getAppsGCPSecretManager = async ({ accessToken }: { accessToken: string }) => { - console.log("getAppsGCPSecretManager"); - console.log("getAppsGCPSecretManager accessToken: ", accessToken); - - let apps: any = []; interface GCPApp { projectNumber: string; @@ -242,24 +241,31 @@ const getAppsGCPSecretManager = async ({ accessToken }: { accessToken: string }) } } - interface GCPRes { + interface GCPGetProjectsRes { projects: GCPApp[]; nextPageToken?: string; } - const pageSize = 10; + interface GCPGetServiceRes { + name: string; + parent: string; + state: "ENABLED" | "DISABLED" | "STATE_UNSPECIFIED" + } + + let gcpApps: GCPApp[] = []; + const apps: App[] = []; + + const pageSize = 100; let pageToken: string | undefined; let hasMorePages = true; while (hasMorePages) { - console.log("iterrr"); const params = new URLSearchParams({ pageSize: String(pageSize), ...(pageToken ? { pageToken } : {}) }); - console.log("params: ", params); - const res: GCPRes = (await standardRequest.get(`${INTEGRATION_GCP_API_URL}/v1/projects`, { + const res: GCPGetProjectsRes = (await standardRequest.get(`${INTEGRATION_GCP_API_URL}/v1/projects`, { params, headers: { "Authorization": `Bearer ${accessToken}`, @@ -269,12 +275,7 @@ const getAppsGCPSecretManager = async ({ accessToken }: { accessToken: string }) ) .data; - res.projects.forEach((project) => { - apps.push({ - name: project.name, - appId: project.projectId - }); - }); + gcpApps = gcpApps.concat(res.projects); if (!res.nextPageToken) { hasMorePages = false; @@ -283,23 +284,29 @@ const getAppsGCPSecretManager = async ({ accessToken }: { accessToken: string }) pageToken = res.nextPageToken; } - // const projects: GCPApp[] = ( - // .projects - - // console.log("res: ", res); + for await (const gcpApp of gcpApps) { + try { + const res: GCPGetServiceRes = (await standardRequest.get( + `${INTEGRATION_GCP_SERVICE_USAGE_URL}/v1/projects/${gcpApp.projectId}/services/${INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME}`, { + headers: { + "Authorization": `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + )).data; + + if (res.state === "ENABLED") { + apps.push({ + name: gcpApp.name, + appId: gcpApp.projectId + }); + } + } catch { + continue; + } + } - // .filter((project: GCPApp) => project.lifecycleState === "ACTIVE"); - - // console.log("projects: ", projects); - - // const apps = projects.map((project) => ({ - // name: project.name, - // appId: project.projectId - // })); - - console.log("apps: ", apps); - - return []; + return apps; }; /** diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 099ef2804..885cf5dfd 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -26,6 +26,8 @@ import { INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, INTEGRATION_FLYIO_API_URL, + INTEGRATION_GCP_SECRET_MANAGER, + INTEGRATION_GCP_SECRET_MANAGER_URL, INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_GITLAB_API_URL, @@ -92,6 +94,13 @@ const syncSecrets = async ({ accessToken: string; }) => { switch (integration.integration) { + case INTEGRATION_GCP_SECRET_MANAGER: + await syncSecretsGCPSecretManager({ + integration, + secrets, + accessToken + }); + break; case INTEGRATION_AZURE_KEY_VAULT: await syncSecretsAzureKeyVault({ integration, @@ -286,6 +295,163 @@ const syncSecrets = async ({ } }; +/** + * Sync/push [secrets] to GCP secret manager project + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for GCP secret manager + */ +const syncSecretsGCPSecretManager = async ({ + integration, + secrets, + accessToken +}: { + integration: IIntegration; + secrets: Record; + accessToken: string; +}) => { + interface GCPSecret { + name: string; + createTime: string; + } + + interface GCPSMListSecretsRes { + secrets: GCPSecret[]; + totalSize: number; + nextPageToken?: string; + } + + let gcpSecrets: GCPSecret[] = []; + + const pageSize = 100; + let pageToken: string | undefined; + let hasMorePages = true; + + while (hasMorePages) { + const params = new URLSearchParams({ + pageSize: String(pageSize), + ...(pageToken ? { pageToken } : {}) + }); + + const res: GCPSMListSecretsRes = (await standardRequest.get( + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets`, + { + params, + headers: { + "Authorization": `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + )).data; + + gcpSecrets = gcpSecrets.concat(res.secrets); + + if (!res.nextPageToken) { + hasMorePages = false; + } + + pageToken = res.nextPageToken; + } + + const res: { [key: string]: string; } = {}; + + interface GCPLatestSecretVersionAccess { + name: string; + payload: { + data: string; + } + } + + for await (const gcpSecret of gcpSecrets) { + const arr = gcpSecret.name.split("/"); + const key = arr[arr.length - 1]; + + const secretLatest: GCPLatestSecretVersionAccess = (await standardRequest.get( + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}/versions/latest:access`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + )).data; + + res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8"); + } + + for await (const key of Object.keys(secrets)) { + if (!(key in res)) { + // case: create secret + await standardRequest.post( + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets`, + { + replication: { + automatic: {} + } + }, + { + params: { + secretId: key + }, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + await standardRequest.post( + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}:addVersion`, + { + payload: { + data: Buffer.from(secrets[key].value).toString("base64") + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } + + for await (const key of Object.keys(res)) { + if (!(key in secrets)) { + // case: delete secret + await standardRequest.delete( + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } else { + // case: update secret + if (secrets[key].value !== res[key]) { + await standardRequest.post( + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}:addVersion`, + { + payload: { + data: Buffer.from(secrets[key].value).toString("base64") + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } + } +} + /** * Sync/push [secrets] to Azure Key Vault with vault URI [integration.app] * @param {Object} obj diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index 41c9f43a1..ed69cf5fc 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -1,12 +1,12 @@ import { getClientIdAzure, getClientIdBitBucket, + getClientIdGCPSecretManager, getClientIdGitHub, getClientIdGitLab, getClientIdHeroku, getClientIdNetlify, - getClientSlugVercel, - getClientIdGCPSecretManager + getClientSlugVercel } from "../config"; // integrations @@ -102,6 +102,10 @@ export const INTEGRATION_DIGITAL_OCEAN_API_URL = "https://api.digitalocean.com"; export const INTEGRATION_CLOUD_66_API_URL = "https://app.cloud66.com/api"; export const INTEGRATION_NORTHFLANK_API_URL = "https://api.northflank.com"; +export const INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com" +export const INTEGRATION_GCP_SECRET_MANAGER_URL = `https://${INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME}`; +export const INTEGRATION_GCP_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com"; + export const getIntegrationOptions = async () => { const INTEGRATION_OPTIONS = [ { diff --git a/docs/images/integrations-gcp-secret-manager-auth.png b/docs/images/integrations-gcp-secret-manager-auth.png new file mode 100644 index 000000000..647cff42f Binary files /dev/null and b/docs/images/integrations-gcp-secret-manager-auth.png differ diff --git a/docs/images/integrations-gcp-secret-manager-create.png b/docs/images/integrations-gcp-secret-manager-create.png new file mode 100644 index 000000000..9e1719a59 Binary files /dev/null and b/docs/images/integrations-gcp-secret-manager-create.png differ diff --git a/docs/images/integrations-gcp-secret-manager.png b/docs/images/integrations-gcp-secret-manager.png new file mode 100644 index 000000000..81fd62f72 Binary files /dev/null and b/docs/images/integrations-gcp-secret-manager.png differ diff --git a/docs/integrations/cloud/gcp-secret-manager.mdx b/docs/integrations/cloud/gcp-secret-manager.mdx new file mode 100644 index 000000000..e596a204e --- /dev/null +++ b/docs/integrations/cloud/gcp-secret-manager.mdx @@ -0,0 +1,37 @@ +--- +title: "GCP Secret Manager" +description: "How to sync secrets from Infisical to GCP Secret Manager" +--- + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) + +## Navigate to your project's integrations tab + +![integrations](../../images/integrations.png) + +## Authorize Infisical for GCP + +Press on the GCP Secret Manager tile and grant Infisical access to GCP. + +![integrations GCP authorization](../../images/integrations-gcp-secret-manager-auth.png) + + + If this is your project's first cloud integration, then you'll have to grant + Infisical access to your project's environment variables. Although this step + breaks E2EE, it's necessary for Infisical to sync the environment variables to + the cloud platform. + + +## Start integration + +Select which Infisical environment secrets you want to sync to which GCP secret manager project. Lastly, press create integration to start syncing secrets to GCP secret manager. + +![integrations GCP secret manager](../../images/integrations-gcp-secret-manager-create.png) +![integrations GCP secret manager](../../images/integrations-gcp-secret-manager.png) + + + Using Infisical to sync secrets to GCP Secret Manager requires that you enable + the Service Usage API in the Google Cloud project you want to sync secrets to. More on that [here](https://cloud.google.com/service-usage/docs/set-up-development-environment). + \ No newline at end of file diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx index d97280361..a464aa9cb 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -31,6 +31,7 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi | [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available | | [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available | | [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available | +| [GCP Secret Manager](/integrations/cloud/gcp-secret-manager) | Cloud | Available | | [Windmill](/integrations/cloud/windmill) | Cloud | Available | | [BitBucket](/integrations/cicd/bitbucket) | CI/CD | Available | | [Codefresh](/integrations/cicd/codefresh) | CI/CD | Available | @@ -53,5 +54,4 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi | [Flask](/integrations/frameworks/flask) | Framework | Available | | [Laravel](/integrations/frameworks/laravel) | Framework | Available | | [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available | -| GCP Secret Manager | Cloud | Coming soon | | Jenkins | CI/CD | Coming soon | diff --git a/docs/mint.json b/docs/mint.json index 4892def24..dbb6c2c94 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -237,6 +237,7 @@ "integrations/cloud/checkly", "integrations/cloud/hashicorp-vault", "integrations/cloud/azure-key-vault", + "integrations/cloud/gcp-secret-manager", "integrations/cloud/cloud-66", "integrations/cloud/windmill", "integrations/cicd/githubactions", diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index 1cba174ca..11adf2e7a 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -29,7 +29,7 @@ const integrationSlugNameMapping: Mapping = { "cloud-66": "Cloud 66", "northflank": "Northflank", "windmill": "Windmill", - "gcp-secret-manager": "Google Cloud Platform" + "gcp-secret-manager": "GCP Secret Manager" } const envMapping: Mapping = { diff --git a/frontend/src/pages/integrations/gcp-secret-manager/create.tsx b/frontend/src/pages/integrations/gcp-secret-manager/create.tsx index 975c05e96..8d5d1ff1b 100644 --- a/frontend/src/pages/integrations/gcp-secret-manager/create.tsx +++ b/frontend/src/pages/integrations/gcp-secret-manager/create.tsx @@ -33,10 +33,8 @@ export default function GCPSecretManagerCreateIntegrationPage() { integrationAuthId: (integrationAuthId as string) ?? "" }); - console.log("integrationAuthApps: ", integrationAuthApps); - const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); - const [targetApp, setTargetApp] = useState(""); + const [targetAppId, setTargetAppId] = useState(""); const [secretPath, setSecretPath] = useState("/"); const [isLoading, setIsLoading] = useState(false); @@ -50,9 +48,9 @@ export default function GCPSecretManagerCreateIntegrationPage() { useEffect(() => { if (integrationAuthApps) { if (integrationAuthApps.length > 0) { - setTargetApp(integrationAuthApps[0].name); + setTargetAppId(integrationAuthApps[0].appId as string); } else { - setTargetApp("none"); + setTargetAppId("none"); } } }, [integrationAuthApps]); @@ -62,12 +60,12 @@ export default function GCPSecretManagerCreateIntegrationPage() { setIsLoading(true); if (!integrationAuth?._id) return; - + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, - app: targetApp, - appId: null, + app: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId)?.name ?? null, + appId: targetAppId, sourceEnvironment: selectedSourceEnvironment, targetEnvironment: null, targetEnvironmentId: null, @@ -90,11 +88,11 @@ export default function GCPSecretManagerCreateIntegrationPage() { workspace && selectedSourceEnvironment && integrationAuthApps && - targetApp ? ( + targetAppId ? (
GCP Secret Manager Integration - {/* + setTargetApp(val)} + value={targetAppId} + onValueChange={(val) => setTargetAppId(val)} className="w-full border border-mineshaft-500" isDisabled={integrationAuthApps.length === 0} > {integrationAuthApps.length > 0 ? ( integrationAuthApps.map((integrationAuthApp) => ( {integrationAuthApp.name} )) ) : ( - No apps found + No projects found )} - */} +