diff --git a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts index 2697d2ce0..02fadb5a5 100644 --- a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts +++ b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts @@ -1,5 +1,4 @@ import { Client as ElasticCacheClient } from "@elastic/elasticsearch"; -import handlebars from "handlebars"; import { customAlphabet } from "nanoid"; import { z } from "zod"; @@ -18,40 +17,6 @@ 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(); @@ -72,13 +37,6 @@ export const ElasticSearchDatabaseProvider = (): TDynamicProviderFns => { 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; }; @@ -123,25 +81,20 @@ export const ElasticSearchDatabaseProvider = (): TDynamicProviderFns => { return infoResponse; }; - const create = async (inputs: unknown, expireAt: number) => { + const create = async (inputs: unknown) => { 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 })({ + await connection.security.putUser({ username, password, - expiration + full_name: "Managed by Infisical.com", + roles: providerInputs.roles }); - const parsedStatement = CreateElasticSearchUserSchema.parse(parseJsonStatement(creationStatement)); - - await connection.security.putUser(parsedStatement); - await connection.close(); return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; }; @@ -150,15 +103,12 @@ export const ElasticSearchDatabaseProvider = (): TDynamicProviderFns => { 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.security.deleteUser({ + username: entityId + }); await connection.close(); - return { entityId: username }; + return { entityId }; }; const renew = async (inputs: unknown, entityId: string) => { diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 19d77fb97..ca360c750 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -33,6 +33,7 @@ export const DynamicSecretAwsElastiCacheSchema = z.object({ 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", [ @@ -48,8 +49,6 @@ export const DynamicSecretElasticSearchSchema = z.object({ }) ]), - creationStatement: z.string().trim(), - revocationStatement: z.string().trim(), ca: z.string().optional() }); diff --git a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx index 96c40fad1..c4e6633e3 100644 --- a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx +++ b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx @@ -49,6 +49,10 @@ The Infisical Elastic Search dynamic secret allows you to generate Elastic Searc 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. @@ -73,24 +77,9 @@ The Infisical Elastic Search dynamic secret allows you to generate Elastic Searc A CA may be required if your DB requires it for incoming connections. This is often the case when connecting to a managed service. - - - If you want to provide specific privileges for the generated dynamic credentials, you can modify the Elastic Search statement to your needs. This is useful if you want to only give access to a specific table(s). + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-input-modal-elastic-search.png) - **Valid JSON attributes for creation statement:** - - `username` (string): The username of new user that is created when a lease is provisioned. Setting this to `"{{username}}"` will automatically generate a new username for each lease, which is highly recommended. - - `password` (string): The password of the new user that is created when a lease is provisioned. Setting this to `"{{password}}"` will automatically generate a new password for each lease, which is highly recommended. - - `refresh` (`true`, `false`, `wait_for`): The refresh state for the newly created user. [Read more here](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-user.html#security-api-put-user-query-params). Defaults to true. - - `email` (string): The email of the new user that is created when a lease is provisioned. By default this field is unset. - - `full_name` (string): The full name of the new user that is created when a lease is provisioned. Defaults to "Managed by Infisical.com". - - `metadata` (object): Additional metadata to be associated with the new user that is created when a lease is provisioned. By default this field is an empty object. - - `roles` (array of strings): 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. - - **Valid JSON attributes for revocation statement:** - - `username` (string): The username of the user that is being revoked. This is a required field.. - - ![Modify ElasticSearch Statements Modal](/images/platform/dynamic-secrets/modify-elastic-search-statement.png) After submitting the form, you will see a dynamic secret created in the dashboard. 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/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index db852c333..b79c8f02e 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -122,9 +122,8 @@ export type TDynamicSecretProvider = inputs: { host: string; port: number; - creationStatement: string; - revocationStatement: string; ca?: string | undefined; + roles: string[]; auth: | { diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx index c0be1a548..ac232a725 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx @@ -1,4 +1,7 @@ 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"; @@ -6,17 +9,14 @@ import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, Button, FormControl, + FormLabel, + IconButton, Input, SecretInput, Select, - SelectItem, - TextArea + SelectItem } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; @@ -51,8 +51,7 @@ const formSchema = z.object({ }) ]), - creationStatement: z.string().trim(), - revocationStatement: 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) => { @@ -107,17 +106,8 @@ export const ElasticSearchInputForm = ({ auth: { type: "user" }, - port: 443, - - creationStatement: `{ - "username": "{{username}}", - "password": "{{password}}", - "roles": ["superuser"], - "metadata": {} -}`, - revocationStatement: `{ - "username": "{{username}}" -}` + roles: ["superuser"], + port: 443 } } }); @@ -147,6 +137,7 @@ export const ElasticSearchInputForm = ({ }; const selectedAuthType = watch("provider.auth.type"); + const selectedRoles = watch("provider.roles"); return (
@@ -219,7 +210,10 @@ export const ElasticSearchInputForm = ({ isError={Boolean(error?.message)} errorText={error?.message} > - + )} /> @@ -314,6 +308,87 @@ export const ElasticSearchInputForm = ({ )} />
+ +
+ +

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) + ); + } + }} + > + + +
+
+ )} + /> + ))} +
+ +
+ +
)} /> - - - Modify ElasticSearch Statements - - ( - -