Merge pull request #3039 from Infisical/feat/gcp-secret-sync

feat: gcp app connections and secret sync
This commit is contained in:
Sheen
2025-01-29 02:23:22 +08:00
committed by GitHub
94 changed files with 1555 additions and 41 deletions

View File

@@ -106,3 +106,6 @@ INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET=
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY=
INF_APP_CONNECTION_GITHUB_APP_SLUG=
INF_APP_CONNECTION_GITHUB_APP_ID=
#gcp app
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL=

View File

@@ -201,6 +201,9 @@ const envSchema = z
INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()),
INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional()),
// gcp app
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL: zpStr(z.string().optional()),
/* CORS ----------------------------------------------------------------------------- */
CORS_ALLOWED_ORIGINS: zpStr(

View File

@@ -116,7 +116,7 @@ export const decryptAsymmetric = ({ ciphertext, nonce, publicKey, privateKey }:
export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64");
export const generateHash = (value: string) => crypto.createHash("sha256").update(value).digest("hex");
export const generateHash = (value: string | Buffer) => crypto.createHash("sha256").update(value).digest("hex");
export const generateAsymmetricKeyPair = () => {
const pair = nacl.box.keyPair();

View File

@@ -4,18 +4,21 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
import { AuthMode } from "@app/services/auth/auth-type";
// can't use discriminated due to multiple schemas for certain apps
const SanitizedAppConnectionSchema = z.union([
...SanitizedAwsConnectionSchema.options,
...SanitizedGitHubConnectionSchema.options
...SanitizedGitHubConnectionSchema.options,
...SanitizedGcpConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
AwsConnectionListItemSchema,
GitHubConnectionListItemSchema
GitHubConnectionListItemSchema,
GcpConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@@ -0,0 +1,48 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateGcpConnectionSchema,
SanitizedGcpConnectionSchema,
UpdateGcpConnectionSchema
} from "@app/services/app-connection/gcp";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerGcpConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.GCP,
server,
sanitizedResponseSchema: SanitizedGcpConnectionSchema,
createSchema: CreateGcpConnectionSchema,
updateSchema: UpdateGcpConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/secret-manager-projects`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({ id: z.string(), name: z.string() }).array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const projects = await server.services.appConnection.gcp.listSecretManagerProjects(connectionId, req.permission);
return projects;
}
});
};

View File

@@ -1,6 +1,7 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { registerAwsConnectionRouter } from "./aws-connection-router";
import { registerGcpConnectionRouter } from "./gcp-connection-router";
import { registerGitHubConnectionRouter } from "./github-connection-router";
export * from "./app-connection-router";
@@ -8,5 +9,6 @@ export * from "./app-connection-router";
export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> =
{
[AppConnection.AWS]: registerAwsConnectionRouter,
[AppConnection.GitHub]: registerGitHubConnectionRouter
[AppConnection.GitHub]: registerGitHubConnectionRouter,
[AppConnection.GCP]: registerGcpConnectionRouter
};

View File

@@ -0,0 +1,13 @@
import { CreateGcpSyncSchema, GcpSyncSchema, UpdateGcpSyncSchema } from "@app/services/secret-sync/gcp";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerGcpSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.GCPSecretManager,
server,
responseSchema: GcpSyncSchema,
createSchema: CreateGcpSyncSchema,
updateSchema: UpdateGcpSyncSchema
});

View File

@@ -1,11 +1,13 @@
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
import { registerGcpSyncRouter } from "./gcp-sync-router";
import { registerGitHubSyncRouter } from "./github-sync-router";
export * from "./secret-sync-router";
export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: FastifyZodProvider) => Promise<void>> = {
[SecretSync.AWSParameterStore]: registerAwsParameterStoreSyncRouter,
[SecretSync.GitHub]: registerGitHubSyncRouter
[SecretSync.GitHub]: registerGitHubSyncRouter,
[SecretSync.GCPSecretManager]: registerGcpSyncRouter
};

View File

@@ -9,13 +9,19 @@ import {
AwsParameterStoreSyncListItemSchema,
AwsParameterStoreSyncSchema
} from "@app/services/secret-sync/aws-parameter-store";
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
const SecretSyncSchema = z.discriminatedUnion("destination", [AwsParameterStoreSyncSchema, GitHubSyncSchema]);
const SecretSyncSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncSchema,
GitHubSyncSchema,
GcpSyncSchema
]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncListItemSchema,
GitHubSyncListItemSchema
GitHubSyncListItemSchema,
GcpSyncListItemSchema
]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

View File

@@ -1,6 +1,7 @@
export enum AppConnection {
GitHub = "github",
AWS = "aws"
AWS = "aws",
GCP = "gcp"
}
export enum AWSRegion {

View File

@@ -1,4 +1,5 @@
import { TAppConnections } from "@app/db/schemas/app-connections";
import { generateHash } from "@app/lib/crypto/encryption";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
@@ -7,6 +8,11 @@ import {
getAwsAppConnectionListItem,
validateAwsConnectionCredentials
} from "@app/services/app-connection/aws";
import {
GcpConnectionMethod,
getGcpAppConnectionListItem,
validateGcpConnectionCredentials
} from "@app/services/app-connection/gcp";
import {
getGitHubConnectionListItem,
GitHubConnectionMethod,
@@ -15,7 +21,9 @@ import {
import { KmsDataKey } from "@app/services/kms/kms-types";
export const listAppConnectionOptions = () => {
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem()].sort((a, b) => a.name.localeCompare(b.name));
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem(), getGcpAppConnectionListItem()].sort((a, b) =>
a.name.localeCompare(b.name)
);
};
export const encryptAppConnectionCredentials = async ({
@@ -69,6 +77,8 @@ export const validateAppConnectionCredentials = async (
return validateAwsConnectionCredentials(appConnection);
case AppConnection.GitHub:
return validateGitHubConnectionCredentials(appConnection);
case AppConnection.GCP:
return validateGcpConnectionCredentials(appConnection);
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection ${app}`);
@@ -85,6 +95,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
return "Access Key";
case AwsConnectionMethod.AssumeRole:
return "Assume Role";
case GcpConnectionMethod.ServiceAccountImpersonation:
return "Service Account Impersonation";
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`);
@@ -101,6 +113,7 @@ export const decryptAppConnection = async (
encryptedCredentials: appConnection.encryptedCredentials,
orgId: appConnection.orgId,
kmsService
})
}),
credentialsHash: generateHash(appConnection.encryptedCredentials)
} as TAppConnection;
};

View File

@@ -2,5 +2,6 @@ import { AppConnection } from "./app-connection-enums";
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.AWS]: "AWS",
[AppConnection.GitHub]: "GitHub"
[AppConnection.GitHub]: "GitHub",
[AppConnection.GCP]: "GCP"
};

View File

@@ -10,6 +10,8 @@ export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
encryptedCredentials: true,
app: true,
method: true
}).extend({
credentialsHash: z.string().optional()
});
export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>

View File

@@ -2,6 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { generateHash } from "@app/lib/crypto/encryption";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
@@ -26,6 +27,8 @@ import { githubConnectionService } from "@app/services/app-connection/github/git
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TAppConnectionDALFactory } from "./app-connection-dal";
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
import { gcpConnectionService } from "./gcp/gcp-connection-service";
export type TAppConnectionServiceFactoryDep = {
appConnectionDAL: TAppConnectionDALFactory;
@@ -37,7 +40,8 @@ export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServic
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema,
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -182,6 +186,7 @@ export const appConnectionServiceFactory = ({
return {
...connection,
credentialsHash: generateHash(connection.encryptedCredentials),
credentials: validatedCredentials
};
});
@@ -382,6 +387,7 @@ export const appConnectionServiceFactory = ({
deleteAppConnection,
connectAppConnectionById,
listAvailableAppConnectionsForUser,
github: githubConnectionService(connectAppConnectionById)
github: githubConnectionService(connectAppConnectionById),
gcp: gcpConnectionService(connectAppConnectionById)
};
};

View File

@@ -11,9 +11,11 @@ import {
TValidateGitHubConnectionCredentials
} from "@app/services/app-connection/github";
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection);
import { TGcpConnection, TGcpConnectionConfig, TGcpConnectionInput, TValidateGcpConnectionCredentials } from "./gcp";
export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput);
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection | TGcpConnection);
export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput | TGcpConnectionInput);
export type TCreateAppConnectionDTO = Pick<
TAppConnectionInput,
@@ -24,8 +26,9 @@ export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "met
connectionId: string;
};
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig;
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig | TGcpConnectionConfig;
export type TValidateAppConnectionCredentials =
| TValidateAwsConnectionCredentials
| TValidateGitHubConnectionCredentials;
| TValidateGitHubConnectionCredentials
| TValidateGcpConnectionCredentials;

View File

@@ -0,0 +1,3 @@
export enum GcpConnectionMethod {
ServiceAccountImpersonation = "service-account-impersonation"
}

View File

@@ -0,0 +1,164 @@
import { gaxios, Impersonated, JWT } from "google-auth-library";
import { GetAccessTokenResponse } from "google-auth-library/build/src/auth/oauth2client";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { AppConnection } from "../app-connection-enums";
import { getAppConnectionMethodName } from "../app-connection-fns";
import { GcpConnectionMethod } from "./gcp-connection-enums";
import {
GCPApp,
GCPGetProjectsRes,
GCPGetServiceRes,
TGcpConnection,
TGcpConnectionConfig
} from "./gcp-connection-types";
export const getGcpAppConnectionListItem = () => {
return {
name: "GCP" as const,
app: AppConnection.GCP as const,
methods: Object.values(GcpConnectionMethod) as [GcpConnectionMethod.ServiceAccountImpersonation]
};
};
export const getGcpConnectionAuthToken = async (appConnection: TGcpConnectionConfig) => {
const appCfg = getConfig();
if (!appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) {
throw new InternalServerError({
message: `Environment variables have not been configured for GCP ${getAppConnectionMethodName(
GcpConnectionMethod.ServiceAccountImpersonation
)}`
});
}
const credJson = JSON.parse(appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) as {
client_email: string;
private_key: string;
};
const sourceClient = new JWT({
email: credJson.client_email,
key: credJson.private_key,
scopes: ["https://www.googleapis.com/auth/cloud-platform"]
});
const impersonatedCredentials = new Impersonated({
sourceClient,
targetPrincipal: appConnection.credentials.serviceAccountEmail,
lifetime: 3600,
delegates: [],
targetScopes: ["https://www.googleapis.com/auth/cloud-platform"]
});
let tokenResponse: GetAccessTokenResponse | undefined;
try {
tokenResponse = await impersonatedCredentials.getAccessToken();
} catch (error) {
let message = "Unable to validate connection";
if (error instanceof gaxios.GaxiosError) {
message = error.message;
}
throw new BadRequestError({
message
});
}
if (!tokenResponse || !tokenResponse.token) {
throw new BadRequestError({
message: `Unable to validate connection`
});
}
return tokenResponse.token;
};
export const getGcpSecretManagerProjects = async (appConnection: TGcpConnection) => {
const accessToken = await getGcpConnectionAuthToken(appConnection);
let gcpApps: GCPApp[] = [];
const pageSize = 100;
let pageToken: string | undefined;
let hasMorePages = true;
const projects: {
name: string;
id: string;
}[] = [];
while (hasMorePages) {
const params = new URLSearchParams({
pageSize: String(pageSize),
...(pageToken ? { pageToken } : {})
});
// eslint-disable-next-line no-await-in-loop
const { data } = await request.get<GCPGetProjectsRes>(`${IntegrationUrls.GCP_API_URL}/v1/projects`, {
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
});
gcpApps = gcpApps.concat(data.projects);
if (!data.nextPageToken) {
hasMorePages = false;
}
pageToken = data.nextPageToken;
}
// eslint-disable-next-line
for await (const gcpApp of gcpApps) {
try {
const res = (
await request.get<GCPGetServiceRes>(
`${IntegrationUrls.GCP_SERVICE_USAGE_URL}/v1/projects/${gcpApp.projectId}/services/${IntegrationUrls.GCP_SECRET_MANAGER_SERVICE_NAME}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
)
).data;
if (res.state === "ENABLED") {
projects.push({
name: gcpApp.name,
id: gcpApp.projectId
});
}
} catch {
// eslint-disable-next-line
continue;
}
}
return projects;
};
export const validateGcpConnectionCredentials = async (appConnection: TGcpConnectionConfig) => {
// Check if provided service account email suffix matches organization ID.
// We do this to mitigate confused deputy attacks in multi-tenant instances
if (appConnection.credentials.serviceAccountEmail) {
const expectedAccountIdSuffix = appConnection.orgId.split("-").slice(0, 2).join("-");
const serviceAccountId = appConnection.credentials.serviceAccountEmail.split("@")[0];
if (!serviceAccountId.endsWith(expectedAccountIdSuffix)) {
throw new BadRequestError({
message: `GCP service account ID (the part of the email before '@') must have a suffix of "${expectedAccountIdSuffix}"`
});
}
}
await getGcpConnectionAuthToken(appConnection);
return appConnection.credentials;
};

