Requested changes

This commit is contained in:
Daniel Hougaard
2024-09-04 13:40:00 +04:00
parent 18951b99de
commit b7c79fa45b
7 changed files with 201 additions and 196 deletions

View File

@@ -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) => {

View File

@@ -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()
});

View File

@@ -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)_
</ParamField>
<ParamField path="Roles" type="string[]" required>
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.
</ParamField>
<ParamField path="Authentication Method" type="API Key | Username/Password" required>
Select the authentication method you want to use to connect to your Elastic Search instance.
</ParamField>
@@ -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.
</ParamField>
</Step>
<Step title="(Optional) Modify Elastic Search Statements">
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)
</Step>
<Step title="Click `Submit`">
After submitting the form, you will see a dynamic secret created in the dashboard.

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

View File

@@ -122,9 +122,8 @@ export type TDynamicSecretProvider =
inputs: {
host: string;
port: number;
creationStatement: string;
revocationStatement: string;
ca?: string | undefined;
roles: string[];
auth:
| {

View File

@@ -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 (
<div>
@@ -219,7 +210,10 @@ export const ElasticSearchInputForm = ({
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
<Input
placeholder="https://fgy543ws2w35dfh7jdaafa12ha.aws-us-east-1.io"
{...field}
/>
</FormControl>
)}
/>
@@ -314,6 +308,87 @@ export const ElasticSearchInputForm = ({
)}
/>
</div>
<div className="mb-3 flex flex-col">
<FormLabel
className="mb-2"
label="Roles"
tooltipText={
<div className="space-y-4">
<p>Select which role(s) to assign the users provisioned by Infisical.</p>
<p>
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{" "}
<Link
passHref
href="https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-roles.html"
>
<a target="_blank" rel="noopener noreferrer">
<span className="cursor-pointer text-primary-400">
read more about roles here
</span>
</a>
</Link>
.
</p>
<p>
You can also assign custom roles by providing the name of the custom role in
the input field.
</p>
</div>
}
/>
<div className="flex flex-col -space-y-2">
{selectedRoles.map((_, i) => (
<Controller
control={control}
name={`provider.roles.${i}`}
// eslint-disable-next-line react/no-array-index-key
key={`role-${i}`}
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error?.message)} errorText={error?.message}>
<div className="flex h-9 items-center gap-2">
<Input
placeholder="Insert role name, (superuser, kibana_admin, custom_role)"
className="mb-0 flex-grow"
{...field}
/>
<IconButton
isDisabled={selectedRoles.length === 1}
ariaLabel="delete key"
className="h-9"
variant="outline_bg"
onClick={() => {
if (selectedRoles && selectedRoles?.length > 1) {
setValue(
"provider.roles",
selectedRoles.filter((__, idx) => idx !== i)
);
}
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</FormControl>
)}
/>
))}
</div>
</div>
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
className="mb-3"
variant="outline_bg"
onClick={() => {
setValue("provider.roles", [...selectedRoles, ""]);
}}
>
Add Role
</Button>
</div>
<div>
<Controller
control={control}
@@ -332,51 +407,6 @@ export const ElasticSearchInputForm = ({
</FormControl>
)}
/>
<Accordion type="single" collapsible className="mb-2 w-full bg-mineshaft-700">
<AccordionItem value="advance-statements">
<AccordionTrigger>Modify ElasticSearch Statements</AccordionTrigger>
<AccordionContent>
<Controller
control={control}
name="provider.creationStatement"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Creation Statement"
isError={Boolean(error?.message)}
errorText={error?.message}
helperText="username, password and expiration are dynamically provisioned"
>
<TextArea
{...field}
reSize="none"
rows={3}
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="provider.revocationStatement"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Revocation Statement"
isError={Boolean(error?.message)}
errorText={error?.message}
helperText="username is dynamically provisioned"
>
<TextArea
{...field}
reSize="none"
rows={3}
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
/>
</FormControl>
)}
/>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
</div>

View File

@@ -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 { useUpdateDynamicSecret } from "@app/hooks/api";
import { TDynamicSecret } 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) => {
@@ -147,6 +146,7 @@ export const EditDynamicSecretElasticSearchForm = ({
};
const selectedAuthType = watch("inputs.auth.type");
const selectedRoles = watch("inputs.roles");
return (
<div>
@@ -310,6 +310,89 @@ export const EditDynamicSecretElasticSearchForm = ({
)}
/>
</div>
<div className="relative mb-3 flex flex-col">
<FormLabel
label="Roles"
tooltipText={
<div className="space-y-4">
<p>Select which role(s) to assign the users provisioned by Infisical.</p>
<p>
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{" "}
<Link
passHref
href="https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-roles.html"
>
<a target="_blank" rel="noopener noreferrer">
<span className="cursor-pointer text-primary-400">
read more about roles here
</span>
</a>
</Link>
.
</p>
<p>
You can also assign custom roles by providing the name of the custom role in
the input field.
</p>
</div>
}
/>
{selectedRoles.map((_, i) => (
<Controller
control={control}
name={`inputs.roles.${i}`}
// eslint-disable-next-line react/no-array-index-key
key={`role-${i}`}
render={({ field, fieldState: { error } }) => (
<div className="flex items-center gap-2">
<div className="flex-grow">
<FormControl isError={Boolean(error?.message)} errorText={error?.message}>
<Input
placeholder="Insert role name, (superuser, kibana_admin, custom_role)"
className="mb-0 flex-grow"
{...field}
/>
</FormControl>
</div>
<IconButton
isDisabled={selectedRoles.length === 1}
ariaLabel="delete key"
className="bottom-2 h-9"
variant="outline_bg"
onClick={() => {
if (selectedRoles && selectedRoles?.length > 1) {
setValue(
"inputs.roles",
selectedRoles.filter((__, idx) => idx !== i)
);
}
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
)}
/>
))}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
className="bottom-2"
variant="outline_bg"
onClick={() => {
setValue("inputs.roles", [...selectedRoles, ""]);
}}
>
Add Role
</Button>
</div>
</div>
<div>
<Controller
control={control}
@@ -328,51 +411,6 @@ export const EditDynamicSecretElasticSearchForm = ({
</FormControl>
)}
/>
<Accordion type="single" collapsible className="mb-2 w-full bg-mineshaft-700">
<AccordionItem value="advance-statements">
<AccordionTrigger>Modify ElasticSearch Statements</AccordionTrigger>
<AccordionContent>
<Controller
control={control}
name="inputs.creationStatement"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Creation Statement"
isError={Boolean(error?.message)}
errorText={error?.message}
helperText="username, password and expiration are dynamically provisioned"
>
<TextArea
{...field}
reSize="none"
rows={3}
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="inputs.revocationStatement"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Revocation Statement"
isError={Boolean(error?.message)}
errorText={error?.message}
helperText="username is dynamically provisioned"
>
<TextArea
{...field}
reSize="none"
rows={3}
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
/>
</FormControl>
)}
/>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
</div>