Merge pull request #902 from Infisical/gcp-integration

GCP Secret Manager Integration
This commit is contained in:
BlackMagiq
2023-08-26 17:42:59 +01:00
committed by GitHub
17 changed files with 621 additions and 50 deletions

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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<string, { value: string; comment?: string }>;
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

View File

@@ -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<IIntegration>(
INTEGRATION_BITBUCKET,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CLOUD_66,
INTEGRATION_NORTHFLANK
INTEGRATION_NORTHFLANK,
INTEGRATION_GCP_SECRET_MANAGER
],
required: true,
},

View File

@@ -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<IIntegrationAuth>(
INTEGRATION_BITBUCKET,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CLOUD_66,
INTEGRATION_NORTHFLANK
INTEGRATION_NORTHFLANK,
INTEGRATION_GCP_SECRET_MANAGER
],
required: true,
},

View File

@@ -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: ""
},
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -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)
<Info>
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.
</Info>
## 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)
<Warning>
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).
</Warning>

View File

@@ -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 |

View File

@@ -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",

View File

@@ -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 = {

View File

@@ -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 ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">GCP Secret Manager Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="GCP Project">
<Select
value={targetAppId}
onValueChange={(val) => setTargetAppId(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.appId as string}
key={`target-app-${integrationAuthApp.appId}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
GCPSecretManagerCreateIntegrationPage.requireAuth = true;

View File

@@ -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 <div />;
}
GCPSecretManagerOAuth2CallbackPage.requireAuth = true;

View File

@@ -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);
}