View File

@@ -0,0 +1,65 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { GcpConnectionMethod } from "./gcp-connection-enums";
export const GcpConnectionServiceAccountImpersonationCredentialsSchema = z.object({
serviceAccountEmail: z.string().email().trim().min(1, "Service account email required")
});
const BaseGcpConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GCP) });
export const GcpConnectionSchema = z.intersection(
BaseGcpConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(GcpConnectionMethod.ServiceAccountImpersonation),
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema
})
])
);
export const SanitizedGcpConnectionSchema = z.discriminatedUnion("method", [
BaseGcpConnectionSchema.extend({
method: z.literal(GcpConnectionMethod.ServiceAccountImpersonation),
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.pick({})
})
]);
export const ValidateGcpConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(GcpConnectionMethod.ServiceAccountImpersonation)
.describe(AppConnections?.CREATE(AppConnection.GCP).method),
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.GCP).credentials
)
})
]);
export const CreateGcpConnectionSchema = ValidateGcpConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.GCP)
);
export const UpdateGcpConnectionSchema = z
.object({
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.GCP).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GCP));
export const GcpConnectionListItemSchema = z.object({
name: z.literal("GCP"),
app: z.literal(AppConnection.GCP),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]),
methods: z.nativeEnum(GcpConnectionMethod).array()
});

