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", 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..2ead58bef --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts @@ -0,0 +1,126 @@ +import { Client as ElasticSearchClient } from "@elastic/elasticsearch"; +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, ElasticSearchAuthTypes, TDynamicProviderFns } from "./models"; + +const generatePassword = () => { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + return customAlphabet(charset, 64)(); +}; + +const generateUsername = () => { + return alphaNumericNanoId(32); +}; + +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" }); + } + + return providerInputs; + }; + + const getClient = async (providerInputs: z.infer) => { + const connection = new ElasticSearchClient({ + node: { + url: new URL(`${providerInputs.host}:${providerInputs.port}`), + ...(providerInputs.ca && { + ssl: { + rejectUnauthorized: false, + ca: providerInputs.ca + } + }) + }, + auth: { + ...(providerInputs.auth.type === ElasticSearchAuthTypes.ApiKey + ? { + 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) => { + const providerInputs = await validateProviderInputs(inputs); + const connection = await getClient(providerInputs); + + const username = generateUsername(); + const password = generatePassword(); + + await connection.security.putUser({ + username, + password, + full_name: "Managed by Infisical.com", + roles: providerInputs.roles + }); + + 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); + + await connection.security.deleteUser({ + username: entityId + }); + + await connection.close(); + return { entityId }; + }; + + 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..06069c981 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -7,6 +7,11 @@ export enum SqlProviders { MsSQL = "mssql" } +export enum ElasticSearchAuthTypes { + User = "user", + ApiKey = "api-key" +} + export const DynamicSecretRedisDBSchema = z.object({ host: z.string().trim().toLowerCase(), port: z.number(), @@ -30,6 +35,28 @@ export const DynamicSecretAwsElastiCacheSchema = z.object({ ca: z.string().optional() }); +export const DynamicSecretElasticSearchSchema = z.object({ + host: z.string().trim().min(1), + port: z.number(), + roles: z.array(z.string().trim().min(1)).min(1), + + // two auth types "user, apikey" + auth: z.discriminatedUnion("type", [ + z.object({ + type: z.literal(ElasticSearchAuthTypes.User), + username: z.string().trim(), + password: z.string().trim() + }), + z.object({ + type: z.literal(ElasticSearchAuthTypes.ApiKey), + apiKey: z.string().trim(), + apiKeyId: z.string().trim() + }) + ]), + + ca: z.string().optional() +}); + export const DynamicSecretSqlDBSchema = z.object({ client: z.nativeEnum(SqlProviders), host: z.string().trim().toLowerCase(), @@ -110,7 +137,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 +147,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 = { 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"; diff --git a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx index d02f47858..c37de08ff 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx @@ -1,9 +1,9 @@ --- -title: "AWS Elasticahe" -description: "Learn how to dynamically generate Redis Database user credentials." +title: "AWS ElastiCache" +description: "Learn how to dynamically generate AWS ElastiCache user credentials." --- -The Infisical Redis dynamic secret allows you to generate Redis Database credentials on demand based on configured role. +The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCache credentials on demand based on configured role. ## Prerequisites @@ -38,7 +38,7 @@ The Infisical Redis dynamic secret allows you to generate Redis Database credent New leases may take up-to a couple of minutes before ElastiCache has the chance to complete their configuration. - It is recommended to use a retry strategy when establishing new Redis ElastiCache connections. + It is recommended to use a retry strategy when establishing new ElastiCache connections. This may prevent errors when trying to use a password that isn't yet live on the targeted ElastiCache cluster. While a leasing is being created, you will be unable to create new leases for the same dynamic secret. @@ -51,7 +51,7 @@ The Infisical Redis dynamic secret allows you to generate Redis Database credent -## Set up Dynamic Secrets with Redis +## Set up Dynamic Secrets with AWS ElastiCache @@ -60,8 +60,8 @@ The Infisical Redis dynamic secret allows you to generate Redis Database credent ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png) - - ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-elasti-cache) + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-elasti-cache.png) diff --git a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx new file mode 100644 index 000000000..c4e6633e3 --- /dev/null +++ b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx @@ -0,0 +1,127 @@ +--- +title: "Elastic Search" +description: "Learn how to dynamically generate Elastic Search user credentials." +--- + +The Infisical Elastic Search dynamic secret allows you to generate Elastic Search credentials on demand based on configured role. + +## Prerequisites + + + +1. Create a role with at least `manage_security` and `monitor` permissions. +2. Assign the newly created role to your API key or user that you'll use later in the dynamic secret configuration. + + + For testing purposes, you can also use a highly privileged role like `superuser`, that will have full control over the cluster. This is not recommended in production environments following the principle of least privilege. + + +## Set up Dynamic Secrets with Elastic Search + + + + Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-elastic-search.png) + + + + Name by which you want the secret to be referenced + + + + Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + + + + Maximum time-to-live for a generated secret. + + + + Your Elastic Search host. This is the endpoint that your instance runs on. _(Example: https://your-cluster-ip)_ + + + + The port that your Elastic Search instance is running on. _(Example: 9200)_ + + + + The roles that the new user that is created when a lease is provisioned will be assigned to. This is a required field. This defaults to `superuser`, which is highly privileged. It is recommended to create a new role with the least privileges required for the lease. + + + + Select the authentication method you want to use to connect to your Elastic Search instance. + + + + The username of the user that will be used to provision new dynamic secret leases. Only required if you selected the `Username/Password` authentication method. + + + + The password of the user that will be used to provision new dynamic secret leases. Only required if you selected the `Username/Password` authentication method. + + + + The ID of the API key that will be used to provision new dynamic secret leases. Only required if you selected the `API Key` authentication method. + + + + The API key that will be used to provision new dynamic secret leases. Only required if you selected the `API Key` authentication method. + + + + A CA may be required if your DB requires it for incoming connections. This is often the case when connecting to a managed service. + + + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-input-modal-elastic-search.png) + + + + + After submitting the form, you will see a dynamic secret created in the dashboard. + + + If this step fails, you may have to add the CA certificate. + + + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png) + + When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease-redis.png) + + + Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret. + + + + Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/lease-values-elastic-search.png) + + + +## Audit or Revoke Leases +Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. +This will allow you see the expiration time of the lease or delete a lease before it's set time to live. + +![Provision Lease](/images/platform/dynamic-secrets/lease-data-redis.png) + +## Renew Leases +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png) + + + Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret + \ No newline at end of file diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-input-modal-elastic-search.png b/docs/images/platform/dynamic-secrets/dynamic-secret-input-modal-elastic-search.png new file mode 100644 index 000000000..14d2d48b2 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-input-modal-elastic-search.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-modal-elastic-search.png b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-elastic-search.png new file mode 100644 index 000000000..bb21f628c Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-elastic-search.png differ diff --git a/docs/images/platform/dynamic-secrets/lease-values-elastic-search.png b/docs/images/platform/dynamic-secrets/lease-values-elastic-search.png new file mode 100644 index 000000000..7d5685a81 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/lease-values-elastic-search.png differ diff --git a/docs/images/platform/dynamic-secrets/modify-elastic-search-statement.png b/docs/images/platform/dynamic-secrets/modify-elastic-search-statement.png new file mode 100644 index 000000000..9315a4077 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/modify-elastic-search-statement.png differ diff --git a/docs/mint.json b/docs/mint.json index e7db8ce6c..b56493e01 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -156,6 +156,7 @@ "documentation/platform/dynamic-secrets/cassandra", "documentation/platform/dynamic-secrets/redis", "documentation/platform/dynamic-secrets/aws-elasticache", + "documentation/platform/dynamic-secrets/elastic-search", "documentation/platform/dynamic-secrets/aws-iam", "documentation/platform/dynamic-secrets/mongo-atlas" ] diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 7076ea929..b79c8f02e 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,27 @@ export type TDynamicSecretProvider = type: string; }[]; }; + } + | { + type: DynamicSecretProviders.ElasticSearch; + inputs: { + host: string; + port: number; + ca?: string | undefined; + roles: string[]; + + 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..ac232a725 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx @@ -0,0 +1,425 @@ +import { Controller, useForm } from "react-hook-form"; +import Link from "next/link"; +import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +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 { + Button, + FormControl, + FormLabel, + IconButton, + Input, + SecretInput, + Select, + SelectItem +} 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() + }) + ]), + + roles: z.array(z.string().trim().min(1)).min(1, "At least one role is required"), + 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" + }, + roles: ["superuser"], + port: 443 + } + } + }); + + 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"); + const selectedRoles = watch("provider.roles"); + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Configuration +
+
+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ +

Select which role(s) to assign the users provisioned by Infisical.

+

+ There is a wide range of in-built roles in Elastic Search. Some include, + superuser, apm_user, kibana_admin, monitoring_user, and many more. You can{" "} + + + + read more about roles here + + + + . +

+

+ You can also assign custom roles by providing the name of the custom role in + the input field. +

+
+ } + /> +
+ {selectedRoles.map((_, i) => ( + ( + +
+ + { + if (selectedRoles && selectedRoles?.length > 1) { + setValue( + "provider.roles", + selectedRoles.filter((__, idx) => idx !== i) + ); + } + }} + > + + +
+
+ )} + /> + ))} +
+
+
+ +
+
+ ( + + + + )} + /> +
+
+
+
+
+ + +
+ +
+ ); +}; diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx index 800cf7747..9b3d069c2 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx @@ -139,6 +139,24 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => { ); } + if (provider === DynamicSecretProviders.ElasticSearch) { + const { DB_USERNAME, DB_PASSWORD } = data as { + DB_USERNAME: string; + DB_PASSWORD: string; + }; + + return ( +
+ + +
+ ); + } + return null; }; diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretElasticSearchForm.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretElasticSearchForm.tsx new file mode 100644 index 000000000..4e1d3b1f3 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretElasticSearchForm.tsx @@ -0,0 +1,429 @@ +import { Controller, useForm } from "react-hook-form"; +import Link from "next/link"; +import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +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 { + Button, + FormControl, + FormLabel, + IconButton, + Input, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { useUpdateDynamicSecret } from "@app/hooks/api"; +import { TDynamicSecret } 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({ + inputs: 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() + }) + ]), + + roles: z.array(z.string().trim().min(1)).min(1, "At least one role is required"), + 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" }); + }), + newName: z + .string() + .refine((val) => val.toLowerCase() === val, "Must be lowercase") + .optional() +}); +type TForm = z.infer; + +type Props = { + onClose: () => void; + dynamicSecret: TDynamicSecret & { inputs: unknown }; + secretPath: string; + environment: string; + projectSlug: string; +}; + +export const EditDynamicSecretElasticSearchForm = ({ + onClose, + dynamicSecret, + secretPath, + environment, + projectSlug +}: Props) => { + const { + control, + formState: { isSubmitting }, + handleSubmit, + setValue, + watch + } = useForm({ + resolver: zodResolver(formSchema), + values: { + defaultTTL: dynamicSecret.defaultTTL, + maxTTL: dynamicSecret.maxTTL, + newName: dynamicSecret.name, + inputs: { + ...(dynamicSecret.inputs as TForm["inputs"]) + } + } + }); + + const updateDynamicSecret = useUpdateDynamicSecret(); + + const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => { + // wait till previous request is finished + if (updateDynamicSecret.isLoading) return; + try { + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs, + newName: newName === dynamicSecret.name ? undefined : newName + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to update dynamic secret" + }); + } + }; + + const selectedAuthType = watch("inputs.auth.type"); + const selectedRoles = watch("inputs.roles"); + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Configuration +
+
+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ +

