diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 3a3405e3d..b5e50d9da 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -37,6 +37,7 @@ export const getClientIdNetlify = async () => (await client.getSecret("CLIENT_ID export const getClientIdGitHub = async () => (await client.getSecret("CLIENT_ID_GITHUB")).secretValue; export const getClientIdGitLab = async () => (await client.getSecret("CLIENT_ID_GITLAB")).secretValue; export const getClientIdBitBucket = async () => (await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue; +export const getClientIdGCPSecretManager = async () => (await client.getSecret("CLIENT_ID_GCP_SECRET_MANAGER")).secretValue; export const getClientSecretAzure = async () => (await client.getSecret("CLIENT_SECRET_AZURE")).secretValue; export const getClientSecretHeroku = async () => (await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue; export const getClientSecretVercel = async () => (await client.getSecret("CLIENT_SECRET_VERCEL")).secretValue; @@ -44,6 +45,7 @@ export const getClientSecretNetlify = async () => (await client.getSecret("CLIEN export const getClientSecretGitHub = async () => (await client.getSecret("CLIENT_SECRET_GITHUB")).secretValue; export const getClientSecretGitLab = async () => (await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue; export const getClientSecretBitBucket = async () => (await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue; +export const getClientSecretGCPSecretManager = async () => (await client.getSecret("CLIENT_SECRET_GCP_SECRET_MANAGER")).secretValue; export const getClientSlugVercel = async () => (await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue; export const getClientIdGoogleLogin = async () => (await client.getSecret("CLIENT_ID_GOOGLE_LOGIN")).secretValue; diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index 4598ab213..af16e29bc 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -18,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, @@ -79,6 +83,11 @@ const getApps = async ({ }) => { let apps: App[] = []; switch (integrationAuth.integration) { + case INTEGRATION_GCP_SECRET_MANAGER: + apps = await getAppsGCPSecretManager({ + accessToken, + }); + break; case INTEGRATION_AZURE_KEY_VAULT: apps = []; break; @@ -210,6 +219,96 @@ const getApps = async ({ return apps; }; +/** + * Return list of apps for GCP secret manager integration + * @param {Object} obj + * @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 }) => { + + interface GCPApp { + projectNumber: string; + projectId: string; + lifecycleState: "ACTIVE" | "LIFECYCLE_STATE_UNSPECIFIED" | "DELETE_REQUESTED" | "DELETE_IN_PROGRESS"; + name: string; + createTime: string; + parent: { + type: "organization" | "folder" | "project"; + id: string; + } + } + + interface GCPGetProjectsRes { + projects: GCPApp[]; + nextPageToken?: string; + } + + 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) { + const params = new URLSearchParams({ + pageSize: String(pageSize), + ...(pageToken ? { pageToken } : {}) + }); + + const res: GCPGetProjectsRes = (await standardRequest.get(`${INTEGRATION_GCP_API_URL}/v1/projects`, { + params, + headers: { + "Authorization": `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + }) + ) + .data; + + gcpApps = gcpApps.concat(res.projects); + + if (!res.nextPageToken) { + hasMorePages = false; + } + + pageToken = res.nextPageToken; + } + + 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; + } + } + + return apps; +}; + /** * Return list of apps for Heroku integration * @param {Object} obj diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts index dddbb65c6..5054df832 100644 --- a/backend/src/integrations/exchange.ts +++ b/backend/src/integrations/exchange.ts @@ -14,8 +14,12 @@ import { INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_VERCEL, INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_GCP_SECRET_MANAGER, + INTEGRATION_GCP_TOKEN_URL } from "../variables"; import { + getClientIdGCPSecretManager, + getClientSecretGCPSecretManager, getClientIdAzure, getClientIdBitBucket, getClientIdGitHub, @@ -113,6 +117,11 @@ const exchangeCode = async ({ let obj = {} as any; switch (integration) { + case INTEGRATION_GCP_SECRET_MANAGER: + obj = await exchangeCodeGCP({ + code, + }); + break; case INTEGRATION_AZURE_KEY_VAULT: obj = await exchangeCodeAzure({ code, @@ -153,6 +162,40 @@ const exchangeCode = async ({ return obj; }; +/** + * Return [accessToken] for GCP OAuth2 code-token exchange + * @param {Object} obj + * @param {String} obj.code - code for code-token exchange + * @returns {Object} obj2 + * @returns {String} obj2.accessToken - access token for GCP API + * @returns {String} obj2.refreshToken - refresh token for GCP API + * @returns {Date} obj2.accessExpiresAt - date of expiration for access token + */ +const exchangeCodeGCP = async ({ code }: { code: string }) => { + const accessExpiresAt = new Date(); + + const res: ExchangeCodeAzureResponse = ( + await standardRequest.post( + INTEGRATION_GCP_TOKEN_URL, + new URLSearchParams({ + grant_type: "authorization_code", + code: code, + client_id: await getClientIdGCPSecretManager(), + client_secret: await getClientSecretGCPSecretManager(), + redirect_uri: `${await getSiteURL()}/integrations/gcp-secret-manager/oauth2/callback`, + } as any) + ) + ).data; + + accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in); + + return { + accessToken: res.access_token, + refreshToken: res.refresh_token, + accessExpiresAt, + }; +}; + /** * Return [accessToken] for Azure OAuth2 code-token exchange * @param param0 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/models/integration.ts b/backend/src/models/integration.ts index 579a7dbf2..6b22c51b8 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -24,7 +24,8 @@ import { INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_TRAVISCI, INTEGRATION_VERCEL, - INTEGRATION_WINDMILL + INTEGRATION_WINDMILL, + INTEGRATION_GCP_SECRET_MANAGER } from "../variables"; import { Schema, Types, model } from "mongoose"; @@ -70,7 +71,8 @@ export interface IIntegration { | "digital-ocean-app-platform" | "cloud-66" | "northflank" - | "windmill"; + | "windmill" + | "gcp-secret-manager"; integrationAuth: Types.ObjectId; } @@ -167,7 +169,8 @@ const integrationSchema = new Schema( INTEGRATION_BITBUCKET, INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_CLOUD_66, - INTEGRATION_NORTHFLANK + INTEGRATION_NORTHFLANK, + INTEGRATION_GCP_SECRET_MANAGER ], required: true, }, diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index 66a26898f..1ad843f74 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -26,7 +26,8 @@ import { INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_TRAVISCI, INTEGRATION_VERCEL, - INTEGRATION_WINDMILL + INTEGRATION_WINDMILL, + INTEGRATION_GCP_SECRET_MANAGER } from "../variables"; import { Document, Schema, Types, model } from "mongoose"; @@ -58,7 +59,8 @@ export interface IIntegrationAuth extends Document { | "terraform-cloud" | "teamcity" | "northflank" - | "windmill"; + | "windmill" + | "gcp-secret-manager"; teamId: string; accountId: string; url: string; @@ -111,7 +113,8 @@ const integrationAuthSchema = new Schema( INTEGRATION_BITBUCKET, INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_CLOUD_66, - INTEGRATION_NORTHFLANK + INTEGRATION_NORTHFLANK, + INTEGRATION_GCP_SECRET_MANAGER ], required: true, }, diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index b08d2a990..ed69cf5fc 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -1,17 +1,19 @@ import { getClientIdAzure, getClientIdBitBucket, + getClientIdGCPSecretManager, getClientIdGitHub, getClientIdGitLab, getClientIdHeroku, getClientIdNetlify, - getClientSlugVercel, + getClientSlugVercel } from "../config"; // integrations export const INTEGRATION_AZURE_KEY_VAULT = "azure-key-vault"; export const INTEGRATION_AWS_PARAMETER_STORE = "aws-parameter-store"; export const INTEGRATION_AWS_SECRET_MANAGER = "aws-secret-manager"; +export const INTEGRATION_GCP_SECRET_MANAGER = "gcp-secret-manager"; export const INTEGRATION_HEROKU = "heroku"; export const INTEGRATION_VERCEL = "vercel"; export const INTEGRATION_NETLIFY = "netlify"; @@ -36,35 +38,37 @@ export const INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platfor export const INTEGRATION_CLOUD_66 = "cloud-66"; export const INTEGRATION_NORTHFLANK = "northflank"; export const INTEGRATION_SET = new Set([ + INTEGRATION_GCP_SECRET_MANAGER, INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, - INTEGRATION_RENDER, - INTEGRATION_FLYIO, - INTEGRATION_CIRCLECI, - INTEGRATION_LARAVELFORGE, - INTEGRATION_TRAVISCI, - INTEGRATION_TEAMCITY, - INTEGRATION_SUPABASE, - INTEGRATION_CHECKLY, - INTEGRATION_TERRAFORM_CLOUD, - INTEGRATION_HASHICORP_VAULT, - INTEGRATION_CLOUDFLARE_PAGES, - INTEGRATION_CODEFRESH, - INTEGRATION_WINDMILL, - INTEGRATION_BITBUCKET, - INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, - INTEGRATION_CLOUD_66, - INTEGRATION_NORTHFLANK + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_GITLAB, + INTEGRATION_RENDER, + INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, + INTEGRATION_LARAVELFORGE, + INTEGRATION_TRAVISCI, + INTEGRATION_TEAMCITY, + INTEGRATION_SUPABASE, + INTEGRATION_CHECKLY, + INTEGRATION_TERRAFORM_CLOUD, + INTEGRATION_HASHICORP_VAULT, + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CODEFRESH, + INTEGRATION_WINDMILL, + INTEGRATION_BITBUCKET, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, + INTEGRATION_CLOUD_66, + INTEGRATION_NORTHFLANK ]); // integration types export const INTEGRATION_OAUTH2 = "oauth2"; // integration oauth endpoints +export const INTEGRATION_GCP_TOKEN_URL = "https://accounts.google.com/o/oauth2/token"; export const INTEGRATION_AZURE_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; export const INTEGRATION_HEROKU_TOKEN_URL = "https://id.heroku.com/oauth/token"; export const INTEGRATION_VERCEL_TOKEN_URL = @@ -76,6 +80,7 @@ export const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token"; export const INTEGRATION_BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token" // integration apps endpoints +export const INTEGRATION_GCP_API_URL = "https://cloudresourcemanager.googleapis.com"; export const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com"; export const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api"; export const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com"; @@ -97,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 = [ { @@ -272,12 +281,12 @@ export const getIntegrationOptions = async () => { docsLink: "", }, { - name: "Google Cloud Platform", - slug: "gcp", + name: "GCP Secret Manager", + slug: "gcp-secret-manager", image: "Google Cloud Platform.png", - isAvailable: false, - type: "", - clientId: "", + isAvailable: true, + type: "oauth", + clientId: await getClientIdGCPSecretManager(), docsLink: "" }, { 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 4eed75c48..05bb4ee98 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 bcfaf1943..11adf2e7a 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -6,29 +6,30 @@ const integrationSlugNameMapping: Mapping = { "azure-key-vault": "Azure Key Vault", "aws-parameter-store": "AWS Parameter Store", "aws-secret-manager": "AWS Secret Manager", - heroku: "Heroku", - vercel: "Vercel", - netlify: "Netlify", - github: "GitHub", - gitlab: "GitLab", - render: "Render", + "heroku": "Heroku", + "vercel": "Vercel", + "netlify": "Netlify", + "github": "GitHub", + "gitlab": "GitLab", + "render": "Render", "laravel-forge": "Laravel Forge", - railway: "Railway", - flyio: "Fly.io", - circleci: "CircleCI", - travisci: "TravisCI", - supabase: "Supabase", - checkly: "Checkly", + "railway": "Railway", + "flyio": "Fly.io", + "circleci": "CircleCI", + "travisci": "TravisCI", + "supabase": "Supabase", + "checkly": "Checkly", "terraform-cloud": "Terraform Cloud", "teamcity": "TeamCity", "hashicorp-vault": "Vault", "cloudflare-pages": "Cloudflare Pages", "codefresh": "Codefresh", "digital-ocean-app-platform": "Digital Ocean App Platform", - bitbucket: "BitBucket", + "bitbucket": "BitBucket", "cloud-66": "Cloud 66", - northflank: "Northflank", - "windmill": "Windmill" + "northflank": "Northflank", + "windmill": "Windmill", + "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 new file mode 100644 index 000000000..8d5d1ff1b --- /dev/null +++ b/frontend/src/pages/integrations/gcp-secret-manager/create.tsx @@ -0,0 +1,157 @@ +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import queryString from "query-string"; + +import { + useCreateIntegration +} from "@app/hooks/api"; + +import { + Button, + Card, + CardTitle, + FormControl, + Input, + Select, + SelectItem +} from "../../../components/v2"; +import { + useGetIntegrationAuthApps, + useGetIntegrationAuthById +} from "../../../hooks/api/integrationAuth"; +import { useGetWorkspaceById } from "../../../hooks/api/workspace"; + +export default function GCPSecretManagerCreateIntegrationPage() { + const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); + + const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); + + const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); + const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); + const { data: integrationAuthApps } = useGetIntegrationAuthApps({ + integrationAuthId: (integrationAuthId as string) ?? "" + }); + + const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); + const [targetAppId, setTargetAppId] = useState(""); + const [secretPath, setSecretPath] = useState("/"); + + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (workspace) { + setSelectedSourceEnvironment(workspace.environments[0].slug); + } + }, [workspace]); + + useEffect(() => { + if (integrationAuthApps) { + if (integrationAuthApps.length > 0) { + setTargetAppId(integrationAuthApps[0].appId as string); + } else { + setTargetAppId("none"); + } + } + }, [integrationAuthApps]); + + const handleButtonClick = async () => { + try { + setIsLoading(true); + + if (!integrationAuth?._id) return; + + await mutateAsync({ + integrationAuthId: integrationAuth?._id, + isActive: true, + app: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId)?.name ?? null, + appId: targetAppId, + sourceEnvironment: selectedSourceEnvironment, + targetEnvironment: null, + targetEnvironmentId: null, + targetService: null, + targetServiceId: null, + owner: null, + path: null, + region: null, + secretPath + }); + + setIsLoading(false); + router.push(`/integrations/${localStorage.getItem("projectData.id")}`); + } catch (err) { + console.error(err); + } + }; + + return integrationAuth && + workspace && + selectedSourceEnvironment && + integrationAuthApps && + targetAppId ? ( +
+ + GCP Secret Manager Integration + + + + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + + + + + + +
+ ) : ( +
+ ); +} + +GCPSecretManagerCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/pages/integrations/gcp-secret-manager/oauth2/callback.tsx b/frontend/src/pages/integrations/gcp-secret-manager/oauth2/callback.tsx new file mode 100644 index 000000000..cc620af8b --- /dev/null +++ b/frontend/src/pages/integrations/gcp-secret-manager/oauth2/callback.tsx @@ -0,0 +1,45 @@ +import { useEffect } from "react"; +import { useRouter } from "next/router"; +import queryString from "query-string"; + +import { + useAuthorizeIntegration +} from "@app/hooks/api"; + +export default function GCPSecretManagerOAuth2CallbackPage() { + console.log("GCPSecretManagerOAuth2CallbackPage"); + const router = useRouter(); + const { mutateAsync } = useAuthorizeIntegration(); + + const { code, state } = queryString.parse(router.asPath.split("?")[1]); + + useEffect(() => { + (async () => { + try { + // validate state + + console.log("gcp oauth2 callback page"); + console.log("gcp oauth2 callback page code: ", code); + console.log("gcp oauth2 callback page state: ", state); + + if (state !== localStorage.getItem("latestCSRFToken")) return; + localStorage.removeItem("latestCSRFToken"); + const integrationAuth = await mutateAsync({ + workspaceId: localStorage.getItem("projectData.id") as string, + code: code as string, + integration: "gcp-secret-manager" + }); + + console.log("integrationAuth: ", integrationAuth); + + router.push(`/integrations/gcp-secret-manager/create?integrationAuthId=${integrationAuth._id}`); + } catch (err) { + console.error(err); + } + })(); + }, []); + + return
; +} + +GCPSecretManagerOAuth2CallbackPage.requireAuth = true; diff --git a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx index e267c13f5..bff23753a 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx @@ -32,12 +32,16 @@ export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) = export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => { try { + // generate CSRF token for OAuth2 code-token exchange integrations const state = crypto.randomBytes(16).toString("hex"); localStorage.setItem("latestCSRFToken", state); let link = ""; switch (integrationOption.slug) { + case "gcp-secret-manager": + link = `https://accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/cloud-platform&response_type=code&access_type=offline&state=${state}&redirect_uri=${window.location.origin}/integrations/gcp-secret-manager/oauth2/callback&client_id=${integrationOption.clientId}`; + break; case "azure-key-vault": link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`; break; @@ -123,6 +127,7 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => if (link !== "") { window.location.assign(link); } + } catch (err) { console.error(err); }