View File

@@ -0,0 +1,29 @@
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { getGcpSecretManagerProjects } from "./gcp-connection-fns";
import { TGcpConnection } from "./gcp-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TGcpConnection>;
export const gcpConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listSecretManagerProjects = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.GCP, connectionId, actor);
try {
const projects = await getGcpSecretManagerProjects(appConnection);
return projects;
} catch (error) {
return [];
}
};
return {
listSecretManagerProjects
};
};

View File

@@ -0,0 +1,45 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateGcpConnectionSchema,
GcpConnectionSchema,
ValidateGcpConnectionCredentialsSchema
} from "./gcp-connection-schemas";
export type TGcpConnection = z.infer<typeof GcpConnectionSchema>;
export type TGcpConnectionInput = z.infer<typeof CreateGcpConnectionSchema> & {
app: AppConnection.GCP;
};
export type TValidateGcpConnectionCredentials = typeof ValidateGcpConnectionCredentialsSchema;
export type TGcpConnectionConfig = DiscriminativePick<TGcpConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};
export type 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;
};
};
export type GCPGetProjectsRes = {
projects: GCPApp[];
nextPageToken?: string;
};
export type GCPGetServiceRes = {
name: string;
parent: string;
state: "ENABLED" | "DISABLED" | "STATE_UNSPECIFIED";
};

View File

@@ -0,0 +1,4 @@
export * from "./gcp-connection-enums";
export * from "./gcp-connection-fns";
export * from "./gcp-connection-schemas";
export * from "./gcp-connection-types";

View File

@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const GCP_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "GCP Secret Manager",
destination: SecretSync.GCPSecretManager,
connection: AppConnection.GCP,
canImportSecrets: true
};

View File

@@ -0,0 +1,3 @@
export enum GcpSyncScope {
Global = "global"
}

View File

