From 508f697bdd530f0cbcca47f1bfe5a190baecdfd6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 9 Sep 2024 13:54:10 +0400 Subject: [PATCH] feat(dynamic-secrets): RabbitMQ --- .../dynamic-secret/providers/models.ts | 26 +- .../dynamic-secret/providers/rabbit-mq.ts | 179 ++++++++ frontend/src/hooks/api/dynamicSecret/types.ts | 24 +- .../CreateDynamicSecretForm.tsx | 26 +- .../RabbitMqInputForm.tsx | 421 ++++++++++++++++++ .../CreateDynamicSecretLease.tsx | 18 + .../EditDynamicSecretForm.tsx | 19 + .../EditDynamicSecretRabbitMqForm.tsx | 421 ++++++++++++++++++ 8 files changed, 1130 insertions(+), 4 deletions(-) create mode 100644 backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts create mode 100644 frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx create mode 100644 frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRabbitMqForm.tsx diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index ae8f25581..f23a60df7 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -56,6 +56,26 @@ export const DynamicSecretElasticSearchSchema = z.object({ ca: z.string().optional() }); +export const DynamicSecretRabbitMqSchema = z.object({ + host: z.string().trim().min(1), + port: z.number(), + tags: z.array(z.string().trim()).default([]), + + username: z.string().trim().min(1), + password: z.string().trim().min(1), + + ca: z.string().optional(), + + virtualHost: z.object({ + name: z.string().trim().min(1), + permissions: z.object({ + read: z.string().trim().min(1), + write: z.string().trim().min(1), + configure: z.string().trim().min(1) + }) + }) +}); + export const DynamicSecretSqlDBSchema = z.object({ client: z.nativeEnum(SqlProviders), host: z.string().trim().toLowerCase(), @@ -154,7 +174,8 @@ export enum DynamicSecretProviders { AwsElastiCache = "aws-elasticache", MongoAtlas = "mongo-db-atlas", ElasticSearch = "elastic-search", - MongoDB = "mongo-db" + MongoDB = "mongo-db", + RabbitMq = "rabbit-mq" } export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ @@ -165,7 +186,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.AwsElastiCache), inputs: DynamicSecretAwsElastiCacheSchema }), z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }), z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }), - z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }) + z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }), + z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }) ]); export type TDynamicProviderFns = { diff --git a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts new file mode 100644 index 000000000..916ba06b9 --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts @@ -0,0 +1,179 @@ +import axios, { Axios } from "axios"; +import https from "https"; +import { customAlphabet } from "nanoid"; +import { z } from "zod"; + +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { logger } from "@app/lib/logger"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +import { DynamicSecretRabbitMqSchema, TDynamicProviderFns } from "./models"; + +const generatePassword = () => { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + return customAlphabet(charset, 64)(); +}; + +const generateUsername = () => { + return alphaNumericNanoId(32); +}; + +type TCreateRabbitMQUser = { + axiosInstance: Axios; + createUser: { + username: string; + password: string; + tags: string[]; + }; + virtualHost: { + name: string; + permissions: { + read: string; + write: string; + configure: string; + }; + }; +}; + +type TDeleteRabbitMqUser = { + axiosInstance: Axios; + usernameToDelete: string; +}; + +async function createRabbitMqUser({ axiosInstance, createUser, virtualHost }: TCreateRabbitMQUser): Promise { + // const baseUrl = `${removeTrailingSlash(connection.host)}:${connection.port}/api`; + + try { + // Create user + const userUrl = `/users/${createUser.username}`; + const userData = { + password: createUser.password, + tags: createUser.tags.join(",") + }; + + await axiosInstance.put(userUrl, userData); + console.log(`User ${createUser.username} created successfully`); + + // Set permissions for the virtual host + if (virtualHost) { + const permissionData = { + configure: virtualHost.permissions.configure, + write: virtualHost.permissions.write, + read: virtualHost.permissions.read + }; + + await axiosInstance.put( + `/permissions/${encodeURIComponent(virtualHost.name)}/${createUser.username}`, + permissionData + ); + console.log(`Permissions set for user ${createUser.username} on virtual host ${virtualHost.name}`); + } + } catch (error) { + logger.error(error, "Error creating RabbitMQ user"); + throw error; + } +} + +async function deleteRabbitMqUser({ axiosInstance, usernameToDelete }: TDeleteRabbitMqUser) { + await axiosInstance.delete(`users/${usernameToDelete}`); + return { username: usernameToDelete }; +} + +export const RabbitMqProvider = (): 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 DynamicSecretRabbitMqSchema.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 axiosInstance = axios.create({ + baseURL: `${removeTrailingSlash(providerInputs.host)}:${providerInputs.port}/api`, + auth: { + username: providerInputs.username, + password: providerInputs.password + }, + headers: { + "Content-Type": "application/json" + }, + + ...(providerInputs.ca && { + httpsAgent: new https.Agent({ ca: providerInputs.ca, rejectUnauthorized: false }) + }) + }); + + return axiosInstance; + }; + + const validateConnection = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + const connection = await getClient(providerInputs); + + const infoResponse = await connection + .get("/whoami") + .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 createRabbitMqUser({ + axiosInstance: connection, + virtualHost: providerInputs.virtualHost, + createUser: { + password, + username, + tags: [...(providerInputs.tags ?? []), "infisical-user"] + } + }); + + 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 deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId }); + + return { entityId }; + }; + + const renew = async (inputs: unknown, entityId: string) => { + // Do nothing + return { entityId }; + }; + + return { + validateProviderInputs, + validateConnection, + create, + revoke, + renew + }; +}; diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index e7e9e3318..32c9b15d0 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -23,7 +23,8 @@ export enum DynamicSecretProviders { AwsElastiCache = "aws-elasticache", MongoAtlas = "mongo-db-atlas", ElasticSearch = "elastic-search", - MongoDB = "mongo-db" + MongoDB = "mongo-db", + RabbitMq = "rabbit-mq" } export enum SqlProviders { @@ -155,6 +156,27 @@ export type TDynamicSecretProvider = apiKeyId: string; }; }; + } + | { + type: DynamicSecretProviders.RabbitMq; + inputs: { + host: string; + port: number; + + username: string; + password: string; + + tags: string[]; + virtualHost: { + name: string; + permissions: { + configure: string; + write: string; + read: string; + }; + }; + ca?: 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 19c3191a4..90eb74012 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { DiRedis } from "react-icons/di"; -import { SiApachecassandra, SiElasticsearch, SiMongodb } from "react-icons/si"; +import { SiApachecassandra, SiElasticsearch, SiMongodb, SiRabbitmq } 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"; @@ -15,6 +15,7 @@ import { CassandraInputForm } from "./CassandraInputForm"; import { ElasticSearchInputForm } from "./ElasticSearchInputForm"; import { MongoAtlasInputForm } from "./MongoAtlasInputForm"; import { MongoDBDatabaseInputForm } from "./MongoDBInputForm"; +import { RabbitMqInputForm } from "./RabbitMqInputForm"; import { RedisInputForm } from "./RedisInputForm"; import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm"; @@ -71,6 +72,11 @@ const DYNAMIC_SECRET_LIST = [ icon: , provider: DynamicSecretProviders.ElasticSearch, title: "Elastic Search" + }, + { + icon: , + provider: DynamicSecretProviders.RabbitMq, + title: "RabbitMQ" } ]; @@ -276,6 +282,24 @@ export const CreateDynamicSecretForm = ({ /> )} + {wizardStep === WizardSteps.ProviderInputs && + selectedProvider === DynamicSecretProviders.RabbitMq && ( + + + + )} diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx new file mode 100644 index 000000000..dae0cd478 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx @@ -0,0 +1,421 @@ +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 } from "@app/components/v2"; +import { useCreateDynamicSecret } from "@app/hooks/api"; +import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; + +const formSchema = z.object({ + provider: z.object({ + host: z.string().trim().min(1), + port: z.coerce.number(), // important: this is the management plugin port + + username: z.string(), + password: z.string(), + + tags: z.array(z.string().trim()), + virtualHost: z.object({ + name: z.string().trim().min(1), + permissions: z.object({ + read: z.string().trim().min(1), + write: z.string().trim().min(1), + configure: z.string().trim().min(1) + }) + }), + 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 RabbitMqInputForm = ({ + onCompleted, + onCancel, + environment, + secretPath, + projectSlug +}: Props) => { + const { + control, + formState: { isSubmitting }, + handleSubmit, + setValue, + watch + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + provider: { + port: 15672, + virtualHost: { + name: "/", + permissions: { + read: ".*", + write: ".*", + configure: ".*" + } + }, + tags: [] + } + } + }); + + 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.RabbitMq, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment + }); + onCompleted(); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }; + + const selectedTags = watch("provider.tags"); + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Configuration +
+
+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ +
+ ( + + + + )} + /> + + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+
+ +
+ +

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

+

+ There is a wide range of in-built roles in RabbitMQ. Some include, + management, policymaker, monitoring, administrator.
+ + + + Read more about management tags here + + + + . +

+

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

+
+ } + /> +
+ {selectedTags.map((_, i) => ( + ( + +
+ + { + if (selectedTags && selectedTags?.length > 1) { + setValue( + "provider.tags", + selectedTags.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 100e2de91..85be20fc1 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx @@ -140,6 +140,24 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => { ); } + if (provider === DynamicSecretProviders.RabbitMq) { + const { DB_USERNAME, DB_PASSWORD } = data as { + DB_USERNAME: string; + DB_PASSWORD: string; + }; + + return ( +
+ + +
+ ); + } + if (provider === DynamicSecretProviders.ElasticSearch) { const { DB_USERNAME, DB_PASSWORD } = data as { DB_USERNAME: string; diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx index 61efdb42e..8f95bcc94 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx @@ -10,6 +10,7 @@ import { EditDynamicSecretCassandraForm } from "./EditDynamicSecretCassandraForm import { EditDynamicSecretElasticSearchForm } from "./EditDynamicSecretElasticSearchForm"; import { EditDynamicSecretMongoAtlasForm } from "./EditDynamicSecretMongoAtlasForm"; import { EditDynamicSecretMongoDBForm } from "./EditDynamicSecretMongoDBForm"; +import { EditDynamicSecretRabbitMqForm } from "./EditDynamicSecretRabbitMqForm"; import { EditDynamicSecretRedisProviderForm } from "./EditDynamicSecretRedisProviderForm"; import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm"; @@ -183,6 +184,24 @@ export const EditDynamicSecretForm = ({ /> )} + + {dynamicSecretDetails?.type === DynamicSecretProviders.RabbitMq && ( + + + + )} ); }; diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRabbitMqForm.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRabbitMqForm.tsx new file mode 100644 index 000000000..a28c257b0 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRabbitMqForm.tsx @@ -0,0 +1,421 @@ +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 } from "@app/components/v2"; +import { useUpdateDynamicSecret } from "@app/hooks/api"; +import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; + +const formSchema = z.object({ + inputs: z.object({ + host: z.string().trim().min(1), + port: z.coerce.number(), // important: this is the management plugin port + + username: z.string(), + password: z.string(), + + tags: z.array(z.string().trim()), + virtualHost: z.object({ + name: z.string().trim().min(1), + permissions: z.object({ + read: z.string().trim().min(1), + write: z.string().trim().min(1), + configure: z.string().trim().min(1) + }) + }), + 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") +}); +type TForm = z.infer; + +type Props = { + onClose: () => void; + dynamicSecret: TDynamicSecret & { inputs: unknown }; + secretPath: string; + environment: string; + projectSlug: string; +}; +export const EditDynamicSecretRabbitMqForm = ({ + 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 selectedTags = watch("inputs.tags"); + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Configuration +
+
+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ +
+ ( + + + + )} + /> + + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+
+ +
+ +

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

+

+ There is a wide range of in-built roles in RabbitMQ. Some include, + management, policymaker, monitoring, administrator.
+ + + + Read more about management tags here + + + + . +

+

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

+
+ } + /> +
+ {selectedTags.map((_, i) => ( + ( + +
+ + { + if (selectedTags && selectedTags?.length > 1) { + setValue( + "inputs.tags", + selectedTags.filter((__, idx) => idx !== i) + ); + } + }} + > + + +
+
+ )} + /> + ))} +
+
+
+ +
+
+ ( + + + + )} + /> +
+
+
+
+
+ + +
+ + + ); +};