From 5f503949eb813283da4dd7982fe1893a4252eb5a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 4 Sep 2024 08:34:45 +0400 Subject: [PATCH 01/12] Installed elasticsearch SDK --- backend/package-lock.json | 82 +++++++++++++++++++++++++++++++++++++++ backend/package.json | 1 + 2 files changed, 83 insertions(+) diff --git a/backend/package-lock.json b/backend/package-lock.json index 0a879d934..cd521119c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,6 +15,7 @@ "@aws-sdk/client-secrets-manager": "^3.504.0", "@aws-sdk/client-sts": "^3.600.0", "@casl/ability": "^6.5.0", + "@elastic/elasticsearch": "^8.15.0", "@fastify/cookie": "^9.3.1", "@fastify/cors": "^8.5.0", "@fastify/etag": "^5.1.0", @@ -3763,6 +3764,60 @@ "node": ">=12" } }, + "node_modules/@elastic/elasticsearch": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.15.0.tgz", + "integrity": "sha512-mG90EMdTDoT6GFSdqpUAhWK9LGuiJo6tOWqs0Usd/t15mPQDj7ZqHXfCBqNkASZpwPZpbAYVjd57S6nbUBINCg==", + "license": "Apache-2.0", + "dependencies": { + "@elastic/transport": "^8.7.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@elastic/transport": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-8.7.1.tgz", + "integrity": "sha512-2eeMVkz57Ayxv+UAZkIKzzrUu7nm96jr3+N3kLfbBqALYe2jwDpLr9pR0jc/x9HyJKAM909YGaNlHFDZeb0+Mw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "1.x", + "debug": "^4.3.4", + "hpagent": "^1.0.0", + "ms": "^2.1.3", + "secure-json-parse": "^2.4.0", + "tslib": "^2.4.0", + "undici": "^6.12.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@elastic/transport/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@elastic/transport/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", @@ -5259,6 +5314,15 @@ "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.1.0.tgz", "integrity": "sha512-y92CpG4kFFtBBjni8LHoV12IegJ+KFxLgKRengrVjKmGE5XMeCuGvlfRe75lTRrgXaG6XIWJlFpIDTlkoJsU8w==" }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@peculiar/asn1-cms": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.3.8.tgz", @@ -11618,6 +11682,15 @@ "node": ">= 6" } }, + "node_modules/hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -17454,6 +17527,15 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/undici": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", + "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/backend/package.json b/backend/package.json index 398dcbf26..91f64c53d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -112,6 +112,7 @@ "@aws-sdk/client-secrets-manager": "^3.504.0", "@aws-sdk/client-sts": "^3.600.0", "@casl/ability": "^6.5.0", + "@elastic/elasticsearch": "^8.15.0", "@fastify/cookie": "^9.3.1", "@fastify/cors": "^8.5.0", "@fastify/etag": "^5.1.0", From 9056d1be0c6f593b2fdcb474ff5f60b76262007b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 4 Sep 2024 08:36:02 +0400 Subject: [PATCH 02/12] feat(dynamic-secrets): ElasticSearch support --- .../providers/elastic-search.ts | 177 ++++++++++++++++++ .../dynamic-secret/providers/index.ts | 4 +- .../dynamic-secret/providers/models.ts | 29 ++- 3 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 backend/src/ee/services/dynamic-secret/providers/elastic-search.ts diff --git a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts new file mode 100644 index 000000000..7429a78e3 --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts @@ -0,0 +1,177 @@ +/* eslint-disable no-console */ +import { Client as ElasticCacheClient } from "@elastic/elasticsearch"; +import handlebars from "handlebars"; +import { customAlphabet } from "nanoid"; +import { z } from "zod"; + +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError } from "@app/lib/errors"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +import { DynamicSecretElasticSearchSchema, TDynamicProviderFns } from "./models"; + +const generatePassword = () => { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + return customAlphabet(charset, 64)(); +}; + +const generateUsername = () => { + return alphaNumericNanoId(32); +}; + +const parseJsonStatement = (str: string) => { + try { + return JSON.parse(str) as object; + } catch { + throw new BadRequestError({ message: "Failed to parse ElasticSearch statements" }); + } +}; + +const CreateElasticSearchUserSchema = z + .object({ + username: z.string(), + password: z.string().optional(), + password_hash: z.string().optional(), + + refresh: z.any(), + email: z.string().optional(), + full_name: z.string().optional().default("Managed by Infisical.com"), // We are overriding this + metadata: z.any().optional(), + + roles: z.array(z.string()).min(1), // i.e ['superuser'] + enabled: z.boolean().default(true) + }) + .refine((data) => { + // Ensure either password_hash or password is present + if (!data.password && !data.password_hash) { + throw new Error("Either password or password_hash is required"); + } + return true; + }); + +const DeleteElasticSearchUserSchema = z.object({ + username: z.string().trim().min(1) +}); + +export const ElasticSearchDatabaseProvider = (): TDynamicProviderFns => { + const validateProviderInputs = async (inputs: unknown) => { + const appCfg = getConfig(); + const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not + + const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs); + if ( + isCloud && + // localhost + // internal ips + (providerInputs.host === "host.docker.internal" || + providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) || + providerInputs.host.match(/^192\.168\.\d+\.\d+/)) + ) { + throw new BadRequestError({ message: "Invalid db host" }); + } + if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") { + throw new BadRequestError({ message: "Invalid db host" }); + } + + if (!CreateElasticSearchUserSchema.safeParse(parseJsonStatement(providerInputs.creationStatement)).success) { + throw new BadRequestError({ message: "Invalid creation statement" }); + } + if (!DeleteElasticSearchUserSchema.safeParse(parseJsonStatement(providerInputs.revocationStatement)).success) { + throw new BadRequestError({ message: "Invalid revocation statement" }); + } + + return providerInputs; + }; + + const getClient = async (providerInputs: z.infer) => { + const connection = new ElasticCacheClient({ + node: { + url: new URL(`${providerInputs.host}:${providerInputs.port}`), + ...(providerInputs.ca && { + ssl: { + rejectUnauthorized: false, + ca: providerInputs.ca + } + }) + }, + auth: { + ...(providerInputs.auth.type === "api-key" + ? { + apiKey: { + api_key: providerInputs.auth.apiKey, + id: providerInputs.auth.apiKeyId + } + } + : { + username: providerInputs.auth.username, + password: providerInputs.auth.password + }) + } + }); + + return connection; + }; + + const validateConnection = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + const connection = await getClient(providerInputs); + + const infoResponse = await connection + .info() + .then(() => true) + .catch(() => false); + + return infoResponse; + }; + + const create = async (inputs: unknown, expireAt: number) => { + const providerInputs = await validateProviderInputs(inputs); + const connection = await getClient(providerInputs); + + const username = generateUsername(); + const password = generatePassword(); + + const expiration = new Date(expireAt).toISOString(); + + const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({ + username, + password, + expiration + }); + + const parsedStatement = CreateElasticSearchUserSchema.parse(parseJsonStatement(creationStatement)); + + await connection.security.putUser(parsedStatement); + + await connection.close(); + return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + }; + + const revoke = async (inputs: unknown, entityId: string) => { + const providerInputs = await validateProviderInputs(inputs); + const connection = await getClient(providerInputs); + + const username = entityId; + + const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username }); + const parsedStatement = DeleteElasticSearchUserSchema.parse(parseJsonStatement(revokeStatement)); + + await connection.security.deleteUser(parsedStatement); + + await connection.close(); + return { entityId: username }; + }; + + const renew = async (inputs: unknown, entityId: string) => { + // Do nothing + return { entityId }; + }; + + return { + validateProviderInputs, + validateConnection, + create, + revoke, + renew + }; +}; diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts index 0eb613df2..23769df32 100644 --- a/backend/src/ee/services/dynamic-secret/providers/index.ts +++ b/backend/src/ee/services/dynamic-secret/providers/index.ts @@ -1,6 +1,7 @@ import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache"; import { AwsIamProvider } from "./aws-iam"; import { CassandraProvider } from "./cassandra"; +import { ElasticSearchDatabaseProvider } from "./elastic-search"; import { DynamicSecretProviders } from "./models"; import { MongoAtlasProvider } from "./mongo-atlas"; import { RedisDatabaseProvider } from "./redis"; @@ -12,5 +13,6 @@ export const buildDynamicSecretProviders = () => ({ [DynamicSecretProviders.AwsIam]: AwsIamProvider(), [DynamicSecretProviders.Redis]: RedisDatabaseProvider(), [DynamicSecretProviders.AwsElastiCache]: AwsElastiCacheDatabaseProvider(), - [DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider() + [DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(), + [DynamicSecretProviders.ElasticSearch]: ElasticSearchDatabaseProvider() }); diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 06bc6a9ed..19d77fb97 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -30,6 +30,29 @@ export const DynamicSecretAwsElastiCacheSchema = z.object({ ca: z.string().optional() }); +export const DynamicSecretElasticSearchSchema = z.object({ + host: z.string().trim().min(1), + port: z.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() +}); + export const DynamicSecretSqlDBSchema = z.object({ client: z.nativeEnum(SqlProviders), host: z.string().trim().toLowerCase(), @@ -110,7 +133,8 @@ export enum DynamicSecretProviders { AwsIam = "aws-iam", Redis = "redis", AwsElastiCache = "aws-elasticache", - MongoAtlas = "mongo-db-atlas" + MongoAtlas = "mongo-db-atlas", + ElasticSearch = "elastic-search" } export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ @@ -119,7 +143,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }), z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }), z.object({ type: z.literal(DynamicSecretProviders.AwsElastiCache), inputs: DynamicSecretAwsElastiCacheSchema }), - z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }) + z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }), + z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }) ]); export type TDynamicProviderFns = { From d93cbb023d036df7c4b90b78c934e55c6f128bed Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 4 Sep 2024 08:36:13 +0400 Subject: [PATCH 03/12] Update redis.ts --- backend/src/ee/services/dynamic-secret/providers/redis.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/ee/services/dynamic-secret/providers/redis.ts b/backend/src/ee/services/dynamic-secret/providers/redis.ts index 23eb454c5..1338e62e3 100644 --- a/backend/src/ee/services/dynamic-secret/providers/redis.ts +++ b/backend/src/ee/services/dynamic-secret/providers/redis.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import handlebars from "handlebars"; import { Redis } from "ioredis"; import { customAlphabet } from "nanoid"; From 5df140cbd5275fba87bb3009c6a7322b1d5d0183 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 4 Sep 2024 08:37:25 +0400 Subject: [PATCH 04/12] 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 + + ( + +