diff --git a/backend/src/ee/services/pam-resource/kubernetes/kubernetes-resource-factory.ts b/backend/src/ee/services/pam-resource/kubernetes/kubernetes-resource-factory.ts index 3721116cf..14aefc243 100644 --- a/backend/src/ee/services/pam-resource/kubernetes/kubernetes-resource-factory.ts +++ b/backend/src/ee/services/pam-resource/kubernetes/kubernetes-resource-factory.ts @@ -67,10 +67,11 @@ export const executeWithGateway = async ( async (proxyPort) => { const protocol = url.protocol === "https:" ? "https" : "http"; const baseUrl = `${protocol}://localhost:${proxyPort}`; + // const baseUrl = `http://localhost:${proxyPort}`; return operation(baseUrl, httpsAgent); }, { - protocol: GatewayProxyProtocol.Http, + protocol: GatewayProxyProtocol.Tcp, relayHost: platformConnectionDetails.relayHost, gateway: platformConnectionDetails.gateway, relay: platformConnectionDetails.relay, @@ -92,9 +93,6 @@ export const kubernetesResourceFactory: TPamResourceFactory< // Validate connection by checking API server version try { await axios.get(`${baseUrl}/version`, { - headers: { - "Content-Type": "application/json" - }, ...(httpsAgent ? { httpsAgent } : {}), signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), timeout: EXTERNAL_REQUEST_TIMEOUT diff --git a/frontend/src/hooks/api/pam/types/index.ts b/frontend/src/hooks/api/pam/types/index.ts index 01b87c282..bff8ac25e 100644 --- a/frontend/src/hooks/api/pam/types/index.ts +++ b/frontend/src/hooks/api/pam/types/index.ts @@ -6,17 +6,19 @@ import { PamResourceType, PamSessionStatus } from "../enums"; +import { TKubernetesAccount, TKubernetesResource } from "./kubernetes-resource"; import { TMySQLAccount, TMySQLResource } from "./mysql-resource"; import { TPostgresAccount, TPostgresResource } from "./postgres-resource"; import { TSSHAccount, TSSHResource } from "./ssh-resource"; +export * from "./kubernetes-resource"; export * from "./mysql-resource"; export * from "./postgres-resource"; export * from "./ssh-resource"; -export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource; +export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource | TKubernetesResource; -export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount; +export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount | TKubernetesAccount; export type TPamFolder = { id: string; diff --git a/frontend/src/hooks/api/pam/types/kubernetes-resource.ts b/frontend/src/hooks/api/pam/types/kubernetes-resource.ts new file mode 100644 index 000000000..cb7670917 --- /dev/null +++ b/frontend/src/hooks/api/pam/types/kubernetes-resource.ts @@ -0,0 +1,35 @@ +import { PamResourceType } from "../enums"; +import { TBasePamAccount } from "./base-account"; +import { TBasePamResource } from "./base-resource"; + +export enum KubernetesAuthMethod { + ServiceAccountToken = "service-account-token" +} + +export type TKubernetesConnectionDetails = { + url: string; + namespace: string; + skipTLSVerify: boolean; + caCertificate?: string; +}; + +export type TKubernetesServiceAccountTokenCredentials = { + authMethod: KubernetesAuthMethod.ServiceAccountToken; + serviceAccountName: string; + serviceAccountToken: string; +}; + +export type TKubernetesCredentials = TKubernetesServiceAccountTokenCredentials; + +// Resources +export type TKubernetesResource = TBasePamResource & { + resourceType: PamResourceType.Kubernetes; +} & { + connectionDetails: TKubernetesConnectionDetails; + rotationAccountCredentials?: TKubernetesCredentials | null; +}; + +// Accounts +export type TKubernetesAccount = TBasePamAccount & { + credentials: TKubernetesCredentials; +}; diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/KubernetesResourceForm.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/KubernetesResourceForm.tsx new file mode 100644 index 000000000..8b650c3b6 --- /dev/null +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/KubernetesResourceForm.tsx @@ -0,0 +1,96 @@ +import { FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, ModalClose } from "@app/components/v2"; +import { KubernetesAuthMethod, PamResourceType, TKubernetesResource } from "@app/hooks/api/pam"; +import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants"; + +import { KubernetesResourceFields } from "./shared/KubernetesResourceFields"; +import { KubernetesRotateAccountFields } from "./shared/KubernetesRotateAccountFields"; +import { GenericResourceFields, genericResourceFieldsSchema } from "./GenericResourceFields"; + +type Props = { + resource?: TKubernetesResource; + onSubmit: (formData: FormData) => Promise; +}; + +const KubernetesConnectionDetailsSchema = z.object({ + url: z.string().url().trim().max(500), + namespace: z.string().trim().max(255), + skipTLSVerify: z.boolean(), + caCertificate: z.string().trim().max(10000).optional() +}); + +const KubernetesServiceAccountTokenCredentialsSchema = z.object({ + authMethod: z.literal(KubernetesAuthMethod.ServiceAccountToken), + serviceAccountName: z.string().trim().max(255), + serviceAccountToken: z.string().trim().max(10000) +}); + +const formSchema = genericResourceFieldsSchema.extend({ + resourceType: z.literal(PamResourceType.Kubernetes), + connectionDetails: KubernetesConnectionDetailsSchema, + rotationAccountCredentials: KubernetesServiceAccountTokenCredentialsSchema.nullable().optional() +}); + +type FormData = z.infer; + +export const KubernetesResourceForm = ({ resource, onSubmit }: Props) => { + const isUpdate = Boolean(resource); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: resource + ? { + ...resource, + rotationAccountCredentials: resource.rotationAccountCredentials + ? { + ...resource.rotationAccountCredentials, + serviceAccountToken: UNCHANGED_PASSWORD_SENTINEL + } + : resource.rotationAccountCredentials + } + : { + resourceType: PamResourceType.Kubernetes, + connectionDetails: { + url: "", + namespace: "default", + skipTLSVerify: false, + caCertificate: undefined + } + } + }); + + const { + handleSubmit, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ + + +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PamResourceForm.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PamResourceForm.tsx index a1cc7cb1b..abc23bde3 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PamResourceForm.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PamResourceForm.tsx @@ -9,6 +9,7 @@ import { import { DiscriminativePick } from "@app/types"; import { PamResourceHeader } from "../PamResourceHeader"; +import { KubernetesResourceForm } from "./KubernetesResourceForm"; import { MySQLResourceForm } from "./MySQLResourceForm"; import { PostgresResourceForm } from "./PostgresResourceForm"; import { SSHResourceForm } from "./SSHResourceForm"; @@ -54,6 +55,8 @@ const CreateForm = ({ resourceType, onComplete, projectId }: CreateFormProps) => return ; case PamResourceType.SSH: return ; + case PamResourceType.Kubernetes: + return ; default: throw new Error(`Unhandled resource: ${resourceType}`); } @@ -84,6 +87,8 @@ const UpdateForm = ({ resource, onComplete }: UpdateFormProps) => { return ; case PamResourceType.SSH: return ; + case PamResourceType.Kubernetes: + return ; default: throw new Error(`Unhandled resource: ${(resource as any).resourceType}`); } diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/KubernetesResourceFields.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/KubernetesResourceFields.tsx new file mode 100644 index 000000000..28795b663 --- /dev/null +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/KubernetesResourceFields.tsx @@ -0,0 +1,79 @@ +import { Controller, useFormContext } from "react-hook-form"; + +import { FormControl, Input, Switch, TextArea } from "@app/components/v2"; + +export const KubernetesResourceFields = () => { + const { control, watch } = useFormContext(); + + const skipTLSVerify = watch("connectionDetails.skipTLSVerify"); + + return ( +
+
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + Skip TLS Verification + + + )} + /> + ( + +