From 5df140cbd5275fba87bb3009c6a7322b1d5d0183 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 4 Sep 2024 08:37:25 +0400 Subject: [PATCH] feat(dynamic-secrets): ElasticSearch support --- frontend/src/hooks/api/dynamicSecret/types.ts | 29 +- .../CreateDynamicSecretForm.tsx | 33 +- .../ElasticSearchInputForm.tsx | 395 ++++++++++++++++++ .../CreateDynamicSecretLease.tsx | 18 + .../EditDynamicSecretElasticSearchForm.tsx | 391 +++++++++++++++++ .../EditDynamicSecretForm.tsx | 19 + 6 files changed, 878 insertions(+), 7 deletions(-) create mode 100644 frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx create mode 100644 frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretElasticSearchForm.tsx diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 7076ea929..db852c333 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -21,7 +21,8 @@ export enum DynamicSecretProviders { AwsIam = "aws-iam", Redis = "redis", AwsElastiCache = "aws-elasticache", - MongoAtlas = "mongo-db-atlas" + MongoAtlas = "mongo-db-atlas", + ElasticSearch = "elastic-search" } export enum SqlProviders { @@ -97,9 +98,9 @@ export type TDynamicSecretProvider = creationStatement: string; revocationStatement: string; ca?: string | undefined; - } + }; } -|{ + | { type: DynamicSecretProviders.MongoAtlas; inputs: { adminPublicKey: string; @@ -115,6 +116,28 @@ export type TDynamicSecretProvider = type: string; }[]; }; + } + | { + type: DynamicSecretProviders.ElasticSearch; + inputs: { + host: string; + port: number; + creationStatement: string; + revocationStatement: string; + ca?: string | undefined; + + auth: + | { + type: "user"; + username: string; + password: string; + } + | { + type: "api-key"; + apiKey: string; + apiKeyId: string; + }; + }; }; export type TCreateDynamicSecretDTO = { diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx index 52b3170a2..71b25e63c 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; -import { SiApachecassandra,SiMongodb } from "react-icons/si"; +import { DiRedis } from "react-icons/di"; +import { SiApachecassandra, SiElasticsearch, SiMongodb } from "react-icons/si"; import { faAws } from "@fortawesome/free-brands-svg-icons"; import { faDatabase } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -11,6 +12,7 @@ import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm"; import { AwsIamInputForm } from "./AwsIamInputForm"; import { CassandraInputForm } from "./CassandraInputForm"; +import { ElasticSearchInputForm } from "./ElasticSearchInputForm"; import { MongoAtlasInputForm } from "./MongoAtlasInputForm"; import { RedisInputForm } from "./RedisInputForm"; import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm"; @@ -40,12 +42,12 @@ const DYNAMIC_SECRET_LIST = [ title: "Cassandra" }, { - icon: , + icon: , provider: DynamicSecretProviders.Redis, title: "Redis" }, { - icon: , + icon: , provider: DynamicSecretProviders.AwsElastiCache, title: "AWS ElastiCache" }, @@ -58,6 +60,11 @@ const DYNAMIC_SECRET_LIST = [ icon: , provider: DynamicSecretProviders.MongoAtlas, title: "Mongo Atlas" + }, + { + icon: , + provider: DynamicSecretProviders.ElasticSearch, + title: "Elastic Search" } ]; @@ -94,7 +101,7 @@ export const CreateDynamicSecretForm = ({ exit={{ opacity: 0, translateX: -30 }} >
Select a service to connect to:
-
+
{DYNAMIC_SECRET_LIST.map(({ icon, provider, title }) => (
)} + {wizardStep === WizardSteps.ProviderInputs && + selectedProvider === DynamicSecretProviders.ElasticSearch && ( + + + + )} diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx new file mode 100644 index 000000000..c0be1a548 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx @@ -0,0 +1,395 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import ms from "ms"; +import { z } from "zod"; + +import { TtlFormLabel } from "@app/components/features"; +import { createNotification } from "@app/components/notifications"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Button, + FormControl, + Input, + SecretInput, + Select, + SelectItem, + TextArea +} from "@app/components/v2"; +import { useCreateDynamicSecret } from "@app/hooks/api"; +import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; + +const authMethods = [ + { + label: "Username/Password", + value: "user" + }, + { + label: "API Key", + value: "api-key" + } +] as const; + +const formSchema = z.object({ + provider: z.object({ + host: z.string().trim().min(1), + port: z.coerce.number(), + + // two auth types "user, apikey" + auth: z.discriminatedUnion("type", [ + z.object({ + type: z.literal("user"), + username: z.string().trim(), + password: z.string().trim() + }), + z.object({ + type: z.literal("api-key"), + apiKey: z.string().trim(), + apiKeyId: z.string().trim() + }) + ]), + + creationStatement: z.string().trim(), + revocationStatement: z.string().trim(), + ca: z.string().optional() + }), + defaultTTL: z.string().superRefine((val, ctx) => { + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") +}); +type TForm = z.infer; + +type Props = { + onCompleted: () => void; + onCancel: () => void; + secretPath: string; + projectSlug: string; + environment: string; +}; + +export const ElasticSearchInputForm = ({ + onCompleted, + onCancel, + environment, + secretPath, + projectSlug +}: Props) => { + const { + control, + formState: { isSubmitting }, + handleSubmit, + setValue, + watch + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + provider: { + auth: { + type: "user" + }, + port: 443, + + creationStatement: `{ + "username": "{{username}}", + "password": "{{password}}", + "roles": ["superuser"], + "metadata": {} +}`, + revocationStatement: `{ + "username": "{{username}}" +}` + } + } + }); + + const createDynamicSecret = useCreateDynamicSecret(); + + const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + // wait till previous request is finished + if (createDynamicSecret.isLoading) return; + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.ElasticSearch, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment + }); + onCompleted(); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }; + + const selectedAuthType = watch("provider.auth.type"); + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Configuration +
+
+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+
+ ( + + + + )} + /> + + + Modify ElasticSearch Statements + + ( + +