Select which role(s) to assign the users provisioned by Infisical.

+

+ There is a wide range of in-built roles in Elastic Search. Some include, + superuser, apm_user, kibana_admin, monitoring_user, and many more. You can{" "} + + + + read more about roles here + + + + . +

+

+ You can also assign custom roles by providing the name of the custom role in + the input field. +

+
+ } + /> + {selectedRoles.map((_, i) => ( + ( +
+
+ + + +
+ + { + if (selectedRoles && selectedRoles?.length > 1) { + setValue( + "inputs.roles", + selectedRoles.filter((__, idx) => idx !== i) + ); + } + }} + > + + +
+ )} + /> + ))} + +
+ +
+
+ +
+ ( + + + + )} + /> +
+
+
+
+
+ + +
+ +
+ ); +}; diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx index 344d8183d..f0a4fd957 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx @@ -7,6 +7,7 @@ import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; import { EditDynamicSecretAwsElastiCacheProviderForm } from "./EditDynamicSecretAwsElastiCacheProviderForm"; import { EditDynamicSecretAwsIamForm } from "./EditDynamicSecretAwsIamForm"; import { EditDynamicSecretCassandraForm } from "./EditDynamicSecretCassandraForm"; +import { EditDynamicSecretElasticSearchForm } from "./EditDynamicSecretElasticSearchForm"; import { EditDynamicSecretMongoAtlasForm } from "./EditDynamicSecretMongoAtlasForm"; import { EditDynamicSecretRedisProviderForm } from "./EditDynamicSecretRedisProviderForm"; import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm"; @@ -146,6 +147,24 @@ export const EditDynamicSecretForm = ({ /> )} + + {dynamicSecretDetails?.type === DynamicSecretProviders.ElasticSearch && ( + + + + )} ); };