Merge pull request #3039 from Infisical/feat/gcp-secret-sync
feat: gcp app connections and secret sync
@@ -106,3 +106,6 @@ INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET=
|
|||||||
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY=
|
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY=
|
||||||
INF_APP_CONNECTION_GITHUB_APP_SLUG=
|
INF_APP_CONNECTION_GITHUB_APP_SLUG=
|
||||||
INF_APP_CONNECTION_GITHUB_APP_ID=
|
INF_APP_CONNECTION_GITHUB_APP_ID=
|
||||||
|
|
||||||
|
#gcp app
|
||||||
|
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL=
|
||||||
|
|||||||
@@ -201,6 +201,9 @@ const envSchema = z
|
|||||||
INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()),
|
INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()),
|
||||||
INF_APP_CONNECTION_GITHUB_APP_ID: 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 ----------------------------------------------------------------------------- */
|
||||||
|
|
||||||
CORS_ALLOWED_ORIGINS: zpStr(
|
CORS_ALLOWED_ORIGINS: zpStr(
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export const decryptAsymmetric = ({ ciphertext, nonce, publicKey, privateKey }:
|
|||||||
|
|
||||||
export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64");
|
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 = () => {
|
export const generateAsymmetricKeyPair = () => {
|
||||||
const pair = nacl.box.keyPair();
|
const pair = nacl.box.keyPair();
|
||||||
|
|||||||
@@ -4,18 +4,21 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
|||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
|
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 { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
// can't use discriminated due to multiple schemas for certain apps
|
// can't use discriminated due to multiple schemas for certain apps
|
||||||
const SanitizedAppConnectionSchema = z.union([
|
const SanitizedAppConnectionSchema = z.union([
|
||||||
...SanitizedAwsConnectionSchema.options,
|
...SanitizedAwsConnectionSchema.options,
|
||||||
...SanitizedGitHubConnectionSchema.options
|
...SanitizedGitHubConnectionSchema.options,
|
||||||
|
...SanitizedGcpConnectionSchema.options
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||||
AwsConnectionListItemSchema,
|
AwsConnectionListItemSchema,
|
||||||
GitHubConnectionListItemSchema
|
GitHubConnectionListItemSchema,
|
||||||
|
GcpConnectionListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
||||||
|
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
||||||
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
||||||
|
|
||||||
export * from "./app-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>> =
|
export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> =
|
||||||
{
|
{
|
||||||
[AppConnection.AWS]: registerAwsConnectionRouter,
|
[AppConnection.AWS]: registerAwsConnectionRouter,
|
||||||
[AppConnection.GitHub]: registerGitHubConnectionRouter
|
[AppConnection.GitHub]: registerGitHubConnectionRouter,
|
||||||
|
[AppConnection.GCP]: registerGcpConnectionRouter
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
});
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
|
import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
|
||||||
|
import { registerGcpSyncRouter } from "./gcp-sync-router";
|
||||||
import { registerGitHubSyncRouter } from "./github-sync-router";
|
import { registerGitHubSyncRouter } from "./github-sync-router";
|
||||||
|
|
||||||
export * from "./secret-sync-router";
|
export * from "./secret-sync-router";
|
||||||
|
|
||||||
export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: FastifyZodProvider) => Promise<void>> = {
|
export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: FastifyZodProvider) => Promise<void>> = {
|
||||||
[SecretSync.AWSParameterStore]: registerAwsParameterStoreSyncRouter,
|
[SecretSync.AWSParameterStore]: registerAwsParameterStoreSyncRouter,
|
||||||
[SecretSync.GitHub]: registerGitHubSyncRouter
|
[SecretSync.GitHub]: registerGitHubSyncRouter,
|
||||||
|
[SecretSync.GCPSecretManager]: registerGcpSyncRouter
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,13 +9,19 @@ import {
|
|||||||
AwsParameterStoreSyncListItemSchema,
|
AwsParameterStoreSyncListItemSchema,
|
||||||
AwsParameterStoreSyncSchema
|
AwsParameterStoreSyncSchema
|
||||||
} from "@app/services/secret-sync/aws-parameter-store";
|
} 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";
|
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", [
|
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||||
AwsParameterStoreSyncListItemSchema,
|
AwsParameterStoreSyncListItemSchema,
|
||||||
GitHubSyncListItemSchema
|
GitHubSyncListItemSchema,
|
||||||
|
GcpSyncListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export enum AppConnection {
|
export enum AppConnection {
|
||||||
GitHub = "github",
|
GitHub = "github",
|
||||||
AWS = "aws"
|
AWS = "aws",
|
||||||
|
GCP = "gcp"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AWSRegion {
|
export enum AWSRegion {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { TAppConnections } from "@app/db/schemas/app-connections";
|
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 { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
|
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
|
||||||
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
|
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
|
||||||
@@ -7,6 +8,11 @@ import {
|
|||||||
getAwsAppConnectionListItem,
|
getAwsAppConnectionListItem,
|
||||||
validateAwsConnectionCredentials
|
validateAwsConnectionCredentials
|
||||||
} from "@app/services/app-connection/aws";
|
} from "@app/services/app-connection/aws";
|
||||||
|
import {
|
||||||
|
GcpConnectionMethod,
|
||||||
|
getGcpAppConnectionListItem,
|
||||||
|
validateGcpConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/gcp";
|
||||||
import {
|
import {
|
||||||
getGitHubConnectionListItem,
|
getGitHubConnectionListItem,
|
||||||
GitHubConnectionMethod,
|
GitHubConnectionMethod,
|
||||||
@@ -15,7 +21,9 @@ import {
|
|||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
export const listAppConnectionOptions = () => {
|
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 ({
|
export const encryptAppConnectionCredentials = async ({
|
||||||
@@ -69,6 +77,8 @@ export const validateAppConnectionCredentials = async (
|
|||||||
return validateAwsConnectionCredentials(appConnection);
|
return validateAwsConnectionCredentials(appConnection);
|
||||||
case AppConnection.GitHub:
|
case AppConnection.GitHub:
|
||||||
return validateGitHubConnectionCredentials(appConnection);
|
return validateGitHubConnectionCredentials(appConnection);
|
||||||
|
case AppConnection.GCP:
|
||||||
|
return validateGcpConnectionCredentials(appConnection);
|
||||||
default:
|
default:
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
throw new Error(`Unhandled App Connection ${app}`);
|
throw new Error(`Unhandled App Connection ${app}`);
|
||||||
@@ -85,6 +95,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
return "Access Key";
|
return "Access Key";
|
||||||
case AwsConnectionMethod.AssumeRole:
|
case AwsConnectionMethod.AssumeRole:
|
||||||
return "Assume Role";
|
return "Assume Role";
|
||||||
|
case GcpConnectionMethod.ServiceAccountImpersonation:
|
||||||
|
return "Service Account Impersonation";
|
||||||
default:
|
default:
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||||
@@ -101,6 +113,7 @@ export const decryptAppConnection = async (
|
|||||||
encryptedCredentials: appConnection.encryptedCredentials,
|
encryptedCredentials: appConnection.encryptedCredentials,
|
||||||
orgId: appConnection.orgId,
|
orgId: appConnection.orgId,
|
||||||
kmsService
|
kmsService
|
||||||
})
|
}),
|
||||||
|
credentialsHash: generateHash(appConnection.encryptedCredentials)
|
||||||
} as TAppConnection;
|
} as TAppConnection;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ import { AppConnection } from "./app-connection-enums";
|
|||||||
|
|
||||||
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||||
[AppConnection.AWS]: "AWS",
|
[AppConnection.AWS]: "AWS",
|
||||||
[AppConnection.GitHub]: "GitHub"
|
[AppConnection.GitHub]: "GitHub",
|
||||||
|
[AppConnection.GCP]: "GCP"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
|
|||||||
encryptedCredentials: true,
|
encryptedCredentials: true,
|
||||||
app: true,
|
app: true,
|
||||||
method: true
|
method: true
|
||||||
|
}).extend({
|
||||||
|
credentialsHash: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>
|
export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
|
|||||||
|
|
||||||
import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
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 { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
|
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
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 { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||||
|
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
||||||
|
import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
||||||
|
|
||||||
export type TAppConnectionServiceFactoryDep = {
|
export type TAppConnectionServiceFactoryDep = {
|
||||||
appConnectionDAL: TAppConnectionDALFactory;
|
appConnectionDAL: TAppConnectionDALFactory;
|
||||||
@@ -37,7 +40,8 @@ export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServic
|
|||||||
|
|
||||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
|
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
|
||||||
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
|
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
|
||||||
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema
|
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema,
|
||||||
|
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appConnectionServiceFactory = ({
|
export const appConnectionServiceFactory = ({
|
||||||
@@ -182,6 +186,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...connection,
|
...connection,
|
||||||
|
credentialsHash: generateHash(connection.encryptedCredentials),
|
||||||
credentials: validatedCredentials
|
credentials: validatedCredentials
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -382,6 +387,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
deleteAppConnection,
|
deleteAppConnection,
|
||||||
connectAppConnectionById,
|
connectAppConnectionById,
|
||||||
listAvailableAppConnectionsForUser,
|
listAvailableAppConnectionsForUser,
|
||||||
github: githubConnectionService(connectAppConnectionById)
|
github: githubConnectionService(connectAppConnectionById),
|
||||||
|
gcp: gcpConnectionService(connectAppConnectionById)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import {
|
|||||||
TValidateGitHubConnectionCredentials
|
TValidateGitHubConnectionCredentials
|
||||||
} from "@app/services/app-connection/github";
|
} 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<
|
export type TCreateAppConnectionDTO = Pick<
|
||||||
TAppConnectionInput,
|
TAppConnectionInput,
|
||||||
@@ -24,8 +26,9 @@ export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "met
|
|||||||
connectionId: string;
|
connectionId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig;
|
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig | TGcpConnectionConfig;
|
||||||
|
|
||||||
export type TValidateAppConnectionCredentials =
|
export type TValidateAppConnectionCredentials =
|
||||||
| TValidateAwsConnectionCredentials
|
| TValidateAwsConnectionCredentials
|
||||||
| TValidateGitHubConnectionCredentials;
|
| TValidateGitHubConnectionCredentials
|
||||||
|
| TValidateGcpConnectionCredentials;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export enum GcpConnectionMethod {
|
||||||
|
ServiceAccountImpersonation = "service-account-impersonation"
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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()
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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";
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./gcp-connection-enums";
|
||||||
|
export * from "./gcp-connection-fns";
|
||||||
|
export * from "./gcp-connection-schemas";
|
||||||
|
export * from "./gcp-connection-types";
|
||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export enum GcpSyncScope {
|
||||||
|
Global = "global"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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)
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./gcp-sync-constants";
|
||||||
|
export * from "./gcp-sync-enums";
|
||||||
|
export * from "./gcp-sync-schemas";
|
||||||
|
export * from "./gcp-sync-types";
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export enum SecretSync {
|
export enum SecretSync {
|
||||||
AWSParameterStore = "aws-parameter-store",
|
AWSParameterStore = "aws-parameter-store",
|
||||||
GitHub = "github"
|
GitHub = "github",
|
||||||
|
GCPSecretManager = "gcp-secret-manager"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SecretSyncInitialSyncBehavior {
|
export enum SecretSyncInitialSyncBehavior {
|
||||||
|
|||||||
@@ -13,9 +13,13 @@ import {
|
|||||||
TSecretSyncWithCredentials
|
TSecretSyncWithCredentials
|
||||||
} from "@app/services/secret-sync/secret-sync-types";
|
} 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> = {
|
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||||
[SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
|
[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 = () => {
|
export const listSecretSyncOptions = () => {
|
||||||
@@ -71,6 +75,8 @@ export const SecretSyncFns = {
|
|||||||
return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap);
|
return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap);
|
||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
return GithubSyncFns.syncSecrets(secretSync, secretMap);
|
return GithubSyncFns.syncSecrets(secretSync, secretMap);
|
||||||
|
case SecretSync.GCPSecretManager:
|
||||||
|
return GcpSyncFns.syncSecrets(secretSync, secretMap);
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||||
@@ -86,6 +92,9 @@ export const SecretSyncFns = {
|
|||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
secretMap = await GithubSyncFns.getSecrets(secretSync);
|
secretMap = await GithubSyncFns.getSecrets(secretSync);
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.GCPSecretManager:
|
||||||
|
secretMap = await GcpSyncFns.getSecrets(secretSync);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||||
@@ -103,6 +112,8 @@ export const SecretSyncFns = {
|
|||||||
return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap);
|
return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap);
|
||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
return GithubSyncFns.removeSecrets(secretSync, secretMap);
|
return GithubSyncFns.removeSecrets(secretSync, secretMap);
|
||||||
|
case SecretSync.GCPSecretManager:
|
||||||
|
return GcpSyncFns.removeSecrets(secretSync, secretMap);
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
`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) {
|
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)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
|||||||
|
|
||||||
export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||||
[SecretSync.AWSParameterStore]: "AWS Parameter Store",
|
[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> = {
|
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||||
[SecretSync.AWSParameterStore]: AppConnection.AWS,
|
[SecretSync.AWSParameterStore]: AppConnection.AWS,
|
||||||
[SecretSync.GitHub]: AppConnection.GitHub
|
[SecretSync.GitHub]: AppConnection.GitHub,
|
||||||
|
[SecretSync.GCPSecretManager]: AppConnection.GCP
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,14 +17,18 @@ import {
|
|||||||
TAwsParameterStoreSyncListItem,
|
TAwsParameterStoreSyncListItem,
|
||||||
TAwsParameterStoreSyncWithCredentials
|
TAwsParameterStoreSyncWithCredentials
|
||||||
} from "./aws-parameter-store";
|
} 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 = {
|
export type TSyncOptionsConfig = {
|
||||||
canImportSecrets: boolean;
|
canImportSecrets: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Available"
|
||||||
|
openapi: "GET /api/v1/app-connections/gcp/available"
|
||||||
|
---
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/app-connections/gcp/{connectionId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by ID"
|
||||||
|
openapi: "GET /api/v1/app-connections/gcp/{connectionId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by Name"
|
||||||
|
openapi: "GET /api/v1/app-connections/gcp/connection-name/{connectionName}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v1/app-connections/gcp"
|
||||||
|
---
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v1/secret-syncs/gcp-secret-manager"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/secret-syncs/gcp-secret-manager/{syncId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by ID"
|
||||||
|
openapi: "GET /api/v1/secret-syncs/gcp-secret-manager/{syncId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by Name"
|
||||||
|
openapi: "GET /api/v1/secret-syncs/gcp-secret-manager/sync-name/{syncName}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Import Secrets"
|
||||||
|
openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/import-secrets"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v1/secret-syncs/gcp-secret-manager"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Remove Secrets"
|
||||||
|
openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/remove-secrets"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Sync Secrets"
|
||||||
|
openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/sync-secrets"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Update"
|
||||||
|
openapi: "PATCH /api/v1/secret-syncs/gcp-secret-manager/{syncId}"
|
||||||
|
---
|
||||||
|
After Width: | Height: | Size: 632 KiB |
|
After Width: | Height: | Size: 411 KiB |
|
After Width: | Height: | Size: 519 KiB |
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 978 KiB |
|
After Width: | Height: | Size: 580 KiB |
|
After Width: | Height: | Size: 669 KiB |
|
After Width: | Height: | Size: 395 KiB |
|
After Width: | Height: | Size: 472 KiB |
|
After Width: | Height: | Size: 451 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 624 KiB |
|
After Width: | Height: | Size: 626 KiB |
|
After Width: | Height: | Size: 602 KiB |
|
After Width: | Height: | Size: 664 KiB |
|
After Width: | Height: | Size: 608 KiB |
|
After Width: | Height: | Size: 628 KiB |
@@ -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">
|
||||||
|

|
||||||
|
</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.
|
||||||
|

|
||||||
|
</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.
|
||||||
|

|
||||||
|
</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">
|
||||||
|

|
||||||
|
</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`
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Configure Service Account Permissions">
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Secret Sync">
|
||||||
|
Add the required permissions for secret syncs:
|
||||||
|

|
||||||
|
</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.
|
||||||
|

|
||||||
|
</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. 
|
||||||
|
</Step>
|
||||||
|
<Step title="Add Connection">
|
||||||
|
Select the **GCP Connection** option from the connection options modal.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Authorize Connection">
|
||||||
|
Select the **Service Account Impersonation** method and click **Connect to
|
||||||
|
GCP**. 
|
||||||
|
</Step>
|
||||||
|
<Step title="Connection Created">
|
||||||
|
Your **GCP Connection** is now available for use. 
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
@@ -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
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Infisical UI">
|
||||||
|
1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button.
|
||||||
|

|
||||||
|
|
||||||
|
2. Select the **GCP Secret Manager** option.
|
||||||
|

|
||||||
|
|
||||||
|
3. Configure the **Source** from where secrets should be retrieved, then click **Next**.
|
||||||
|

|
||||||
|
|
||||||
|
- **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**.
|
||||||
|

|
||||||
|
|
||||||
|
- **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**.
|
||||||
|

|
||||||
|
|
||||||
|
- **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**.
|
||||||
|

|
||||||
|
|
||||||
|
- **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**.
|
||||||
|

|
||||||
|
|
||||||
|
8. If enabled, your GCP Secret Manager Sync will begin syncing your secrets to the destination endpoint.
|
||||||
|

|
||||||
|
|
||||||
|
</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>
|
||||||
@@ -352,7 +352,8 @@
|
|||||||
"group": "Connections",
|
"group": "Connections",
|
||||||
"pages": [
|
"pages": [
|
||||||
"integrations/app-connections/aws",
|
"integrations/app-connections/aws",
|
||||||
"integrations/app-connections/github"
|
"integrations/app-connections/github",
|
||||||
|
"integrations/app-connections/gcp"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -365,7 +366,8 @@
|
|||||||
"group": "Syncs",
|
"group": "Syncs",
|
||||||
"pages": [
|
"pages": [
|
||||||
"integrations/secret-syncs/aws-parameter-store",
|
"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": [
|
"pages": [
|
||||||
"api-reference/endpoints/app-connections/list",
|
"api-reference/endpoints/app-connections/list",
|
||||||
"api-reference/endpoints/app-connections/options",
|
"api-reference/endpoints/app-connections/options",
|
||||||
{ "group": "AWS",
|
{
|
||||||
|
"group": "AWS",
|
||||||
"pages": [
|
"pages": [
|
||||||
"api-reference/endpoints/app-connections/aws/list",
|
"api-reference/endpoints/app-connections/aws/list",
|
||||||
"api-reference/endpoints/app-connections/aws/available",
|
"api-reference/endpoints/app-connections/aws/available",
|
||||||
@@ -810,7 +813,8 @@
|
|||||||
"api-reference/endpoints/app-connections/aws/delete"
|
"api-reference/endpoints/app-connections/aws/delete"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ "group": "GitHub",
|
{
|
||||||
|
"group": "GitHub",
|
||||||
"pages": [
|
"pages": [
|
||||||
"api-reference/endpoints/app-connections/github/list",
|
"api-reference/endpoints/app-connections/github/list",
|
||||||
"api-reference/endpoints/app-connections/github/available",
|
"api-reference/endpoints/app-connections/github/available",
|
||||||
@@ -820,6 +824,18 @@
|
|||||||
"api-reference/endpoints/app-connections/github/update",
|
"api-reference/endpoints/app-connections/github/update",
|
||||||
"api-reference/endpoints/app-connections/github/delete"
|
"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": [
|
"pages": [
|
||||||
"api-reference/endpoints/secret-syncs/list",
|
"api-reference/endpoints/secret-syncs/list",
|
||||||
"api-reference/endpoints/secret-syncs/options",
|
"api-reference/endpoints/secret-syncs/options",
|
||||||
{ "group": "AWS Parameter Store",
|
{
|
||||||
|
"group": "AWS Parameter Store",
|
||||||
"pages": [
|
"pages": [
|
||||||
"api-reference/endpoints/secret-syncs/aws-parameter-store/list",
|
"api-reference/endpoints/secret-syncs/aws-parameter-store/list",
|
||||||
"api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-id",
|
"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"
|
"api-reference/endpoints/secret-syncs/aws-parameter-store/remove-secrets"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ "group": "GitHub",
|
{
|
||||||
|
"group": "GitHub",
|
||||||
"pages": [
|
"pages": [
|
||||||
"api-reference/endpoints/secret-syncs/github/list",
|
"api-reference/endpoints/secret-syncs/github/list",
|
||||||
"api-reference/endpoints/secret-syncs/github/get-by-id",
|
"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/sync-secrets",
|
||||||
"api-reference/endpoints/secret-syncs/github/remove-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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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't see the project you'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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
|
|||||||
|
|
||||||
import { TSecretSyncForm } from "../schemas";
|
import { TSecretSyncForm } from "../schemas";
|
||||||
import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields";
|
import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields";
|
||||||
|
import { GcpSyncFields } from "./GcpSyncFields";
|
||||||
import { GitHubSyncFields } from "./GitHubSyncFields";
|
import { GitHubSyncFields } from "./GitHubSyncFields";
|
||||||
|
|
||||||
export const SecretSyncDestinationFields = () => {
|
export const SecretSyncDestinationFields = () => {
|
||||||
@@ -16,6 +17,8 @@ export const SecretSyncDestinationFields = () => {
|
|||||||
return <AwsParameterStoreSyncFields />;
|
return <AwsParameterStoreSyncFields />;
|
||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
return <GitHubSyncFields />;
|
return <GitHubSyncFields />;
|
||||||
|
case SecretSync.GCPSecretManager:
|
||||||
|
return <GcpSyncFields />;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled Destination Config Field: ${destination}`);
|
throw new Error(`Unhandled Destination Config Field: ${destination}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
};
|
||||||
@@ -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 { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields";
|
import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields";
|
||||||
|
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
|
||||||
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
|
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
|
||||||
|
|
||||||
export const SecretSyncReviewFields = () => {
|
export const SecretSyncReviewFields = () => {
|
||||||
@@ -38,6 +39,9 @@ export const SecretSyncReviewFields = () => {
|
|||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
DestinationFieldsComponent = <GitHubSyncReviewFields />;
|
DestinationFieldsComponent = <GitHubSyncReviewFields />;
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.GCPSecretManager:
|
||||||
|
DestinationFieldsComponent = <GcpSyncReviewFields />;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
|
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -5,6 +5,7 @@ import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs";
|
|||||||
import { slugSchema } from "@app/lib/schemas";
|
import { slugSchema } from "@app/lib/schemas";
|
||||||
|
|
||||||
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
|
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
|
||||||
|
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
|
||||||
|
|
||||||
const BaseSecretSyncSchema = z.object({
|
const BaseSecretSyncSchema = z.object({
|
||||||
name: slugSchema({ field: "Name" }),
|
name: slugSchema({ field: "Name" }),
|
||||||
@@ -31,7 +32,8 @@ const BaseSecretSyncSchema = z.object({
|
|||||||
|
|
||||||
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||||
AwsParameterStoreSyncDestinationSchema,
|
AwsParameterStoreSyncDestinationSchema,
|
||||||
GitHubSyncDestinationSchema
|
GitHubSyncDestinationSchema,
|
||||||
|
GcpSyncDestinationSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const SecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema);
|
export const SecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema);
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export const SecretPathInput = ({
|
|||||||
maxHeight: "var(--radix-select-content-available-height)"
|
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) => (
|
{suggestions.map((suggestion, i) => (
|
||||||
<div
|
<div
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
|||||||
@@ -4,13 +4,18 @@ import { faKey, faPassport, faUser } from "@fortawesome/free-solid-svg-icons";
|
|||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import {
|
import {
|
||||||
AwsConnectionMethod,
|
AwsConnectionMethod,
|
||||||
|
GcpConnectionMethod,
|
||||||
GitHubConnectionMethod,
|
GitHubConnectionMethod,
|
||||||
TAppConnection
|
TAppConnection
|
||||||
} from "@app/hooks/api/appConnections/types";
|
} from "@app/hooks/api/appConnections/types";
|
||||||
|
|
||||||
export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: string }> = {
|
export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: string }> = {
|
||||||
[AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" },
|
[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"]) => {
|
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
|
||||||
@@ -23,6 +28,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
|||||||
return { name: "Access Key", icon: faKey };
|
return { name: "Access Key", icon: faKey };
|
||||||
case AwsConnectionMethod.AssumeRole:
|
case AwsConnectionMethod.AssumeRole:
|
||||||
return { name: "Assume Role", icon: faUser };
|
return { name: "Assume Role", icon: faUser };
|
||||||
|
case GcpConnectionMethod.ServiceAccountImpersonation:
|
||||||
|
return { name: "Service Account Impersonation", icon: faUser };
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import {
|
|||||||
|
|
||||||
export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = {
|
export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = {
|
||||||
[SecretSync.AWSParameterStore]: { name: "Parameter Store", image: "Amazon Web Services.png" },
|
[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> = {
|
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||||
[SecretSync.AWSParameterStore]: AppConnection.AWS,
|
[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<
|
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export enum AppConnection {
|
export enum AppConnection {
|
||||||
AWS = "aws",
|
AWS = "aws",
|
||||||
GitHub = "github"
|
GitHub = "github",
|
||||||
|
GCP = "gcp"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export type TGcpProject = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
@@ -16,9 +16,14 @@ export type TGitHubConnectionOption = TAppConnectionOptionBase & {
|
|||||||
appClientSlug?: string;
|
appClientSlug?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGcpConnectionOption = TAppConnectionOptionBase & {
|
||||||
|
app: AppConnection.GCP;
|
||||||
|
};
|
||||||
|
|
||||||
export type TAppConnectionOption = TAwsConnectionOption | TGitHubConnectionOption;
|
export type TAppConnectionOption = TAwsConnectionOption | TGitHubConnectionOption;
|
||||||
|
|
||||||
export type TAppConnectionOptionMap = {
|
export type TAppConnectionOptionMap = {
|
||||||
[AppConnection.AWS]: TAwsConnectionOption;
|
[AppConnection.AWS]: TAwsConnectionOption;
|
||||||
[AppConnection.GitHub]: TGitHubConnectionOption;
|
[AppConnection.GitHub]: TGitHubConnectionOption;
|
||||||
|
[AppConnection.GCP]: TGcpConnectionOption;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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 { TAwsConnection } from "@app/hooks/api/appConnections/types/aws-connection";
|
||||||
import { TGitHubConnection } from "@app/hooks/api/appConnections/types/github-connection";
|
import { TGitHubConnection } from "@app/hooks/api/appConnections/types/github-connection";
|
||||||
|
|
||||||
|
import { TGcpConnection } from "./gcp-connection";
|
||||||
|
|
||||||
export * from "./aws-connection";
|
export * from "./aws-connection";
|
||||||
|
export * from "./gcp-connection";
|
||||||
export * from "./github-connection";
|
export * from "./github-connection";
|
||||||
|
|
||||||
export type TAppConnection = TAwsConnection | TGitHubConnection;
|
export type TAppConnection = TAwsConnection | TGitHubConnection | TGcpConnection;
|
||||||
|
|
||||||
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "app" | "id">;
|
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "app" | "id">;
|
||||||
|
|
||||||
@@ -36,4 +39,5 @@ export type TDeleteAppConnectionDTO = {
|
|||||||
export type TAppConnectionMap = {
|
export type TAppConnectionMap = {
|
||||||
[AppConnection.AWS]: TAwsConnection;
|
[AppConnection.AWS]: TAwsConnection;
|
||||||
[AppConnection.GitHub]: TGitHubConnection;
|
[AppConnection.GitHub]: TGitHubConnection;
|
||||||
|
[AppConnection.GCP]: TGcpConnection;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export enum SecretSync {
|
export enum SecretSync {
|
||||||
AWSParameterStore = "aws-parameter-store",
|
AWSParameterStore = "aws-parameter-store",
|
||||||
GitHub = "github"
|
GitHub = "github",
|
||||||
|
GCPSecretManager = "gcp-secret-manager"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SecretSyncStatus {
|
export enum SecretSyncStatus {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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 { TGitHubSync } from "@app/hooks/api/secretSyncs/types/github-sync";
|
||||||
import { DiscriminativePick } from "@app/types";
|
import { DiscriminativePick } from "@app/types";
|
||||||
|
|
||||||
|
import { TGcpSync } from "./gcp-sync";
|
||||||
|
|
||||||
export type TSecretSyncOption = {
|
export type TSecretSyncOption = {
|
||||||
name: string;
|
name: string;
|
||||||
destination: SecretSync;
|
destination: SecretSync;
|
||||||
canImportSecrets: boolean;
|
canImportSecrets: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync;
|
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync | TGcpSync;
|
||||||
|
|
||||||
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
|
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { DiscriminativePick } from "@app/types";
|
|||||||
|
|
||||||
import { AppConnectionHeader } from "../AppConnectionHeader";
|
import { AppConnectionHeader } from "../AppConnectionHeader";
|
||||||
import { AwsConnectionForm } from "./AwsConnectionForm";
|
import { AwsConnectionForm } from "./AwsConnectionForm";
|
||||||
|
import { GcpConnectionForm } from "./GcpConnectionForm";
|
||||||
import { GitHubConnectionForm } from "./GitHubConnectionForm";
|
import { GitHubConnectionForm } from "./GitHubConnectionForm";
|
||||||
|
|
||||||
type FormProps = {
|
type FormProps = {
|
||||||
@@ -50,6 +51,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
|||||||
return <AwsConnectionForm onSubmit={onSubmit} />;
|
return <AwsConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.GitHub:
|
case AppConnection.GitHub:
|
||||||
return <GitHubConnectionForm />;
|
return <GitHubConnectionForm />;
|
||||||
|
case AppConnection.GCP:
|
||||||
|
return <GcpConnectionForm onSubmit={onSubmit} />;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled App ${app}`);
|
throw new Error(`Unhandled App ${app}`);
|
||||||
}
|
}
|
||||||
@@ -87,6 +90,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
|||||||
return <AwsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
return <AwsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||||
case AppConnection.GitHub:
|
case AppConnection.GitHub:
|
||||||
return <GitHubConnectionForm appConnection={appConnection} />;
|
return <GitHubConnectionForm appConnection={appConnection} />;
|
||||||
|
case AppConnection.GCP:
|
||||||
|
return <GcpConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
|
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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} />;
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
import { AwsParameterStoreSyncDestinationCol } from "./AwsParameterStoreSyncDestinationCol";
|
import { AwsParameterStoreSyncDestinationCol } from "./AwsParameterStoreSyncDestinationCol";
|
||||||
|
import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol";
|
||||||
import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol";
|
import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -13,6 +14,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
|
|||||||
return <AwsParameterStoreSyncDestinationCol secretSync={secretSync} />;
|
return <AwsParameterStoreSyncDestinationCol secretSync={secretSync} />;
|
||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
return <GitHubSyncDestinationCol secretSync={secretSync} />;
|
return <GitHubSyncDestinationCol secretSync={secretSync} />;
|
||||||
|
case SecretSync.GCPSecretManager:
|
||||||
|
return <GcpSyncDestinationCol secretSync={secretSync} />;
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
|
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
|
|||||||
throw new Error(`Unhandled GitHub Scope Destination Col Values ${destination}`);
|
throw new Error(`Unhandled GitHub Scope Destination Col Values ${destination}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.GCPSecretManager:
|
||||||
|
primaryText = destinationConfig.projectId;
|
||||||
|
secondaryText = "Global";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled Destination Col Values ${destination}`);
|
throw new Error(`Unhandled Destination Col Values ${destination}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
};
|
||||||
@@ -12,6 +12,8 @@ import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
|
|||||||
import { AwsParameterStoreSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsParameterStoreSyncDestinationSection";
|
import { AwsParameterStoreSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsParameterStoreSyncDestinationSection";
|
||||||
import { GitHubSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GitHubSyncDestinationSection";
|
import { GitHubSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GitHubSyncDestinationSection";
|
||||||
|
|
||||||
|
import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
secretSync: TSecretSync;
|
secretSync: TSecretSync;
|
||||||
onEditDestination: VoidFunction;
|
onEditDestination: VoidFunction;
|
||||||
@@ -30,6 +32,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
|
|||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
DestinationComponents = <GitHubSyncDestinationSection secretSync={secretSync} />;
|
DestinationComponents = <GitHubSyncDestinationSection secretSync={secretSync} />;
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.GCPSecretManager:
|
||||||
|
DestinationComponents = <GcpSyncDestinationSection secretSync={secretSync} />;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled Destination Section components: ${destination}`);
|
throw new Error(`Unhandled Destination Section components: ${destination}`);
|
||||||
}
|
}
|
||||||
|
|||||||