mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 18:26:16 +00:00
misc: added import support and a few ui/ux updates
This commit is contained in:
@@ -25,7 +25,7 @@ export const getGcpAppConnectionListItem = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAuthToken = async (appConnection: TGcpConnectionConfig) => {
|
export const getGcpConnectionAuthToken = async (appConnection: TGcpConnectionConfig) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
if (!appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) {
|
if (!appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) {
|
||||||
throw new InternalServerError({
|
throw new InternalServerError({
|
||||||
@@ -78,7 +78,7 @@ export const getAuthToken = async (appConnection: TGcpConnectionConfig) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getGcpSecretManagerProjects = async (appConnection: TGcpConnection) => {
|
export const getGcpSecretManagerProjects = async (appConnection: TGcpConnection) => {
|
||||||
const accessToken = await getAuthToken(appConnection);
|
const accessToken = await getGcpConnectionAuthToken(appConnection);
|
||||||
|
|
||||||
let gcpApps: GCPApp[] = [];
|
let gcpApps: GCPApp[] = [];
|
||||||
|
|
||||||
@@ -146,19 +146,19 @@ export const getGcpSecretManagerProjects = async (appConnection: TGcpConnection)
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const validateGcpConnectionCredentials = async (appConnection: TGcpConnectionConfig) => {
|
export const validateGcpConnectionCredentials = async (appConnection: TGcpConnectionConfig) => {
|
||||||
// Check if provided service account email prefix matches organization ID.
|
// Check if provided service account email suffix matches organization ID.
|
||||||
// We do this to mitigate confused deputy attacks in multi-tenant instances
|
// We do this to mitigate confused deputy attacks in multi-tenant instances
|
||||||
const expectedEmailPrefix = appConnection.orgId.split("-").slice(0, 2).join("-");
|
if (appConnection.credentials.serviceAccountEmail) {
|
||||||
if (
|
const expectedAccountIdSuffix = appConnection.orgId.split("-").slice(0, 2).join("-");
|
||||||
appConnection.credentials.serviceAccountEmail &&
|
const serviceAccountId = appConnection.credentials.serviceAccountEmail.split("@")[0];
|
||||||
!appConnection.credentials.serviceAccountEmail.startsWith(expectedEmailPrefix)
|
if (!serviceAccountId.endsWith(expectedAccountIdSuffix)) {
|
||||||
) {
|
throw new BadRequestError({
|
||||||
throw new BadRequestError({
|
message: `GCP service account ID (the part of the email before '@') must have a suffix of "${expectedAccountIdSuffix}"`
|
||||||
message: `GCP service account email must have a prefix of "${expectedEmailPrefix}"`
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await getAuthToken(appConnection);
|
await getGcpConnectionAuthToken(appConnection);
|
||||||
|
|
||||||
return appConnection.credentials;
|
return appConnection.credentials;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ export const GCP_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
|||||||
name: "GCP Secret Manager",
|
name: "GCP Secret Manager",
|
||||||
destination: SecretSync.GCPSecretManager,
|
destination: SecretSync.GCPSecretManager,
|
||||||
connection: AppConnection.GCP,
|
connection: AppConnection.GCP,
|
||||||
canImportSecrets: false
|
canImportSecrets: true
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export enum GcpSyncScope {
|
||||||
|
Global = "global"
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { getAuthToken } from "@app/services/app-connection/gcp";
|
import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp";
|
||||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
|
||||||
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
|
import { SecretSyncError } from "../secret-sync-errors";
|
||||||
import { TSecretMap } from "../secret-sync-types";
|
import { TSecretMap } from "../secret-sync-types";
|
||||||
import {
|
import {
|
||||||
GCPLatestSecretVersionAccess,
|
GCPLatestSecretVersionAccess,
|
||||||
@@ -13,6 +15,8 @@ import {
|
|||||||
} from "./gcp-sync-types";
|
} from "./gcp-sync-types";
|
||||||
|
|
||||||
const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => {
|
const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => {
|
||||||
|
const { destinationConfig } = secretSync;
|
||||||
|
|
||||||
let gcpSecrets: GCPSecret[] = [];
|
let gcpSecrets: GCPSecret[] = [];
|
||||||
|
|
||||||
const pageSize = 100;
|
const pageSize = 100;
|
||||||
@@ -48,21 +52,13 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden
|
|||||||
pageToken = secretsRes.nextPageToken;
|
pageToken = secretsRes.nextPageToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
return gcpSecrets;
|
const res: { [key: string]: string } = {};
|
||||||
};
|
|
||||||
|
|
||||||
export const GcpSyncFns = {
|
for await (const gcpSecret of gcpSecrets) {
|
||||||
syncSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => {
|
const arr = gcpSecret.name.split("/");
|
||||||
const { destinationConfig, connection } = secretSync;
|
const key = arr[arr.length - 1];
|
||||||
const accessToken = await getAuthToken(connection);
|
|
||||||
|
|
||||||
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
|
|
||||||
const res: { [key: string]: string } = {};
|
|
||||||
|
|
||||||
for await (const gcpSecret of gcpSecrets) {
|
|
||||||
const arr = gcpSecret.name.split("/");
|
|
||||||
const key = arr[arr.length - 1];
|
|
||||||
|
|
||||||
|
try {
|
||||||
const { data: secretLatest } = await request.get<GCPLatestSecretVersionAccess>(
|
const { data: secretLatest } = await request.get<GCPLatestSecretVersionAccess>(
|
||||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}/versions/latest:access`,
|
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}/versions/latest:access`,
|
||||||
{
|
{
|
||||||
@@ -74,100 +70,132 @@ export const GcpSyncFns = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
|
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
|
||||||
|
} catch (error) {
|
||||||
|
// when a secret in GCP has no versions, we treat it as if it's a blank value
|
||||||
|
if (error instanceof AxiosError && error.response?.status === 404) {
|
||||||
|
res[key] = "";
|
||||||
|
} else {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GcpSyncFns = {
|
||||||
|
syncSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
|
const { destinationConfig, connection } = secretSync;
|
||||||
|
const accessToken = await getGcpConnectionAuthToken(connection);
|
||||||
|
|
||||||
|
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
|
||||||
|
|
||||||
for await (const key of Object.keys(secretMap)) {
|
for await (const key of Object.keys(secretMap)) {
|
||||||
if (!(key in res)) {
|
try {
|
||||||
// case: create secret
|
if (!(key in gcpSecrets)) {
|
||||||
await request.post(
|
// case: create secret
|
||||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets`,
|
await request.post(
|
||||||
{
|
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets`,
|
||||||
replication: {
|
{
|
||||||
automatic: {}
|
replication: {
|
||||||
}
|
automatic: {}
|
||||||
},
|
}
|
||||||
{
|
|
||||||
params: {
|
|
||||||
secretId: key
|
|
||||||
},
|
},
|
||||||
headers: {
|
{
|
||||||
Authorization: `Bearer ${accessToken}`,
|
params: {
|
||||||
"Accept-Encoding": "application/json"
|
secretId: key
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
);
|
|
||||||
|
|
||||||
if (!secretMap[key].value) {
|
await request.post(
|
||||||
logger.warn(
|
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
|
||||||
`syncSecretsGcpsecretManager: create secret value in gcp where [key=${key}] and [projectId=${destinationConfig.projectId}]`
|
{
|
||||||
|
payload: {
|
||||||
|
data: Buffer.from(secretMap[key].value).toString("base64")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
await request.post(
|
throw new SecretSyncError({
|
||||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
|
error,
|
||||||
{
|
secretKey: key
|
||||||
payload: {
|
});
|
||||||
data: Buffer.from(secretMap[key].value).toString("base64")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
"Accept-Encoding": "application/json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for await (const key of Object.keys(res)) {
|
for await (const key of Object.keys(gcpSecrets)) {
|
||||||
if (!(key in secretMap)) {
|
try {
|
||||||
// case: delete secret
|
if (!(key in secretMap)) {
|
||||||
await request.delete(
|
// case: delete secret
|
||||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
|
await request.delete(
|
||||||
{
|
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
|
||||||
headers: {
|
{
|
||||||
Authorization: `Bearer ${accessToken}`,
|
headers: {
|
||||||
"Accept-Encoding": "application/json"
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
} else if (secretMap[key].value !== gcpSecrets[key]) {
|
||||||
|
if (!secretMap[key].value) {
|
||||||
|
logger.warn(
|
||||||
|
`syncSecretsGcpsecretManager: update secret value in gcp where [key=${key}] and [projectId=${destinationConfig.projectId}]`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
} else if (secretMap[key].value !== res[key]) {
|
await request.post(
|
||||||
if (!secretMap[key].value) {
|
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
|
||||||
logger.warn(
|
{
|
||||||
`syncSecretsGcpsecretManager: update secret value in gcp where [key=${key}] and [projectId=${destinationConfig.projectId}]`
|
payload: {
|
||||||
|
data: Buffer.from(secretMap[key].value).toString("base64")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
await request.post(
|
throw new SecretSyncError({
|
||||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
|
error,
|
||||||
{
|
secretKey: key
|
||||||
payload: {
|
});
|
||||||
data: Buffer.from(secretMap[key].value).toString("base64")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
"Accept-Encoding": "application/json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getSecrets: async (secretSync: TGcpSyncWithCredentials): Promise<TSecretMap> => {
|
getSecrets: async (secretSync: TGcpSyncWithCredentials): Promise<TSecretMap> => {
|
||||||
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
|
const { connection } = secretSync;
|
||||||
|
const accessToken = await getGcpConnectionAuthToken(connection);
|
||||||
|
|
||||||
|
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
|
||||||
|
return Object.fromEntries(Object.entries(gcpSecrets).map(([key, value]) => [key, { value: value ?? "" }]));
|
||||||
},
|
},
|
||||||
|
|
||||||
removeSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => {
|
removeSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
const { destinationConfig, connection } = secretSync;
|
const { destinationConfig, connection } = secretSync;
|
||||||
const accessToken = await getAuthToken(connection);
|
const accessToken = await getGcpConnectionAuthToken(connection);
|
||||||
|
|
||||||
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
|
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
|
||||||
for await (const entry of gcpSecrets) {
|
for await (const [key] of Object.entries(gcpSecrets)) {
|
||||||
const arr = entry.name.split("/");
|
|
||||||
const key = arr[arr.length - 1];
|
|
||||||
if (key in secretMap) {
|
if (key in secretMap) {
|
||||||
await request.delete(
|
await request.delete(
|
||||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
|
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import {
|
|||||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
import { SecretSync } from "../secret-sync-enums";
|
import { SecretSync } from "../secret-sync-enums";
|
||||||
|
import { GcpSyncScope } from "./gcp-sync-enums";
|
||||||
|
|
||||||
const GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
|
const GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||||
|
|
||||||
const GcpSyncDestinationConfigSchema = z.object({
|
const GcpSyncDestinationConfigSchema = z.object({
|
||||||
|
scope: z.literal(GcpSyncScope.Global),
|
||||||
projectId: z.string().min(1, "Project ID is required")
|
projectId: z.string().min(1, "Project ID is required")
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,5 +41,5 @@ export const GcpSyncListItemSchema = z.object({
|
|||||||
name: z.literal("GCP Secret Manager"),
|
name: z.literal("GCP Secret Manager"),
|
||||||
connection: z.literal(AppConnection.GCP),
|
connection: z.literal(AppConnection.GCP),
|
||||||
destination: z.literal(SecretSync.GCPSecretManager),
|
destination: z.literal(SecretSync.GCPSecretManager),
|
||||||
canImportSecrets: z.literal(false)
|
canImportSecrets: z.literal(true)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./gcp-sync-constants";
|
export * from "./gcp-sync-constants";
|
||||||
|
export * from "./gcp-sync-enums";
|
||||||
export * from "./gcp-sync-schemas";
|
export * from "./gcp-sync-schemas";
|
||||||
export * from "./gcp-sync-types";
|
export * from "./gcp-sync-types";
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export const parseSyncErrorMessage = (err: unknown): string => {
|
|||||||
if (err instanceof SecretSyncError) {
|
if (err instanceof SecretSyncError) {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
secretKey: err.secretKey,
|
secretKey: err.secretKey,
|
||||||
error: err.message ?? parseSyncErrorMessage(err.error)
|
error: err.message || parseSyncErrorMessage(err.error)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Import Secrets"
|
||||||
|
openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/import-secrets"
|
||||||
|
---
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 610 KiB After Width: | Height: | Size: 632 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 416 KiB After Width: | Height: | Size: 380 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 685 KiB After Width: | Height: | Size: 602 KiB |
@@ -40,11 +40,11 @@ Infisical supports [service account impersonation](https://cloud.google.com/iam/
|
|||||||
<Step title="Create Service Account">
|
<Step title="Create Service Account">
|
||||||
Create a new service account with an ID that follows this requirement:
|
Create a new service account with an ID that follows this requirement:
|
||||||
|
|
||||||
Your service account ID must start with the first two sections of your Infisical organization ID.
|
Your service account ID must end with the first two sections of your Infisical organization ID.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
- Infisical organization ID: `df92581a-0fe9-42b5-b526-0a1e88ec8085`
|
- Infisical organization ID: `df92581a-0fe9-42b5-b526-0a1e88ec8085`
|
||||||
- Required service account ID prefix: `df92581a-0fe9`
|
- Required service account ID suffix: `df92581a-0fe9`
|
||||||
|
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
|
|||||||
|
|
||||||
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||||
- Create a [GCP Connection](/integrations/app-connections/gcp) with the required **Secret Sync** permissions
|
- Create a [GCP Connection](/integrations/app-connections/gcp) with the required **Secret Sync** permissions
|
||||||
- Enable Cloud Manager Resource API and Secret Manager API on your GCP project
|
- Enable **Cloud Resource Manager API** and **Secret Manager API** on your GCP project
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
@@ -40,6 +40,8 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
|
|||||||
|
|
||||||
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
|
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
|
||||||
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
||||||
|
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Infisical if secrets conflict.
|
||||||
|
- **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in GCP secret manager if secrets conflict.
|
||||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||||
|
|
||||||
6. Configure the **Details** of your GCP Secret Manager Sync, then click **Next**.
|
6. Configure the **Details** of your GCP Secret Manager Sync, then click **Next**.
|
||||||
@@ -67,6 +69,7 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
|
|||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
--data '{
|
--data '{
|
||||||
"destinationConfig": {
|
"destinationConfig": {
|
||||||
|
"scope": "global",
|
||||||
"projectId": "infisical-test-playground"
|
"projectId": "infisical-test-playground"
|
||||||
},
|
},
|
||||||
"name": "my-gcp-sync",
|
"name": "my-gcp-sync",
|
||||||
|
|||||||
@@ -880,6 +880,7 @@
|
|||||||
"api-reference/endpoints/secret-syncs/gcp-secret-manager/update",
|
"api-reference/endpoints/secret-syncs/gcp-secret-manager/update",
|
||||||
"api-reference/endpoints/secret-syncs/gcp-secret-manager/delete",
|
"api-reference/endpoints/secret-syncs/gcp-secret-manager/delete",
|
||||||
"api-reference/endpoints/secret-syncs/gcp-secret-manager/sync-secrets",
|
"api-reference/endpoints/secret-syncs/gcp-secret-manager/sync-secrets",
|
||||||
|
"api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets",
|
||||||
"api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets"
|
"api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-2
@@ -1,11 +1,15 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||||
import { SingleValue } from "react-select";
|
import { SingleValue } from "react-select";
|
||||||
|
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||||
import { FilterableSelect, FormControl } from "@app/components/v2";
|
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
|
||||||
import { useGcpConnectionListProjects } from "@app/hooks/api/appConnections/gcp/queries";
|
import { useGcpConnectionListProjects } from "@app/hooks/api/appConnections/gcp/queries";
|
||||||
import { TGitHubConnectionEnvironment } from "@app/hooks/api/appConnections/github";
|
import { TGitHubConnectionEnvironment } from "@app/hooks/api/appConnections/github";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||||
|
|
||||||
import { TSecretSyncForm } from "../schemas";
|
import { TSecretSyncForm } from "../schemas";
|
||||||
|
|
||||||
@@ -20,6 +24,10 @@ export const GcpSyncFields = () => {
|
|||||||
enabled: Boolean(connectionId)
|
enabled: Boolean(connectionId)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue("destinationConfig.scope", GcpSyncScope.Global);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SecretSyncConnectionField
|
<SecretSyncConnectionField
|
||||||
@@ -31,7 +39,22 @@ export const GcpSyncFields = () => {
|
|||||||
name="destinationConfig.projectId"
|
name="destinationConfig.projectId"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Project">
|
<FormControl
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Project"
|
||||||
|
helperText={
|
||||||
|
<Tooltip
|
||||||
|
className="max-w-md"
|
||||||
|
content="Ensure that you've enabled the Secret Manager API and Cloud Resource Manager API on your GCP project. Additionally, make sure that the service account is assigned the appropriate GCP roles."
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span>Don't see the project you're looking for?</span>{" "}
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
<FilterableSelect
|
<FilterableSelect
|
||||||
menuPlacement="top"
|
menuPlacement="top"
|
||||||
isLoading={isPending && Boolean(connectionId)}
|
isLoading={isPending && Boolean(connectionId)}
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/hel
|
|||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields";
|
import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields";
|
||||||
import { GcpSyncReviewFields } from "./GcpSyncReviewFIelds";
|
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
|
||||||
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
|
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
|
||||||
|
|
||||||
export const SecretSyncReviewFields = () => {
|
export const SecretSyncReviewFields = () => {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||||
|
|
||||||
export const GcpSyncDestinationSchema = z.object({
|
export const GcpSyncDestinationSchema = z.object({
|
||||||
destination: z.literal(SecretSync.GCPSecretManager),
|
destination: z.literal(SecretSync.GCPSecretManager),
|
||||||
destinationConfig: z.object({
|
destinationConfig: z.object({
|
||||||
|
scope: z.literal(GcpSyncScope.Global),
|
||||||
projectId: z.string().min(1, "Project ID required")
|
projectId: z.string().min(1, "Project ID required")
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,9 +2,14 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
|||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||||
|
|
||||||
|
export enum GcpSyncScope {
|
||||||
|
Global = "global"
|
||||||
|
}
|
||||||
|
|
||||||
export type TGcpSync = TRootSecretSync & {
|
export type TGcpSync = TRootSecretSync & {
|
||||||
destination: SecretSync.GCPSecretManager;
|
destination: SecretSync.GCPSecretManager;
|
||||||
destinationConfig: {
|
destinationConfig: {
|
||||||
|
scope: GcpSyncScope.Global;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
};
|
};
|
||||||
connection: {
|
connection: {
|
||||||
|
|||||||
+1
-1
@@ -103,7 +103,7 @@ export const GcpConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
isError={Boolean(error?.message)}
|
isError={Boolean(error?.message)}
|
||||||
label="Service Account Email"
|
label="Service Account Email"
|
||||||
className="group"
|
className="group"
|
||||||
helperText={`Service account email must be prefixed with "${currentOrg.id.split("-").slice(0, 2).join("-")}".`}
|
helperText={`Service account ID (the part of the email before '@') must be suffixed with "${currentOrg.id.split("-").slice(0, 2).join("-")}".`}
|
||||||
>
|
>
|
||||||
<SecretInput
|
<SecretInput
|
||||||
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||||
|
|||||||
Reference in New Issue
Block a user