@@ -0,0 +1,218 @@
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { logger } from "@app/lib/logger";
import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { SecretSyncError } from "../secret-sync-errors";
import { TSecretMap } from "../secret-sync-types";
import {
GCPLatestSecretVersionAccess,
GCPSecret,
GCPSMListSecretsRes,
TGcpSyncWithCredentials
} from "./gcp-sync-types";
const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => {
const { destinationConfig } = secretSync;
let gcpSecrets: GCPSecret[] = [];
const pageSize = 100;
let pageToken: string | undefined;
let hasMorePages = true;
while (hasMorePages) {
const params = new URLSearchParams({
pageSize: String(pageSize),
...(pageToken ? { pageToken } : {})
});
// eslint-disable-next-line no-await-in-loop
const { data: secretsRes } = await request.get<GCPSMListSecretsRes>(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${secretSync.destinationConfig.projectId}/secrets`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
if (secretsRes.secrets) {
gcpSecrets = gcpSecrets.concat(secretsRes.secrets);
}
if (!secretsRes.nextPageToken) {
hasMorePages = false;
}
pageToken = secretsRes.nextPageToken;
}
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>(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}/versions/latest:access`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
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)) {
try {
// we do not process secrets with no value because GCP secret manager does not allow it
if (!secretMap[key].value) {
// eslint-disable-next-line no-continue
continue;
}
if (!(key in gcpSecrets)) {
// case: create secret
await request.post(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets`,
{
replication: {
automatic: {}
}
},
{
params: {
secretId: key
},
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
await request.post(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
{
payload: {
data: Buffer.from(secretMap[key].value).toString("base64")
}
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
for await (const key of Object.keys(gcpSecrets)) {
try {
if (!(key in secretMap) || !secretMap[key].value) {
// case: delete secret
await request.delete(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
{
headers: {
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}]`
);
}
await request.post(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
{
payload: {
data: Buffer.from(secretMap[key].value).toString("base64")
}
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
},
getSecrets: async (secretSync: TGcpSyncWithCredentials): Promise<TSecretMap> => {
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) => {
const { destinationConfig, connection } = secretSync;
const accessToken = await getGcpConnectionAuthToken(connection);
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
for await (const [key] of Object.entries(gcpSecrets)) {
if (key in secretMap) {
await request.delete(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
}
}
}
};

View File

@@ -0,0 +1,45 @@
import z from "zod";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
import { SecretSync } from "../secret-sync-enums";
import { GcpSyncScope } from "./gcp-sync-enums";
const GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
const GcpSyncDestinationConfigSchema = z.object({
scope: z.literal(GcpSyncScope.Global),
projectId: z.string().min(1, "Project ID is required")
});
export const GcpSyncSchema = BaseSecretSyncSchema(SecretSync.GCPSecretManager, GcpSyncOptionsConfig).extend({
destination: z.literal(SecretSync.GCPSecretManager),
destinationConfig: GcpSyncDestinationConfigSchema
});
export const CreateGcpSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.GCPSecretManager,
GcpSyncOptionsConfig
).extend({
destinationConfig: GcpSyncDestinationConfigSchema
});
export const UpdateGcpSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.GCPSecretManager,
GcpSyncOptionsConfig
).extend({
destinationConfig: GcpSyncDestinationConfigSchema.optional()
});
export const GcpSyncListItemSchema = z.object({
name: z.literal("GCP Secret Manager"),
connection: z.literal(AppConnection.GCP),
destination: z.literal(SecretSync.GCPSecretManager),
canImportSecrets: z.literal(true)
});

View File

@@ -0,0 +1,33 @@
import z from "zod";
import { TGcpConnection } from "@app/services/app-connection/gcp";
import { CreateGcpSyncSchema, GcpSyncListItemSchema, GcpSyncSchema } from "./gcp-sync-schemas";
export type TGcpSyncListItem = z.infer<typeof GcpSyncListItemSchema>;
export type TGcpSync = z.infer<typeof GcpSyncSchema>;
export type TGcpSyncInput = z.infer<typeof CreateGcpSyncSchema>;
export type TGcpSyncWithCredentials = TGcpSync & {
connection: TGcpConnection;
};
export type GCPSecret = {
name: string;
createTime: string;
};
export type GCPSMListSecretsRes = {
secrets?: GCPSecret[];
totalSize?: number;
nextPageToken?: string;
};
export type GCPLatestSecretVersionAccess = {
name: string;
payload: {
data: string;
};
};

View File

@@ -0,0 +1,4 @@
export * from "./gcp-sync-constants";
export * from "./gcp-sync-enums";
export * from "./gcp-sync-schemas";
export * from "./gcp-sync-types";

View File

@@ -1,6 +1,7 @@
export enum SecretSync {
AWSParameterStore = "aws-parameter-store",
GitHub = "github"
GitHub = "github",
GCPSecretManager = "gcp-secret-manager"
}
export enum SecretSyncInitialSyncBehavior {

View File

@@ -13,9 +13,13 @@ import {
TSecretSyncWithCredentials
} from "@app/services/secret-sync/secret-sync-types";
import { GCP_SYNC_LIST_OPTION } from "./gcp";
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
[SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION
[SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION,
[SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION
};
export const listSecretSyncOptions = () => {
@@ -71,6 +75,8 @@ export const SecretSyncFns = {
return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap);
case SecretSync.GitHub:
return GithubSyncFns.syncSecrets(secretSync, secretMap);
case SecretSync.GCPSecretManager:
return GcpSyncFns.syncSecrets(secretSync, secretMap);
default:
throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -86,6 +92,9 @@ export const SecretSyncFns = {
case SecretSync.GitHub:
secretMap = await GithubSyncFns.getSecrets(secretSync);
break;
case SecretSync.GCPSecretManager:
secretMap = await GcpSyncFns.getSecrets(secretSync);
break;
default:
throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -103,6 +112,8 @@ export const SecretSyncFns = {
return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap);
case SecretSync.GitHub:
return GithubSyncFns.removeSecrets(secretSync, secretMap);
case SecretSync.GCPSecretManager:
return GcpSyncFns.removeSecrets(secretSync, secretMap);
default:
throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -115,7 +126,7 @@ export const parseSyncErrorMessage = (err: unknown): string => {
if (err instanceof SecretSyncError) {
return JSON.stringify({
secretKey: err.secretKey,
error: err.message ?? parseSyncErrorMessage(err.error)
error: err.message || parseSyncErrorMessage(err.error)
});
}

View File

@@ -3,10 +3,12 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.AWSParameterStore]: "AWS Parameter Store",
[SecretSync.GitHub]: "GitHub"
[SecretSync.GitHub]: "GitHub",
[SecretSync.GCPSecretManager]: "GCP Secret Manager"
};
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.AWSParameterStore]: AppConnection.AWS,
[SecretSync.GitHub]: AppConnection.GitHub
[SecretSync.GitHub]: AppConnection.GitHub,
[SecretSync.GCPSecretManager]: AppConnection.GCP
};

View File

@@ -17,14 +17,18 @@ import {
TAwsParameterStoreSyncListItem,
TAwsParameterStoreSyncWithCredentials
} from "./aws-parameter-store";
import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp";
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync;
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync | TGcpSync;
export type TSecretSyncWithCredentials = TAwsParameterStoreSyncWithCredentials | TGitHubSyncWithCredentials;
export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials
| TGitHubSyncWithCredentials
| TGcpSyncWithCredentials;
export type TSecretSyncInput = TAwsParameterStoreSyncInput | TGitHubSyncInput;
export type TSecretSyncInput = TAwsParameterStoreSyncInput | TGitHubSyncInput | TGcpSyncInput;
export type TSecretSyncListItem = TAwsParameterStoreSyncListItem | TGitHubSyncListItem;
export type TSecretSyncListItem = TAwsParameterStoreSyncListItem | TGitHubSyncListItem | TGcpSyncListItem;
export type TSyncOptionsConfig = {
canImportSecrets: boolean;

View File

@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/gcp/available"
---

View File

@@ -0,0 +1,10 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/gcp"
---
<Note>
Check out the configuration docs for [GCP
Connections](/integrations/app-connections/gcp) to learn how to obtain the
required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/gcp/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/gcp/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/gcp/connection-name/{connectionName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/gcp"
---

View File

@@ -0,0 +1,10 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/gcp/{connectionId}"
---
<Note>
Check out the configuration docs for [GCP
Connections](/integrations/app-connections/gcp) to learn how to obtain the
required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/secret-syncs/gcp-secret-manager"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/secret-syncs/gcp-secret-manager/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/secret-syncs/gcp-secret-manager/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/secret-syncs/gcp-secret-manager/sync-name/{syncName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Import Secrets"
openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/import-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/secret-syncs/gcp-secret-manager"
---

View File

@@ -0,0 +1,4 @@
---
title: "Remove Secrets"
openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/remove-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Sync Secrets"
openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/sync-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/secret-syncs/gcp-secret-manager/{syncId}"
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 KiB

View File

@@ -0,0 +1,88 @@
---
title: "GCP Connection"
description: "Learn how to configure a GCP Connection for Infisical."
---
Infisical supports [service account impersonation](https://cloud.google.com/iam/docs/service-account-impersonation) to connect with your GCP projects.
<Accordion title="Self-Hosted Instance">
Using the GCP integration on a self-hosted instance of Infisical requires configuring a service account on GCP and
configuring your instance to use it.
<Steps>
<Step title="Navigate to IAM & Admin > Service Accounts in Google Cloud Console">
![Service Account Page](/images/app-connections/gcp/service-account-overview.png)
</Step>
<Step title="Create a Service Account">
Create a new service account that will be used to impersonate other GCP service accounts for your app connections.
![Service Account Page](/images/app-connections/gcp/create-instance-service-account.png)
</Step>
<Step title="Generate Service Account Key">
Download the JSON key file for your service account. This will be used to authenticate your instance with GCP.
![Service Account Page](/images/app-connections/gcp/create-service-account-credential.png)
</Step>
<Step title="Configure Your Instance">
1. Copy the entire contents of the downloaded JSON key file.
2. Set it as a string value for the `INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL` environment variable.
3. Restart your Infisical instance to apply the changes.
4. You can now use GCP integration with service account impersonation.
</Step>
</Steps>
</Accordion>
## Configure Service Account for Infisical
<Steps>
<Step title="Navigate to IAM & Admin > Service Accounts in Google Cloud Console">
![Service Account Page](/images/app-connections/gcp/service-account-overview.png)
</Step>
<Step title="Create Service Account">
Create a new service account with an ID that follows this requirement:
Your service account ID must end with the first two sections of your Infisical organization ID.
Example:
- Infisical organization ID: `df92581a-0fe9-42b5-b526-0a1e88ec8085`
- Required service account ID suffix: `df92581a-0fe9`
![Create Service Account](/images/app-connections/gcp/create-service-account.png)
</Step>
<Step title="Configure Service Account Permissions">
<Tabs>
<Tab title="Secret Sync">
Add the required permissions for secret syncs:
![Assign Service Account Permission](/images/app-connections/gcp/service-account-secret-sync-permission.png)
</Tab>
</Tabs>
</Step>
<Step title="Enable Service Account Impersonation">
On the new service account, assign the `Service Account Token Creator` role to the Infisical instance's service account. This allows Infisical to impersonate the new service account.
![Service Account Page](/images/app-connections/gcp/service-account-grant-access.png)
</Step>
</Steps>
## Setup GCP Connection in Infisical
<Steps>
<Step title="Navigate to the App Connections">
Navigate to the **App Connections** tab on the **Organization Settings**
page. ![App Connections
Tab](/images/app-connections/general/add-connection.png)
</Step>
<Step title="Add Connection">
Select the **GCP Connection** option from the connection options modal.
![Select GCP
Connection](/images/app-connections/gcp/select-gcp-connection.png)
</Step>
<Step title="Authorize Connection">
Select the **Service Account Impersonation** method and click **Connect to
GCP**. ![Connect via GCP
impersonation](/images/app-connections/gcp/create-gcp-impersonation-method.png)
</Step>
<Step title="Connection Created">
Your **GCP Connection** is now available for use. ![Impersonation GCP
Connection](/images/app-connections/gcp/gcp-app-impersonation-connection.png)
</Step>
</Steps>

View File

@@ -0,0 +1,141 @@
---
title: "GCP Secret Manager Sync"
description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
---
**Prerequisites:**
- 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
- Enable **Cloud Resource Manager API** and **Secret Manager API** on your GCP project
![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-resource-manager-api.png)
![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-secret-manager-api.png)
<Tabs>
<Tab title="Infisical UI">
1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button.
![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png)
2. Select the **GCP Secret Manager** option.
![Select GCP Secret Manager](/images/secret-syncs/gcp-secret-manager/select-gcp-secret-manager-option.png)
3. Configure the **Source** from where secrets should be retrieved, then click **Next**.
![Configure Source](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-source.png)
- **Environment**: The project environment to retrieve secrets from.
- **Secret Path**: The folder path to retrieve secrets from.
<Tip>
If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports).
</Tip>
4. Configure the **Destination** to where secrets should be deployed, then click **Next**.
![Configure Destination](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-destination.png)
- **GCP Connection**: The GCP Connection to authenticate with.
- **Project**: The GCP project to sync with.
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
![Configure Options](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-options.png)
- **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.
- **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.
6. Configure the **Details** of your GCP Secret Manager Sync, then click **Next**.
![Configure Details](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-details.png)
- **Name**: The name of your sync. Must be slug-friendly.
- **Description**: An optional description for your sync.
7. Review your Secret Manager Sync configuration, then click **Create Sync**.
![Confirm Configuration](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-review.png)
8. If enabled, your GCP Secret Manager Sync will begin syncing your secrets to the destination endpoint.
![Sync Secrets](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-created.png)
</Tab>
<Tab title="API">
To create a **GCP Secret Manager Sync**, make an API request to the [Create GCP
Secret Manager Sync](/api-reference/endpoints/secret-syncs/gcp-secret-manager/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/secret-syncs/gcp-secret-manager \
--header 'Content-Type: application/json' \
--data '{
"destinationConfig": {
"scope": "global",
"projectId": "infisical-test-playground"
},
"name": "my-gcp-sync",
"description": "this is an example secret sync",
"secretPath": "/",
"syncOptions": {
"initialSyncBehavior": "overwrite-destination"
},
"isAutoSyncEnabled": true,
"connectionId": "eec83609-5eb4-4d8d-9f6e-ded016984f0d",
"environment": "dev",
"projectId": "09eda1f8-85a3-47a9-8a6f-e27f133b2a36"
}'
```
### Sample response
```bash Response
{
"secretSync": {
"id": "aee02c4a-4a5f-488c-82dd-0b3164772871",
"name": "my-gcp-sync",
"description": "this is an example secret sync",
"isAutoSyncEnabled": true,
"version": 1,
"projectId": "09eda1f8-85a3-47a9-8a6f-e27f133b2a36",
"folderId": "1447389e-16fb-49ba-96fd-361b5a2522af",
"connectionId": "eec83609-5eb4-4d8d-9f6e-ded016984f0d",
"createdAt": "2025-01-27T12:28:59.408Z",
"updatedAt": "2025-01-27T12:28:59.408Z",
"syncStatus": "pending",
"lastSyncJobId": null,
"lastSyncMessage": null,
"lastSyncedAt": null,
"importStatus": null,
"lastImportJobId": null,
"lastImportMessage": null,
"lastImportedAt": null,
"removeStatus": null,
"lastRemoveJobId": null,
"lastRemoveMessage": null,
"lastRemovedAt": null,
"syncOptions": {
"initialSyncBehavior": "overwrite-destination"
},
"connection": {
"app": "gcp",
"name": "my-gcp-connection",
"id": "eec83609-5eb4-4d8d-9f6e-ded016984f0d"
},
"environment": {
"slug": "dev",
"name": "Development",
"id": "124e0392-4070-4b1c-900e-ced30cd55bf3"
},
"folder": {
"id": "1447389e-16fb-49ba-96fd-361b5a2522af",
"path": "/"
},
"destination": "gcp-secret-manager",
"destinationConfig": {
"projectId": "infisical-test-playground"
}
}
}
```
</Tab>
</Tabs>

View File

@@ -352,7 +352,8 @@
"group": "Connections",
"pages": [
"integrations/app-connections/aws",
"integrations/app-connections/github"
"integrations/app-connections/github",
"integrations/app-connections/gcp"
]
}
]
@@ -365,7 +366,8 @@
"group": "Syncs",
"pages": [
"integrations/secret-syncs/aws-parameter-store",
"integrations/secret-syncs/github"
"integrations/secret-syncs/github",
"integrations/secret-syncs/gcp-secret-manager"
]
}
]
@@ -799,7 +801,8 @@
"pages": [
"api-reference/endpoints/app-connections/list",
"api-reference/endpoints/app-connections/options",
{ "group": "AWS",
{
"group": "AWS",
"pages": [
"api-reference/endpoints/app-connections/aws/list",
"api-reference/endpoints/app-connections/aws/available",
@@ -810,7 +813,8 @@
"api-reference/endpoints/app-connections/aws/delete"
]
},
{ "group": "GitHub",
{
"group": "GitHub",
"pages": [
"api-reference/endpoints/app-connections/github/list",
"api-reference/endpoints/app-connections/github/available",
@@ -820,6 +824,18 @@
"api-reference/endpoints/app-connections/github/update",
"api-reference/endpoints/app-connections/github/delete"
]
},
{
"group": "GCP",
"pages": [
"api-reference/endpoints/app-connections/gcp/list",
"api-reference/endpoints/app-connections/gcp/available",
"api-reference/endpoints/app-connections/gcp/get-by-id",
"api-reference/endpoints/app-connections/gcp/get-by-name",
"api-reference/endpoints/app-connections/gcp/create",
"api-reference/endpoints/app-connections/gcp/update",
"api-reference/endpoints/app-connections/gcp/delete"
]
}
]
},
@@ -828,7 +844,8 @@
"pages": [
"api-reference/endpoints/secret-syncs/list",
"api-reference/endpoints/secret-syncs/options",
{ "group": "AWS Parameter Store",
{
"group": "AWS Parameter Store",
"pages": [
"api-reference/endpoints/secret-syncs/aws-parameter-store/list",
"api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-id",
@@ -841,7 +858,8 @@
"api-reference/endpoints/secret-syncs/aws-parameter-store/remove-secrets"
]
},
{ "group": "GitHub",
{
"group": "GitHub",
"pages": [
"api-reference/endpoints/secret-syncs/github/list",
"api-reference/endpoints/secret-syncs/github/get-by-id",
@@ -852,6 +870,20 @@
"api-reference/endpoints/secret-syncs/github/sync-secrets",
"api-reference/endpoints/secret-syncs/github/remove-secrets"
]
},
{
"group": "GCP Secret Manager",
"pages": [
"api-reference/endpoints/secret-syncs/gcp-secret-manager/list",
"api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-id",
"api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-name",
"api-reference/endpoints/secret-syncs/gcp-secret-manager/create",
"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/sync-secrets",
"api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets",
"api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets"
]
}
]
},

View File

@@ -0,0 +1,76 @@
import { useEffect } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
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 { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
import { useGcpConnectionListProjects } from "@app/hooks/api/appConnections/gcp/queries";
import { TGitHubConnectionEnvironment } from "@app/hooks/api/appConnections/github";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
import { TSecretSyncForm } from "../schemas";
export const GcpSyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.GCPSecretManager }
>();
const connectionId = useWatch({ name: "connection.id", control });
const { data: projects, isPending } = useGcpConnectionListProjects(connectionId, {
enabled: Boolean(connectionId)
});
useEffect(() => {
setValue("destinationConfig.scope", GcpSyncScope.Global);
}, []);
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.projectId", "");
}}
/>
<Controller
name="destinationConfig.projectId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<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&#39;t see the project you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={projects?.find((project) => project.id === value) ?? null}
onChange={(option) =>
onChange((option as SingleValue<TGitHubConnectionEnvironment>)?.id ?? null)
}
options={projects}
placeholder="Select a GCP project..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id.toString()}
/>
</FormControl>
)}
/>
</>
);
};

View File

@@ -4,6 +4,7 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields";
import { GcpSyncFields } from "./GcpSyncFields";
import { GitHubSyncFields } from "./GitHubSyncFields";
export const SecretSyncDestinationFields = () => {
@@ -16,6 +17,8 @@ export const SecretSyncDestinationFields = () => {
return <AwsParameterStoreSyncFields />;
case SecretSync.GitHub:
return <GitHubSyncFields />;
case SecretSync.GCPSecretManager:
return <GcpSyncFields />;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}

View File

@@ -0,0 +1,14 @@
import { useFormContext } from "react-hook-form";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const GcpSyncReviewFields = () => {
const { watch } = useFormContext<
TSecretSyncForm & { destination: SecretSync.GCPSecretManager }
>();
const projectId = watch("destinationConfig.projectId");
return <SecretSyncLabel label="Project ID">{projectId}</SecretSyncLabel>;
};

View File

@@ -8,6 +8,7 @@ import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/hel
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields";
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
export const SecretSyncReviewFields = () => {
@@ -38,6 +39,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.GitHub:
DestinationFieldsComponent = <GitHubSyncReviewFields />;
break;
case SecretSync.GCPSecretManager:
DestinationFieldsComponent = <GcpSyncReviewFields />;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}

View File

@@ -0,0 +1,12 @@
import { z } from "zod";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
export const GcpSyncDestinationSchema = z.object({
destination: z.literal(SecretSync.GCPSecretManager),
destinationConfig: z.object({
scope: z.literal(GcpSyncScope.Global),
projectId: z.string().min(1, "Project ID required")
})
});

View File

@@ -5,6 +5,7 @@ import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs";
import { slugSchema } from "@app/lib/schemas";
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
const BaseSecretSyncSchema = z.object({
name: slugSchema({ field: "Name" }),
@@ -31,7 +32,8 @@ const BaseSecretSyncSchema = z.object({
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncDestinationSchema,
GitHubSyncDestinationSchema
GitHubSyncDestinationSchema,
GcpSyncDestinationSchema
]);
export const SecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema);

View File

@@ -141,7 +141,7 @@ export const SecretPathInput = ({
maxHeight: "var(--radix-select-content-available-height)"
}}
>
<div className="h-full w-full max-w-60 flex-col items-center justify-center rounded-md text-white">
<div className="h-full w-full flex-col items-center justify-center rounded-md text-white">
{suggestions.map((suggestion, i) => (
<div
tabIndex={0}

View File

@@ -4,13 +4,18 @@ import { faKey, faPassport, faUser } from "@fortawesome/free-solid-svg-icons";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
AwsConnectionMethod,
GcpConnectionMethod,
GitHubConnectionMethod,
TAppConnection
} from "@app/hooks/api/appConnections/types";
export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: string }> = {
[AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" },
[AppConnection.GitHub]: { name: "GitHub", image: "GitHub.png" }
[AppConnection.GitHub]: { name: "GitHub", image: "GitHub.png" },
[AppConnection.GCP]: {
name: "GCP",
image: "Google Cloud Platform.png"
}
};
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
@@ -23,6 +28,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
return { name: "Access Key", icon: faKey };
case AwsConnectionMethod.AssumeRole:
return { name: "Assume Role", icon: faUser };
case GcpConnectionMethod.ServiceAccountImpersonation:
return { name: "Service Account Impersonation", icon: faUser };
default:
throw new Error(`Unhandled App Connection Method: ${method}`);
}

View File

@@ -7,12 +7,14 @@ import {
export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = {
[SecretSync.AWSParameterStore]: { name: "Parameter Store", image: "Amazon Web Services.png" },
[SecretSync.GitHub]: { name: "GitHub", image: "GitHub.png" }
[SecretSync.GitHub]: { name: "GitHub", image: "GitHub.png" },
[SecretSync.GCPSecretManager]: { name: "GCP Secret Manager", image: "Google Cloud Platform.png" }
};
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.AWSParameterStore]: AppConnection.AWS,
[SecretSync.GitHub]: AppConnection.GitHub
[SecretSync.GitHub]: AppConnection.GitHub,
[SecretSync.GCPSecretManager]: AppConnection.GCP
};
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<

View File

@@ -1,4 +1,5 @@
export enum AppConnection {
AWS = "aws",
GitHub = "github"
GitHub = "github",
GCP = "gcp"
}

View File

@@ -0,0 +1,37 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "../queries";
import { TGcpProject } from "./types";
const gcpConnectionKeys = {
all: [...appConnectionKeys.all, "gcp"] as const,
listProjects: (connectionId: string) =>
[...gcpConnectionKeys.all, "projects", connectionId] as const
};
export const useGcpConnectionListProjects = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TGcpProject[],
unknown,
TGcpProject[],
ReturnType<typeof gcpConnectionKeys.listProjects>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: gcpConnectionKeys.listProjects(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TGcpProject[]>(
`/api/v1/app-connections/gcp/${connectionId}/secret-manager-projects`
);
return data;
},
...options
});
};

View File

@@ -0,0 +1,4 @@
export type TGcpProject = {
id: string;
name: string;
};

View File

@@ -16,9 +16,14 @@ export type TGitHubConnectionOption = TAppConnectionOptionBase & {
appClientSlug?: string;
};
export type TGcpConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.GCP;
};
export type TAppConnectionOption = TAwsConnectionOption | TGitHubConnectionOption;
export type TAppConnectionOptionMap = {
[AppConnection.AWS]: TAwsConnectionOption;
[AppConnection.GitHub]: TGitHubConnectionOption;
[AppConnection.GCP]: TGcpConnectionOption;
};

View File

@@ -0,0 +1,13 @@
import { AppConnection } from "../enums";
import { TRootAppConnection } from "./root-connection";
export enum GcpConnectionMethod {
ServiceAccountImpersonation = "service-account-impersonation"
}
export type TGcpConnection = TRootAppConnection & { app: AppConnection.GCP } & {
method: GcpConnectionMethod.ServiceAccountImpersonation;
credentials: {
serviceAccountEmail: string;
};
};

View File

@@ -3,10 +3,13 @@ import { TAppConnectionOption } from "@app/hooks/api/appConnections/types/app-op
import { TAwsConnection } from "@app/hooks/api/appConnections/types/aws-connection";
import { TGitHubConnection } from "@app/hooks/api/appConnections/types/github-connection";
import { TGcpConnection } from "./gcp-connection";
export * from "./aws-connection";
export * from "./gcp-connection";
export * from "./github-connection";
export type TAppConnection = TAwsConnection | TGitHubConnection;
export type TAppConnection = TAwsConnection | TGitHubConnection | TGcpConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "app" | "id">;
@@ -36,4 +39,5 @@ export type TDeleteAppConnectionDTO = {
export type TAppConnectionMap = {
[AppConnection.AWS]: TAwsConnection;
[AppConnection.GitHub]: TGitHubConnection;
[AppConnection.GCP]: TGcpConnection;
};

View File

@@ -1,6 +1,7 @@
export enum SecretSync {
AWSParameterStore = "aws-parameter-store",
GitHub = "github"
GitHub = "github",
GCPSecretManager = "gcp-secret-manager"
}
export enum SecretSyncStatus {

View File

@@ -0,0 +1,20 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export enum GcpSyncScope {
Global = "global"
}
export type TGcpSync = TRootSecretSync & {
destination: SecretSync.GCPSecretManager;
destinationConfig: {
scope: GcpSyncScope.Global;
projectId: string;
};
connection: {
app: AppConnection.GCP;
name: string;
id: string;
};
};

View File

@@ -3,13 +3,15 @@ import { TAwsParameterStoreSync } from "@app/hooks/api/secretSyncs/types/aws-par
import { TGitHubSync } from "@app/hooks/api/secretSyncs/types/github-sync";
import { DiscriminativePick } from "@app/types";
import { TGcpSync } from "./gcp-sync";
export type TSecretSyncOption = {
name: string;
destination: SecretSync;
canImportSecrets: boolean;
};
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync;
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync | TGcpSync;
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };

View File

@@ -10,6 +10,7 @@ import { DiscriminativePick } from "@app/types";
import { AppConnectionHeader } from "../AppConnectionHeader";
import { AwsConnectionForm } from "./AwsConnectionForm";
import { GcpConnectionForm } from "./GcpConnectionForm";
import { GitHubConnectionForm } from "./GitHubConnectionForm";
type FormProps = {
@@ -50,6 +51,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <AwsConnectionForm onSubmit={onSubmit} />;
case AppConnection.GitHub:
return <GitHubConnectionForm />;
case AppConnection.GCP:
return <GcpConnectionForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled App ${app}`);
}
@@ -87,6 +90,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <AwsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.GitHub:
return <GitHubConnectionForm appConnection={appConnection} />;
case AppConnection.GCP:
return <GcpConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}

View File

@@ -0,0 +1,178 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
IconButton,
ModalClose,
SecretInput,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { useToggle } from "@app/hooks";
import { GcpConnectionMethod, TGcpConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TGcpConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.GCP)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(GcpConnectionMethod.ServiceAccountImpersonation),
credentials: z.object({
serviceAccountEmail: z.string().email().trim().min(1, "Service account email required")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const GcpConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.GCP,
method: GcpConnectionMethod.ServiceAccountImpersonation
}
});
const { currentOrg } = useOrganization();
const [isCopied, { timedToggle: toggleIsCopied }] = useToggle(false);
const expectedAccountIdSuffix = currentOrg.id.split("-").slice(0, 2).join("-");
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.GCP].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(GcpConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.serviceAccountEmail"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Service Account Email"
className="group"
helperText={
<>
<span>
{`Service account ID (the part of the email before '@') must be suffixed with "${expectedAccountIdSuffix}"`}
</span>
<Tooltip className="relative right-2" position="bottom" content="Copy">
<IconButton
variant="plain"
ariaLabel="copy"
onClick={() => {
if (isCopied) {
return;
}
navigator.clipboard.writeText(expectedAccountIdSuffix);
createNotification({
text: "Copied to clipboard",
type: "info"
});
toggleIsCopied(2000);
}}
className="hover:bg-bunker-100/10"
>
<FontAwesomeIcon
icon={!isCopied ? faCopy : faCheck}
size="sm"
className="cursor-pointer"
/>
</IconButton>
</Tooltip>
</>
}
>
<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"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to GCP"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};

View File

@@ -0,0 +1,14 @@
import { TGcpSync } from "@app/hooks/api/secretSyncs/types/gcp-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TGcpSync;
};
export const GcpSyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};

