Finish GCP secret manager integration

This commit is contained in:
Tuan Dang
2023-08-26 17:36:20 +01:00
parent 63af7d4a15
commit a07ddb806d
11 changed files with 271 additions and 58 deletions

View File

@@ -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;
};
/**

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

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

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

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

View File

@@ -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 ? (
<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">
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
@@ -117,29 +115,29 @@ export default function GCPSecretManagerCreateIntegrationPage() {
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Heroku App" className="mt-4">
<FormControl label="GCP Project">
<Select
value={targetApp}
onValueChange={(val) => 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) => (
<SelectItem
value={integrationAuthApp.name}
key={`target-app-${integrationAuthApp.name}`}
value={integrationAuthApp.appId as string}
key={`target-app-${integrationAuthApp.appId}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No apps found
No projects found
</SelectItem>
)}
</Select>
</FormControl> */}
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"