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_SLUG=
|
||||
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_ID: zpStr(z.string().optional()),
|
||||
|
||||
// gcp app
|
||||
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL: zpStr(z.string().optional()),
|
||||
|
||||
/* CORS ----------------------------------------------------------------------------- */
|
||||
|
||||
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 generateHash = (value: string) => crypto.createHash("sha256").update(value).digest("hex");
|
||||
export const generateHash = (value: string | Buffer) => crypto.createHash("sha256").update(value).digest("hex");
|
||||
|
||||
export const generateAsymmetricKeyPair = () => {
|
||||
const pair = nacl.box.keyPair();
|
||||
|
||||
@@ -4,18 +4,21 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
|
||||
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
|
||||
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
// can't use discriminated due to multiple schemas for certain apps
|
||||
const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedAwsConnectionSchema.options,
|
||||
...SanitizedGitHubConnectionSchema.options
|
||||
...SanitizedGitHubConnectionSchema.options,
|
||||
...SanitizedGcpConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
AwsConnectionListItemSchema,
|
||||
GitHubConnectionListItemSchema
|
||||
GitHubConnectionListItemSchema,
|
||||
GcpConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -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 { registerAwsConnectionRouter } from "./aws-connection-router";
|
||||
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
||||
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
||||
|
||||
export * from "./app-connection-router";
|
||||
@@ -8,5 +9,6 @@ export * from "./app-connection-router";
|
||||
export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> =
|
||||
{
|
||||
[AppConnection.AWS]: registerAwsConnectionRouter,
|
||||
[AppConnection.GitHub]: registerGitHubConnectionRouter
|
||||
[AppConnection.GitHub]: registerGitHubConnectionRouter,
|
||||
[AppConnection.GCP]: registerGcpConnectionRouter
|
||||
};
|
||||
|
||||
@@ -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 { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
|
||||
import { registerGcpSyncRouter } from "./gcp-sync-router";
|
||||
import { registerGitHubSyncRouter } from "./github-sync-router";
|
||||
|
||||
export * from "./secret-sync-router";
|
||||
|
||||
export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: FastifyZodProvider) => Promise<void>> = {
|
||||
[SecretSync.AWSParameterStore]: registerAwsParameterStoreSyncRouter,
|
||||
[SecretSync.GitHub]: registerGitHubSyncRouter
|
||||
[SecretSync.GitHub]: registerGitHubSyncRouter,
|
||||
[SecretSync.GCPSecretManager]: registerGcpSyncRouter
|
||||
};
|
||||
|
||||
@@ -9,13 +9,19 @@ import {
|
||||
AwsParameterStoreSyncListItemSchema,
|
||||
AwsParameterStoreSyncSchema
|
||||
} from "@app/services/secret-sync/aws-parameter-store";
|
||||
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
|
||||
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
|
||||
|
||||
const SecretSyncSchema = z.discriminatedUnion("destination", [AwsParameterStoreSyncSchema, GitHubSyncSchema]);
|
||||
const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
AwsParameterStoreSyncSchema,
|
||||
GitHubSyncSchema,
|
||||
GcpSyncSchema
|
||||
]);
|
||||
|
||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
AwsParameterStoreSyncListItemSchema,
|
||||
GitHubSyncListItemSchema
|
||||
GitHubSyncListItemSchema,
|
||||
GcpSyncListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum AppConnection {
|
||||
GitHub = "github",
|
||||
AWS = "aws"
|
||||
AWS = "aws",
|
||||
GCP = "gcp"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TAppConnections } from "@app/db/schemas/app-connections";
|
||||
import { generateHash } from "@app/lib/crypto/encryption";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
|
||||
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
|
||||
@@ -7,6 +8,11 @@ import {
|
||||
getAwsAppConnectionListItem,
|
||||
validateAwsConnectionCredentials
|
||||
} from "@app/services/app-connection/aws";
|
||||
import {
|
||||
GcpConnectionMethod,
|
||||
getGcpAppConnectionListItem,
|
||||
validateGcpConnectionCredentials
|
||||
} from "@app/services/app-connection/gcp";
|
||||
import {
|
||||
getGitHubConnectionListItem,
|
||||
GitHubConnectionMethod,
|
||||
@@ -15,7 +21,9 @@ import {
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
export const listAppConnectionOptions = () => {
|
||||
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
||||
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem(), getGcpAppConnectionListItem()].sort((a, b) =>
|
||||
a.name.localeCompare(b.name)
|
||||
);
|
||||
};
|
||||
|
||||
export const encryptAppConnectionCredentials = async ({
|
||||
@@ -69,6 +77,8 @@ export const validateAppConnectionCredentials = async (
|
||||
return validateAwsConnectionCredentials(appConnection);
|
||||
case AppConnection.GitHub:
|
||||
return validateGitHubConnectionCredentials(appConnection);
|
||||
case AppConnection.GCP:
|
||||
return validateGcpConnectionCredentials(appConnection);
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unhandled App Connection ${app}`);
|
||||
@@ -85,6 +95,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
return "Access Key";
|
||||
case AwsConnectionMethod.AssumeRole:
|
||||
return "Assume Role";
|
||||
case GcpConnectionMethod.ServiceAccountImpersonation:
|
||||
return "Service Account Impersonation";
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||
@@ -101,6 +113,7 @@ export const decryptAppConnection = async (
|
||||
encryptedCredentials: appConnection.encryptedCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
})
|
||||
}),
|
||||
credentialsHash: generateHash(appConnection.encryptedCredentials)
|
||||
} as TAppConnection;
|
||||
};
|
||||
|
||||
@@ -2,5 +2,6 @@ import { AppConnection } from "./app-connection-enums";
|
||||
|
||||
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.AWS]: "AWS",
|
||||
[AppConnection.GitHub]: "GitHub"
|
||||
[AppConnection.GitHub]: "GitHub",
|
||||
[AppConnection.GCP]: "GCP"
|
||||
};
|
||||
|
||||
@@ -10,6 +10,8 @@ export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
|
||||
encryptedCredentials: true,
|
||||
app: true,
|
||||
method: true
|
||||
}).extend({
|
||||
credentialsHash: z.string().optional()
|
||||
});
|
||||
|
||||
export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { generateHash } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
@@ -26,6 +27,8 @@ import { githubConnectionService } from "@app/services/app-connection/github/git
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
||||
import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
||||
|
||||
export type TAppConnectionServiceFactoryDep = {
|
||||
appConnectionDAL: TAppConnectionDALFactory;
|
||||
@@ -37,7 +40,8 @@ export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServic
|
||||
|
||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
|
||||
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
|
||||
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema
|
||||
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema,
|
||||
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -182,6 +186,7 @@ export const appConnectionServiceFactory = ({
|
||||
|
||||
return {
|
||||
...connection,
|
||||
credentialsHash: generateHash(connection.encryptedCredentials),
|
||||
credentials: validatedCredentials
|
||||
};
|
||||
});
|
||||
@@ -382,6 +387,7 @@ export const appConnectionServiceFactory = ({
|
||||
deleteAppConnection,
|
||||
connectAppConnectionById,
|
||||
listAvailableAppConnectionsForUser,
|
||||
github: githubConnectionService(connectAppConnectionById)
|
||||
github: githubConnectionService(connectAppConnectionById),
|
||||
gcp: gcpConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,9 +11,11 @@ import {
|
||||
TValidateGitHubConnectionCredentials
|
||||
} from "@app/services/app-connection/github";
|
||||
|
||||
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection);
|
||||
import { TGcpConnection, TGcpConnectionConfig, TGcpConnectionInput, TValidateGcpConnectionCredentials } from "./gcp";
|
||||
|
||||
export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput);
|
||||
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection | TGcpConnection);
|
||||
|
||||
export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput | TGcpConnectionInput);
|
||||
|
||||
export type TCreateAppConnectionDTO = Pick<
|
||||
TAppConnectionInput,
|
||||
@@ -24,8 +26,9 @@ export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "met
|
||||
connectionId: string;
|
||||
};
|
||||
|
||||
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig;
|
||||
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig | TGcpConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentials =
|
||||
| TValidateAwsConnectionCredentials
|
||||
| TValidateGitHubConnectionCredentials;
|
||||
| TValidateGitHubConnectionCredentials
|
||||
| TValidateGcpConnectionCredentials;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum GcpConnectionMethod {
|
||||
ServiceAccountImpersonation = "service-account-impersonation"
|
||||
}
|
||||
164
backend/src/services/app-connection/gcp/gcp-connection-fns.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { gaxios, Impersonated, JWT } from "google-auth-library";
|
||||
import { GetAccessTokenResponse } from "google-auth-library/build/src/auth/oauth2client";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { getAppConnectionMethodName } from "../app-connection-fns";
|
||||
import { GcpConnectionMethod } from "./gcp-connection-enums";
|
||||
import {
|
||||
GCPApp,
|
||||
GCPGetProjectsRes,
|
||||
GCPGetServiceRes,
|
||||
TGcpConnection,
|
||||
TGcpConnectionConfig
|
||||
} from "./gcp-connection-types";
|
||||
|
||||
export const getGcpAppConnectionListItem = () => {
|
||||
return {
|
||||
name: "GCP" as const,
|
||||
app: AppConnection.GCP as const,
|
||||
methods: Object.values(GcpConnectionMethod) as [GcpConnectionMethod.ServiceAccountImpersonation]
|
||||
};
|
||||
};
|
||||
|
||||
export const getGcpConnectionAuthToken = async (appConnection: TGcpConnectionConfig) => {
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) {
|
||||
throw new InternalServerError({
|
||||
message: `Environment variables have not been configured for GCP ${getAppConnectionMethodName(
|
||||
GcpConnectionMethod.ServiceAccountImpersonation
|
||||
)}`
|
||||
});
|
||||
}
|
||||
|
||||
const credJson = JSON.parse(appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) as {
|
||||
client_email: string;
|
||||
private_key: string;
|
||||
};
|
||||
|
||||
const sourceClient = new JWT({
|
||||
email: credJson.client_email,
|
||||
key: credJson.private_key,
|
||||
scopes: ["https://www.googleapis.com/auth/cloud-platform"]
|
||||
});
|
||||
|
||||
const impersonatedCredentials = new Impersonated({
|
||||
sourceClient,
|
||||
targetPrincipal: appConnection.credentials.serviceAccountEmail,
|
||||
lifetime: 3600,
|
||||
delegates: [],
|
||||
targetScopes: ["https://www.googleapis.com/auth/cloud-platform"]
|
||||
});
|
||||
|
||||
let tokenResponse: GetAccessTokenResponse | undefined;
|
||||
try {
|
||||
tokenResponse = await impersonatedCredentials.getAccessToken();
|
||||
} catch (error) {
|
||||
let message = "Unable to validate connection";
|
||||
if (error instanceof gaxios.GaxiosError) {
|
||||
message = error.message;
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
if (!tokenResponse || !tokenResponse.token) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection`
|
||||
});
|
||||
}
|
||||
|
||||
return tokenResponse.token;
|
||||
};
|
||||
|
||||
export const getGcpSecretManagerProjects = async (appConnection: TGcpConnection) => {
|
||||
const accessToken = await getGcpConnectionAuthToken(appConnection);
|
||||
|
||||
let gcpApps: GCPApp[] = [];
|
||||
|
||||
const pageSize = 100;
|
||||
let pageToken: string | undefined;
|
||||
let hasMorePages = true;
|
||||
|
||||
const projects: {
|
||||
name: string;
|
||||
id: string;
|
||||
}[] = [];
|
||||
|
||||
while (hasMorePages) {
|
||||
const params = new URLSearchParams({
|
||||
pageSize: String(pageSize),
|
||||
...(pageToken ? { pageToken } : {})
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { data } = await request.get<GCPGetProjectsRes>(`${IntegrationUrls.GCP_API_URL}/v1/projects`, {
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
gcpApps = gcpApps.concat(data.projects);
|
||||
|
||||
if (!data.nextPageToken) {
|
||||
hasMorePages = false;
|
||||
}
|
||||
|
||||
pageToken = data.nextPageToken;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
for await (const gcpApp of gcpApps) {
|
||||
try {
|
||||
const res = (
|
||||
await request.get<GCPGetServiceRes>(
|
||||
`${IntegrationUrls.GCP_SERVICE_USAGE_URL}/v1/projects/${gcpApp.projectId}/services/${IntegrationUrls.GCP_SECRET_MANAGER_SERVICE_NAME}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
)
|
||||
).data;
|
||||
|
||||
if (res.state === "ENABLED") {
|
||||
projects.push({
|
||||
name: gcpApp.name,
|
||||
id: gcpApp.projectId
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// eslint-disable-next-line
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return projects;
|
||||
};
|
||||
|
||||
export const validateGcpConnectionCredentials = async (appConnection: TGcpConnectionConfig) => {
|
||||
// Check if provided service account email suffix matches organization ID.
|
||||
// We do this to mitigate confused deputy attacks in multi-tenant instances
|
||||
if (appConnection.credentials.serviceAccountEmail) {
|
||||
const expectedAccountIdSuffix = appConnection.orgId.split("-").slice(0, 2).join("-");
|
||||
const serviceAccountId = appConnection.credentials.serviceAccountEmail.split("@")[0];
|
||||
if (!serviceAccountId.endsWith(expectedAccountIdSuffix)) {
|
||||
throw new BadRequestError({
|
||||
message: `GCP service account ID (the part of the email before '@') must have a suffix of "${expectedAccountIdSuffix}"`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await getGcpConnectionAuthToken(appConnection);
|
||||
|
||||
return appConnection.credentials;
|
||||
};
|
||||
@@ -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";
|
||||
};
|
||||
4
backend/src/services/app-connection/gcp/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./gcp-connection-enums";
|
||||
export * from "./gcp-connection-fns";
|
||||
export * from "./gcp-connection-schemas";
|
||||
export * from "./gcp-connection-types";
|
||||
10
backend/src/services/secret-sync/gcp/gcp-sync-constants.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const GCP_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "GCP Secret Manager",
|
||||
destination: SecretSync.GCPSecretManager,
|
||||
connection: AppConnection.GCP,
|
||||
canImportSecrets: true
|
||||
};
|
||||
3
backend/src/services/secret-sync/gcp/gcp-sync-enums.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export enum GcpSyncScope {
|
||||
Global = "global"
|
||||
}
|
||||
218
backend/src/services/secret-sync/gcp/gcp-sync-fns.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { SecretSyncError } from "../secret-sync-errors";
|
||||
import { TSecretMap } from "../secret-sync-types";
|
||||
import {
|
||||
GCPLatestSecretVersionAccess,
|
||||
GCPSecret,
|
||||
GCPSMListSecretsRes,
|
||||
TGcpSyncWithCredentials
|
||||
} from "./gcp-sync-types";
|
||||
|
||||
const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
|
||||
let gcpSecrets: GCPSecret[] = [];
|
||||
|
||||
const pageSize = 100;
|
||||
let pageToken: string | undefined;
|
||||
let hasMorePages = true;
|
||||
|
||||
while (hasMorePages) {
|
||||
const params = new URLSearchParams({
|
||||
pageSize: String(pageSize),
|
||||
...(pageToken ? { pageToken } : {})
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { data: secretsRes } = await request.get<GCPSMListSecretsRes>(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${secretSync.destinationConfig.projectId}/secrets`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (secretsRes.secrets) {
|
||||
gcpSecrets = gcpSecrets.concat(secretsRes.secrets);
|
||||
}
|
||||
|
||||
if (!secretsRes.nextPageToken) {
|
||||
hasMorePages = false;
|
||||
}
|
||||
|
||||
pageToken = secretsRes.nextPageToken;
|
||||
}
|
||||
|
||||
const res: { [key: string]: string } = {};
|
||||
|
||||
for await (const gcpSecret of gcpSecrets) {
|
||||
const arr = gcpSecret.name.split("/");
|
||||
const key = arr[arr.length - 1];
|
||||
|
||||
try {
|
||||
const { data: secretLatest } = await request.get<GCPLatestSecretVersionAccess>(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}/versions/latest:access`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
|
||||
} catch (error) {
|
||||
// when a secret in GCP has no versions, we treat it as if it's a blank value
|
||||
if (error instanceof AxiosError && error.response?.status === 404) {
|
||||
res[key] = "";
|
||||
} else {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const GcpSyncFns = {
|
||||
syncSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig, connection } = secretSync;
|
||||
const accessToken = await getGcpConnectionAuthToken(connection);
|
||||
|
||||
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
|
||||
|
||||
for await (const key of Object.keys(secretMap)) {
|
||||
try {
|
||||
// we do not process secrets with no value because GCP secret manager does not allow it
|
||||
if (!secretMap[key].value) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(key in gcpSecrets)) {
|
||||
// case: create secret
|
||||
await request.post(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets`,
|
||||
{
|
||||
replication: {
|
||||
automatic: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
params: {
|
||||
secretId: key
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await request.post(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
|
||||
{
|
||||
payload: {
|
||||
data: Buffer.from(secretMap[key].value).toString("base64")
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for await (const key of Object.keys(gcpSecrets)) {
|
||||
try {
|
||||
if (!(key in secretMap) || !secretMap[key].value) {
|
||||
// case: delete secret
|
||||
await request.delete(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
} else if (secretMap[key].value !== gcpSecrets[key]) {
|
||||
if (!secretMap[key].value) {
|
||||
logger.warn(
|
||||
`syncSecretsGcpsecretManager: update secret value in gcp where [key=${key}] and [projectId=${destinationConfig.projectId}]`
|
||||
);
|
||||
}
|
||||
|
||||
await request.post(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
|
||||
{
|
||||
payload: {
|
||||
data: Buffer.from(secretMap[key].value).toString("base64")
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getSecrets: async (secretSync: TGcpSyncWithCredentials): Promise<TSecretMap> => {
|
||||
const { connection } = secretSync;
|
||||
const accessToken = await getGcpConnectionAuthToken(connection);
|
||||
|
||||
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
|
||||
return Object.fromEntries(Object.entries(gcpSecrets).map(([key, value]) => [key, { value: value ?? "" }]));
|
||||
},
|
||||
|
||||
removeSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig, connection } = secretSync;
|
||||
const accessToken = await getGcpConnectionAuthToken(connection);
|
||||
|
||||
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
|
||||
for await (const [key] of Object.entries(gcpSecrets)) {
|
||||
if (key in secretMap) {
|
||||
await request.delete(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
45
backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { SecretSync } from "../secret-sync-enums";
|
||||
import { GcpSyncScope } from "./gcp-sync-enums";
|
||||
|
||||
const GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
const GcpSyncDestinationConfigSchema = z.object({
|
||||
scope: z.literal(GcpSyncScope.Global),
|
||||
projectId: z.string().min(1, "Project ID is required")
|
||||
});
|
||||
|
||||
export const GcpSyncSchema = BaseSecretSyncSchema(SecretSync.GCPSecretManager, GcpSyncOptionsConfig).extend({
|
||||
destination: z.literal(SecretSync.GCPSecretManager),
|
||||
destinationConfig: GcpSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateGcpSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.GCPSecretManager,
|
||||
GcpSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: GcpSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateGcpSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.GCPSecretManager,
|
||||
GcpSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: GcpSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const GcpSyncListItemSchema = z.object({
|
||||
name: z.literal("GCP Secret Manager"),
|
||||
connection: z.literal(AppConnection.GCP),
|
||||
destination: z.literal(SecretSync.GCPSecretManager),
|
||||
canImportSecrets: z.literal(true)
|
||||
});
|
||||
33
backend/src/services/secret-sync/gcp/gcp-sync-types.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import z from "zod";
|
||||
|
||||
import { TGcpConnection } from "@app/services/app-connection/gcp";
|
||||
|
||||
import { CreateGcpSyncSchema, GcpSyncListItemSchema, GcpSyncSchema } from "./gcp-sync-schemas";
|
||||
|
||||
export type TGcpSyncListItem = z.infer<typeof GcpSyncListItemSchema>;
|
||||
|
||||
export type TGcpSync = z.infer<typeof GcpSyncSchema>;
|
||||
|
||||
export type TGcpSyncInput = z.infer<typeof CreateGcpSyncSchema>;
|
||||
|
||||
export type TGcpSyncWithCredentials = TGcpSync & {
|
||||
connection: TGcpConnection;
|
||||
};
|
||||
|
||||
export type GCPSecret = {
|
||||
name: string;
|
||||
createTime: string;
|
||||
};
|
||||
|
||||
export type GCPSMListSecretsRes = {
|
||||
secrets?: GCPSecret[];
|
||||
totalSize?: number;
|
||||
nextPageToken?: string;
|
||||
};
|
||||
|
||||
export type GCPLatestSecretVersionAccess = {
|
||||
name: string;
|
||||
payload: {
|
||||
data: string;
|
||||
};
|
||||
};
|
||||
4
backend/src/services/secret-sync/gcp/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./gcp-sync-constants";
|
||||
export * from "./gcp-sync-enums";
|
||||
export * from "./gcp-sync-schemas";
|
||||
export * from "./gcp-sync-types";
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum SecretSync {
|
||||
AWSParameterStore = "aws-parameter-store",
|
||||
GitHub = "github"
|
||||
GitHub = "github",
|
||||
GCPSecretManager = "gcp-secret-manager"
|
||||
}
|
||||
|
||||
export enum SecretSyncInitialSyncBehavior {
|
||||
|
||||
@@ -13,9 +13,13 @@ import {
|
||||
TSecretSyncWithCredentials
|
||||
} from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
||||
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
||||
|
||||
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
|
||||
[SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION
|
||||
[SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION,
|
||||
[SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretSyncOptions = () => {
|
||||
@@ -71,6 +75,8 @@ export const SecretSyncFns = {
|
||||
return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.GitHub:
|
||||
return GithubSyncFns.syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.GCPSecretManager:
|
||||
return GcpSyncFns.syncSecrets(secretSync, secretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -86,6 +92,9 @@ export const SecretSyncFns = {
|
||||
case SecretSync.GitHub:
|
||||
secretMap = await GithubSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.GCPSecretManager:
|
||||
secretMap = await GcpSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -103,6 +112,8 @@ export const SecretSyncFns = {
|
||||
return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.GitHub:
|
||||
return GithubSyncFns.removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.GCPSecretManager:
|
||||
return GcpSyncFns.removeSecrets(secretSync, secretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -115,7 +126,7 @@ export const parseSyncErrorMessage = (err: unknown): string => {
|
||||
if (err instanceof SecretSyncError) {
|
||||
return JSON.stringify({
|
||||
secretKey: err.secretKey,
|
||||
error: err.message ?? parseSyncErrorMessage(err.error)
|
||||
error: err.message || parseSyncErrorMessage(err.error)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.AWSParameterStore]: "AWS Parameter Store",
|
||||
[SecretSync.GitHub]: "GitHub"
|
||||
[SecretSync.GitHub]: "GitHub",
|
||||
[SecretSync.GCPSecretManager]: "GCP Secret Manager"
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.AWSParameterStore]: AppConnection.AWS,
|
||||
[SecretSync.GitHub]: AppConnection.GitHub
|
||||
[SecretSync.GitHub]: AppConnection.GitHub,
|
||||
[SecretSync.GCPSecretManager]: AppConnection.GCP
|
||||
};
|
||||
|
||||
@@ -17,14 +17,18 @@ import {
|
||||
TAwsParameterStoreSyncListItem,
|
||||
TAwsParameterStoreSyncWithCredentials
|
||||
} from "./aws-parameter-store";
|
||||
import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp";
|
||||
|
||||
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync;
|
||||
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync | TGcpSync;
|
||||
|
||||
export type TSecretSyncWithCredentials = TAwsParameterStoreSyncWithCredentials | TGitHubSyncWithCredentials;
|
||||
export type TSecretSyncWithCredentials =
|
||||
| TAwsParameterStoreSyncWithCredentials
|
||||
| TGitHubSyncWithCredentials
|
||||
| TGcpSyncWithCredentials;
|
||||
|
||||
export type TSecretSyncInput = TAwsParameterStoreSyncInput | TGitHubSyncInput;
|
||||
export type TSecretSyncInput = TAwsParameterStoreSyncInput | TGitHubSyncInput | TGcpSyncInput;
|
||||
|
||||
export type TSecretSyncListItem = TAwsParameterStoreSyncListItem | TGitHubSyncListItem;
|
||||
export type TSecretSyncListItem = TAwsParameterStoreSyncListItem | TGitHubSyncListItem | TGcpSyncListItem;
|
||||
|
||||
export type TSyncOptionsConfig = {
|
||||
canImportSecrets: boolean;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Available"
|
||||
openapi: "GET /api/v1/app-connections/gcp/available"
|
||||
---
|
||||
10
docs/api-reference/endpoints/app-connections/gcp/create.mdx
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/app-connections/gcp"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [GCP
|
||||
Connections](/integrations/app-connections/gcp) to learn how to obtain the
|
||||
required credentials.
|
||||
</Note>
|
||||
@@ -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"
|
||||
---
|
||||
10
docs/api-reference/endpoints/app-connections/gcp/update.mdx
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/app-connections/gcp/{connectionId}"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [GCP
|
||||
Connections](/integrations/app-connections/gcp) to learn how to obtain the
|
||||
required credentials.
|
||||
</Note>
|
||||
@@ -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 |
BIN
docs/images/app-connections/gcp/create-service-account.png
Normal file
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 978 KiB |
BIN
docs/images/app-connections/gcp/select-gcp-connection.png
Normal file
|
After Width: | Height: | Size: 580 KiB |
BIN
docs/images/app-connections/gcp/service-account-grant-access.png
Normal file
|
After Width: | Height: | Size: 669 KiB |
BIN
docs/images/app-connections/gcp/service-account-overview.png
Normal file
|
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 |
88
docs/integrations/app-connections/gcp.mdx
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: "GCP Connection"
|
||||
description: "Learn how to configure a GCP Connection for Infisical."
|
||||
---
|
||||
|
||||
Infisical supports [service account impersonation](https://cloud.google.com/iam/docs/service-account-impersonation) to connect with your GCP projects.
|
||||
|
||||
<Accordion title="Self-Hosted Instance">
|
||||
Using the GCP integration on a self-hosted instance of Infisical requires configuring a service account on GCP and
|
||||
configuring your instance to use it.
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to IAM & Admin > Service Accounts in Google Cloud Console">
|
||||

|
||||
</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>
|
||||
141
docs/integrations/secret-syncs/gcp-secret-manager.mdx
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
title: "GCP Secret Manager Sync"
|
||||
description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
|
||||
---
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||
- Create a [GCP Connection](/integrations/app-connections/gcp) with the required **Secret Sync** permissions
|
||||
- Enable **Cloud Resource Manager API** and **Secret Manager API** on your GCP project
|
||||

|
||||

|
||||
|
||||
<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",
|
||||
"pages": [
|
||||
"integrations/app-connections/aws",
|
||||
"integrations/app-connections/github"
|
||||
"integrations/app-connections/github",
|
||||
"integrations/app-connections/gcp"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -365,7 +366,8 @@
|
||||
"group": "Syncs",
|
||||
"pages": [
|
||||
"integrations/secret-syncs/aws-parameter-store",
|
||||
"integrations/secret-syncs/github"
|
||||
"integrations/secret-syncs/github",
|
||||
"integrations/secret-syncs/gcp-secret-manager"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -799,7 +801,8 @@
|
||||
"pages": [
|
||||
"api-reference/endpoints/app-connections/list",
|
||||
"api-reference/endpoints/app-connections/options",
|
||||
{ "group": "AWS",
|
||||
{
|
||||
"group": "AWS",
|
||||
"pages": [
|
||||
"api-reference/endpoints/app-connections/aws/list",
|
||||
"api-reference/endpoints/app-connections/aws/available",
|
||||
@@ -810,7 +813,8 @@
|
||||
"api-reference/endpoints/app-connections/aws/delete"
|
||||
]
|
||||
},
|
||||
{ "group": "GitHub",
|
||||
{
|
||||
"group": "GitHub",
|
||||
"pages": [
|
||||
"api-reference/endpoints/app-connections/github/list",
|
||||
"api-reference/endpoints/app-connections/github/available",
|
||||
@@ -820,6 +824,18 @@
|
||||
"api-reference/endpoints/app-connections/github/update",
|
||||
"api-reference/endpoints/app-connections/github/delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "GCP",
|
||||
"pages": [
|
||||
"api-reference/endpoints/app-connections/gcp/list",
|
||||
"api-reference/endpoints/app-connections/gcp/available",
|
||||
"api-reference/endpoints/app-connections/gcp/get-by-id",
|
||||
"api-reference/endpoints/app-connections/gcp/get-by-name",
|
||||
"api-reference/endpoints/app-connections/gcp/create",
|
||||
"api-reference/endpoints/app-connections/gcp/update",
|
||||
"api-reference/endpoints/app-connections/gcp/delete"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -828,7 +844,8 @@
|
||||
"pages": [
|
||||
"api-reference/endpoints/secret-syncs/list",
|
||||
"api-reference/endpoints/secret-syncs/options",
|
||||
{ "group": "AWS Parameter Store",
|
||||
{
|
||||
"group": "AWS Parameter Store",
|
||||
"pages": [
|
||||
"api-reference/endpoints/secret-syncs/aws-parameter-store/list",
|
||||
"api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-id",
|
||||
@@ -841,7 +858,8 @@
|
||||
"api-reference/endpoints/secret-syncs/aws-parameter-store/remove-secrets"
|
||||
]
|
||||
},
|
||||
{ "group": "GitHub",
|
||||
{
|
||||
"group": "GitHub",
|
||||
"pages": [
|
||||
"api-reference/endpoints/secret-syncs/github/list",
|
||||
"api-reference/endpoints/secret-syncs/github/get-by-id",
|
||||
@@ -852,6 +870,20 @@
|
||||
"api-reference/endpoints/secret-syncs/github/sync-secrets",
|
||||
"api-reference/endpoints/secret-syncs/github/remove-secrets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "GCP Secret Manager",
|
||||
"pages": [
|
||||
"api-reference/endpoints/secret-syncs/gcp-secret-manager/list",
|
||||
"api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-id",
|
||||
"api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-name",
|
||||
"api-reference/endpoints/secret-syncs/gcp-secret-manager/create",
|
||||
"api-reference/endpoints/secret-syncs/gcp-secret-manager/update",
|
||||
"api-reference/endpoints/secret-syncs/gcp-secret-manager/delete",
|
||||
"api-reference/endpoints/secret-syncs/gcp-secret-manager/sync-secrets",
|
||||
"api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets",
|
||||
"api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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 { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields";
|
||||
import { GcpSyncFields } from "./GcpSyncFields";
|
||||
import { GitHubSyncFields } from "./GitHubSyncFields";
|
||||
|
||||
export const SecretSyncDestinationFields = () => {
|
||||
@@ -16,6 +17,8 @@ export const SecretSyncDestinationFields = () => {
|
||||
return <AwsParameterStoreSyncFields />;
|
||||
case SecretSync.GitHub:
|
||||
return <GitHubSyncFields />;
|
||||
case SecretSync.GCPSecretManager:
|
||||
return <GcpSyncFields />;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Config Field: ${destination}`);
|
||||
}
|
||||
|
||||
@@ -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 { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields";
|
||||
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
|
||||
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
|
||||
|
||||
export const SecretSyncReviewFields = () => {
|
||||
@@ -38,6 +39,9 @@ export const SecretSyncReviewFields = () => {
|
||||
case SecretSync.GitHub:
|
||||
DestinationFieldsComponent = <GitHubSyncReviewFields />;
|
||||
break;
|
||||
case SecretSync.GCPSecretManager:
|
||||
DestinationFieldsComponent = <GcpSyncReviewFields />;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
|
||||
}
|
||||
|
||||
@@ -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 { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
|
||||
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
|
||||
|
||||
const BaseSecretSyncSchema = z.object({
|
||||
name: slugSchema({ field: "Name" }),
|
||||
@@ -31,7 +32,8 @@ const BaseSecretSyncSchema = z.object({
|
||||
|
||||
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||
AwsParameterStoreSyncDestinationSchema,
|
||||
GitHubSyncDestinationSchema
|
||||
GitHubSyncDestinationSchema,
|
||||
GcpSyncDestinationSchema
|
||||
]);
|
||||
|
||||
export const SecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema);
|
||||
|
||||
@@ -141,7 +141,7 @@ export const SecretPathInput = ({
|
||||
maxHeight: "var(--radix-select-content-available-height)"
|
||||
}}
|
||||
>
|
||||
<div className="h-full w-full max-w-60 flex-col items-center justify-center rounded-md text-white">
|
||||
<div className="h-full w-full flex-col items-center justify-center rounded-md text-white">
|
||||
{suggestions.map((suggestion, i) => (
|
||||
<div
|
||||
tabIndex={0}
|
||||
|
||||
@@ -4,13 +4,18 @@ import { faKey, faPassport, faUser } from "@fortawesome/free-solid-svg-icons";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import {
|
||||
AwsConnectionMethod,
|
||||
GcpConnectionMethod,
|
||||
GitHubConnectionMethod,
|
||||
TAppConnection
|
||||
} from "@app/hooks/api/appConnections/types";
|
||||
|
||||
export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: string }> = {
|
||||
[AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" },
|
||||
[AppConnection.GitHub]: { name: "GitHub", image: "GitHub.png" }
|
||||
[AppConnection.GitHub]: { name: "GitHub", image: "GitHub.png" },
|
||||
[AppConnection.GCP]: {
|
||||
name: "GCP",
|
||||
image: "Google Cloud Platform.png"
|
||||
}
|
||||
};
|
||||
|
||||
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
|
||||
@@ -23,6 +28,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
||||
return { name: "Access Key", icon: faKey };
|
||||
case AwsConnectionMethod.AssumeRole:
|
||||
return { name: "Assume Role", icon: faUser };
|
||||
case GcpConnectionMethod.ServiceAccountImpersonation:
|
||||
return { name: "Service Account Impersonation", icon: faUser };
|
||||
default:
|
||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ import {
|
||||
|
||||
export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = {
|
||||
[SecretSync.AWSParameterStore]: { name: "Parameter Store", image: "Amazon Web Services.png" },
|
||||
[SecretSync.GitHub]: { name: "GitHub", image: "GitHub.png" }
|
||||
[SecretSync.GitHub]: { name: "GitHub", image: "GitHub.png" },
|
||||
[SecretSync.GCPSecretManager]: { name: "GCP Secret Manager", image: "Google Cloud Platform.png" }
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.AWSParameterStore]: AppConnection.AWS,
|
||||
[SecretSync.GitHub]: AppConnection.GitHub
|
||||
[SecretSync.GitHub]: AppConnection.GitHub,
|
||||
[SecretSync.GCPSecretManager]: AppConnection.GCP
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum AppConnection {
|
||||
AWS = "aws",
|
||||
GitHub = "github"
|
||||
GitHub = "github",
|
||||
GCP = "gcp"
|
||||
}
|
||||
|
||||
37
frontend/src/hooks/api/appConnections/gcp/queries.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { appConnectionKeys } from "../queries";
|
||||
import { TGcpProject } from "./types";
|
||||
|
||||
const gcpConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "gcp"] as const,
|
||||
listProjects: (connectionId: string) =>
|
||||
[...gcpConnectionKeys.all, "projects", connectionId] as const
|
||||
};
|
||||
|
||||
export const useGcpConnectionListProjects = (
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TGcpProject[],
|
||||
unknown,
|
||||
TGcpProject[],
|
||||
ReturnType<typeof gcpConnectionKeys.listProjects>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: gcpConnectionKeys.listProjects(connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TGcpProject[]>(
|
||||
`/api/v1/app-connections/gcp/${connectionId}/secret-manager-projects`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
4
frontend/src/hooks/api/appConnections/gcp/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export type TGcpProject = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
@@ -16,9 +16,14 @@ export type TGitHubConnectionOption = TAppConnectionOptionBase & {
|
||||
appClientSlug?: string;
|
||||
};
|
||||
|
||||
export type TGcpConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.GCP;
|
||||
};
|
||||
|
||||
export type TAppConnectionOption = TAwsConnectionOption | TGitHubConnectionOption;
|
||||
|
||||
export type TAppConnectionOptionMap = {
|
||||
[AppConnection.AWS]: TAwsConnectionOption;
|
||||
[AppConnection.GitHub]: TGitHubConnectionOption;
|
||||
[AppConnection.GCP]: TGcpConnectionOption;
|
||||
};
|
||||
|
||||
@@ -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 { TGitHubConnection } from "@app/hooks/api/appConnections/types/github-connection";
|
||||
|
||||
import { TGcpConnection } from "./gcp-connection";
|
||||
|
||||
export * from "./aws-connection";
|
||||
export * from "./gcp-connection";
|
||||
export * from "./github-connection";
|
||||
|
||||
export type TAppConnection = TAwsConnection | TGitHubConnection;
|
||||
export type TAppConnection = TAwsConnection | TGitHubConnection | TGcpConnection;
|
||||
|
||||
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "app" | "id">;
|
||||
|
||||
@@ -36,4 +39,5 @@ export type TDeleteAppConnectionDTO = {
|
||||
export type TAppConnectionMap = {
|
||||
[AppConnection.AWS]: TAwsConnection;
|
||||
[AppConnection.GitHub]: TGitHubConnection;
|
||||
[AppConnection.GCP]: TGcpConnection;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum SecretSync {
|
||||
AWSParameterStore = "aws-parameter-store",
|
||||
GitHub = "github"
|
||||
GitHub = "github",
|
||||
GCPSecretManager = "gcp-secret-manager"
|
||||
}
|
||||
|
||||
export enum SecretSyncStatus {
|
||||
|
||||
20
frontend/src/hooks/api/secretSyncs/types/gcp-sync.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||
|
||||
export enum GcpSyncScope {
|
||||
Global = "global"
|
||||
}
|
||||
|
||||
export type TGcpSync = TRootSecretSync & {
|
||||
destination: SecretSync.GCPSecretManager;
|
||||
destinationConfig: {
|
||||
scope: GcpSyncScope.Global;
|
||||
projectId: string;
|
||||
};
|
||||
connection: {
|
||||
app: AppConnection.GCP;
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
@@ -3,13 +3,15 @@ import { TAwsParameterStoreSync } from "@app/hooks/api/secretSyncs/types/aws-par
|
||||
import { TGitHubSync } from "@app/hooks/api/secretSyncs/types/github-sync";
|
||||
import { DiscriminativePick } from "@app/types";
|
||||
|
||||
import { TGcpSync } from "./gcp-sync";
|
||||
|
||||
export type TSecretSyncOption = {
|
||||
name: string;
|
||||
destination: SecretSync;
|
||||
canImportSecrets: boolean;
|
||||
};
|
||||
|
||||
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync;
|
||||
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync | TGcpSync;
|
||||
|
||||
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { DiscriminativePick } from "@app/types";
|
||||
|
||||
import { AppConnectionHeader } from "../AppConnectionHeader";
|
||||
import { AwsConnectionForm } from "./AwsConnectionForm";
|
||||
import { GcpConnectionForm } from "./GcpConnectionForm";
|
||||
import { GitHubConnectionForm } from "./GitHubConnectionForm";
|
||||
|
||||
type FormProps = {
|
||||
@@ -50,6 +51,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
||||
return <AwsConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.GitHub:
|
||||
return <GitHubConnectionForm />;
|
||||
case AppConnection.GCP:
|
||||
return <GcpConnectionForm onSubmit={onSubmit} />;
|
||||
default:
|
||||
throw new Error(`Unhandled App ${app}`);
|
||||
}
|
||||
@@ -87,6 +90,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
return <AwsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||
case AppConnection.GitHub:
|
||||
return <GitHubConnectionForm appConnection={appConnection} />;
|
||||
case AppConnection.GCP:
|
||||
return <GcpConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||
default:
|
||||
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
|
||||
}
|
||||
|
||||
@@ -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 { AwsParameterStoreSyncDestinationCol } from "./AwsParameterStoreSyncDestinationCol";
|
||||
import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol";
|
||||
import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol";
|
||||
|
||||
type Props = {
|
||||
@@ -13,6 +14,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
return <AwsParameterStoreSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.GitHub:
|
||||
return <GitHubSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.GCPSecretManager:
|
||||
return <GcpSyncDestinationCol secretSync={secretSync} />;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
|
||||
|
||||
@@ -39,6 +39,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
|
||||
throw new Error(`Unhandled GitHub Scope Destination Col Values ${destination}`);
|
||||
}
|
||||
break;
|
||||
case SecretSync.GCPSecretManager:
|
||||
primaryText = destinationConfig.projectId;
|
||||
secondaryText = "Global";
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Col Values ${destination}`);
|
||||
}
|
||||
|
||||
@@ -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 { GitHubSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GitHubSyncDestinationSection";
|
||||
|
||||
import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection";
|
||||
|
||||
type Props = {
|
||||
secretSync: TSecretSync;
|
||||
onEditDestination: VoidFunction;
|
||||
@@ -30,6 +32,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
|
||||
case SecretSync.GitHub:
|
||||
DestinationComponents = <GitHubSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
case SecretSync.GCPSecretManager:
|
||||
DestinationComponents = <GcpSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Section components: ${destination}`);
|
||||
}
|
||||
|
||||