View File

@@ -1,6 +1,7 @@
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
import { AwsParameterStoreSyncDestinationCol } from "./AwsParameterStoreSyncDestinationCol";
import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol";
import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol";
type Props = {
@@ -13,6 +14,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <AwsParameterStoreSyncDestinationCol secretSync={secretSync} />;
case SecretSync.GitHub:
return <GitHubSyncDestinationCol secretSync={secretSync} />;
case SecretSync.GCPSecretManager:
return <GcpSyncDestinationCol secretSync={secretSync} />;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`

View File

@@ -39,6 +39,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
throw new Error(`Unhandled GitHub Scope Destination Col Values ${destination}`);
}
break;
case SecretSync.GCPSecretManager:
primaryText = destinationConfig.projectId;
secondaryText = "Global";
break;
default:
throw new Error(`Unhandled Destination Col Values ${destination}`);
}

View File

@@ -0,0 +1,14 @@
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { TGcpSync } from "@app/hooks/api/secretSyncs/types/gcp-sync";
type Props = {
secretSync: TGcpSync;
};
export const GcpSyncDestinationSection = ({ secretSync }: Props) => {
const {
destinationConfig: { projectId }
} = secretSync;
return <SecretSyncLabel label="Project ID">{projectId}</SecretSyncLabel>;
};

View File

@@ -12,6 +12,8 @@ import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
import { AwsParameterStoreSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsParameterStoreSyncDestinationSection";
import { GitHubSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GitHubSyncDestinationSection";
import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection";
type Props = {
secretSync: TSecretSync;
onEditDestination: VoidFunction;
@@ -30,6 +32,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.GitHub:
DestinationComponents = <GitHubSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.GCPSecretManager:
DestinationComponents = <GcpSyncDestinationSection secretSync={secretSync} />;
break;
default:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}