mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 15:38:03 -05:00
feat: gcp secret sync
This commit is contained in:
@@ -104,4 +104,7 @@ INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID=
|
|||||||
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET=
|
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(
|
||||||
|
|||||||
@@ -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,17 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateGcpConnectionSchema,
|
||||||
|
SanitizedGcpConnectionSchema,
|
||||||
|
UpdateGcpConnectionSchema
|
||||||
|
} from "@app/services/app-connection/gcp";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerGcpConnectionRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.GCP,
|
||||||
|
server,
|
||||||
|
sanitizedResponseSchema: SanitizedGcpConnectionSchema,
|
||||||
|
createSchema: CreateGcpConnectionSchema,
|
||||||
|
updateSchema: UpdateGcpConnectionSchema
|
||||||
|
});
|
||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -7,6 +7,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 +20,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 +76,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 +94,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}`);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ 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";
|
||||||
|
|
||||||
export type TAppConnectionServiceFactoryDep = {
|
export type TAppConnectionServiceFactoryDep = {
|
||||||
appConnectionDAL: TAppConnectionDALFactory;
|
appConnectionDAL: TAppConnectionDALFactory;
|
||||||
@@ -37,7 +38,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 = ({
|
||||||
|
|||||||
@@ -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,71 @@
|
|||||||
|
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 { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import { getAppConnectionMethodName } from "../app-connection-fns";
|
||||||
|
import { GcpConnectionMethod } from "./gcp-connection-enums";
|
||||||
|
import { 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 validateGcpConnectionCredentials = 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 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().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,22 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
4
backend/src/services/app-connection/gcp/index.ts
Normal file
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";
|
||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export enum AppConnection {
|
export enum AppConnection {
|
||||||
AWS = "aws",
|
AWS = "aws",
|
||||||
GitHub = "github"
|
GitHub = "github",
|
||||||
|
GCP = "gcp"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">;
|
||||||
|
|
||||||
|
|||||||
@@ -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,133 @@
|
|||||||
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
ModalClose,
|
||||||
|
SecretInput,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||||
|
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().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 {
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user