1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-24 00:15:26 +00:00

Compare commits

..

13 Commits

Author SHA1 Message Date
096417281e Update rabbit-mq.ts 2024-09-10 11:21:52 +04:00
763a96faf8 Update rabbit-mq.ts 2024-09-10 11:21:52 +04:00
870eaf9301 docs(dynamic-secrets): rabbit mq 2024-09-10 11:21:52 +04:00
10abf192a1 chore(docs): cleanup incorrectly formatted images 2024-09-10 11:21:52 +04:00
508f697bdd feat(dynamic-secrets): RabbitMQ 2024-09-10 11:21:52 +04:00
8ea8a6f72e Fix: ElasticSearch provider typo 2024-09-10 11:17:35 +04:00
ea3b3c5cec Merge pull request from Infisical/misc/update-kms-of-existing-params-for-integration
misc: ensure that selected kms key in aws param integration is followed
2024-09-10 12:51:06 +08:00
45f3675337 Merge pull request from Infisical/misc/support-glob-patterns-oidc
misc: support glob patterns for OIDC
2024-09-09 18:22:51 -04:00
2b39d9e6c4 Merge pull request from Infisical/pki-issuer-docs
Documentation for Infisical PKI Issuer for K8s Cert-Manager
2024-09-09 14:33:15 -04:00
fbc4b47198 misc: ensure that selected kms key in aws param integration is applied 2024-09-09 22:23:22 +08:00
8e68d21115 misc: support glob patterns for oidc 2024-09-09 17:17:12 +08:00
6112bc9356 Add certificate template field + warning to pki issuer docs 2024-09-07 19:23:11 -07:00
6c3156273c Add docs for infisical pki issuer 2024-09-07 16:28:28 -07:00
37 changed files with 1639 additions and 55 deletions

@ -17,7 +17,7 @@ const generateUsername = () => {
return alphaNumericNanoId(32);
};
export const ElasticSearchDatabaseProvider = (): TDynamicProviderFns => {
export const ElasticSearchProvider = (): 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

@ -1,10 +1,11 @@
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
import { AwsIamProvider } from "./aws-iam";
import { CassandraProvider } from "./cassandra";
import { ElasticSearchDatabaseProvider } from "./elastic-search";
import { ElasticSearchProvider } from "./elastic-search";
import { DynamicSecretProviders } from "./models";
import { MongoAtlasProvider } from "./mongo-atlas";
import { MongoDBProvider } from "./mongo-db";
import { RabbitMqProvider } from "./rabbit-mq";
import { RedisDatabaseProvider } from "./redis";
import { SqlDatabaseProvider } from "./sql-database";
@ -15,6 +16,7 @@ export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
[DynamicSecretProviders.AwsElastiCache]: AwsElastiCacheDatabaseProvider(),
[DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(),
[DynamicSecretProviders.ElasticSearch]: ElasticSearchDatabaseProvider(),
[DynamicSecretProviders.MongoDB]: MongoDBProvider()
[DynamicSecretProviders.MongoDB]: MongoDBProvider(),
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider()
});

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

@ -0,0 +1,172 @@
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<void> {
try {
// Create user
const userUrl = `/users/${createUser.username}`;
const userData = {
password: createUser.password,
tags: createUser.tags.join(",")
};
await axiosInstance.put(userUrl, userData);
// 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
);
}
} 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<typeof DynamicSecretRabbitMqSchema>) => {
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);
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
};
};

@ -7,7 +7,7 @@ const isValidDate = (dateString: string) => {
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
export const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
export const hostnameRegex = /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
export const validateAltNamesField = z
.string()
.trim()

@ -0,0 +1,4 @@
import picomatch from "picomatch";
export const doesFieldValueMatchOidcPolicy = (fieldValue: string, policyValue: string) =>
policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue);

@ -28,6 +28,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TOrgBotDALFactory } from "../org/org-bot-dal";
import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal";
import { doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns";
import {
TAttachOidcAuthDTO,
TGetOidcAuthDTO,
@ -123,7 +124,7 @@ export const identityOidcAuthServiceFactory = ({
}) as Record<string, string>;
if (identityOidcAuth.boundSubject) {
if (tokenData.sub !== identityOidcAuth.boundSubject) {
if (!doesFieldValueMatchOidcPolicy(tokenData.sub, identityOidcAuth.boundSubject)) {
throw new ForbiddenRequestError({
message: "Access denied: OIDC subject not allowed."
});
@ -131,7 +132,11 @@ export const identityOidcAuthServiceFactory = ({
}
if (identityOidcAuth.boundAudiences) {
if (!identityOidcAuth.boundAudiences.split(", ").includes(tokenData.aud)) {
if (
!identityOidcAuth.boundAudiences
.split(", ")
.some((policyValue) => doesFieldValueMatchOidcPolicy(tokenData.aud, policyValue))
) {
throw new ForbiddenRequestError({
message: "Access denied: OIDC audience not allowed."
});
@ -142,7 +147,9 @@ export const identityOidcAuthServiceFactory = ({
Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => {
const claimValue = (identityOidcAuth.boundClaims as Record<string, string>)[claimKey];
// handle both single and multi-valued claims
if (!claimValue.split(", ").some((claimEntry) => tokenData[claimKey] === claimEntry)) {
if (
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry))
) {
throw new ForbiddenRequestError({
message: "Access denied: OIDC claim not allowed."
});

@ -567,8 +567,8 @@ const syncSecretsAWSParameterStore = async ({
});
ssm.config.update(config);
const metadata = z.record(z.any()).parse(integration.metadata || {});
const awsParameterStoreSecretsObj: Record<string, AWS.SSM.Parameter> = {};
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
const awsParameterStoreSecretsObj: Record<string, AWS.SSM.Parameter & { KeyId?: string }> = {};
logger.info(
`getIntegrationSecrets: integration sync triggered for ssm with [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [shouldDisableDelete=${metadata.shouldDisableDelete}]`
);
@ -598,18 +598,57 @@ const syncSecretsAWSParameterStore = async ({
nextToken = parameters.NextToken;
}
logger.info(
`getIntegrationSecrets: all fetched keys from AWS SSM [projectId=${projectId}] [environment=${
integration.environment.slug
}] [secretPath=${integration.secretPath}] [awsParameterStoreSecretsObj=${Object.keys(
awsParameterStoreSecretsObj
).join(",")}]`
);
logger.info(
`getIntegrationSecrets: all secrets from Infisical to send to AWS SSM [projectId=${projectId}] [environment=${
integration.environment.slug
}] [secretPath=${integration.secretPath}] [secrets=${Object.keys(secrets).join(",")}]`
);
let areParametersKmsKeysFetched = false;
if (metadata.kmsKeyId) {
// we put this inside a try catch so that existing integrations without the ssm:DescribeParameters
// AWS permission will not break
try {
let hasNextDescribePage = true;
let describeNextToken: string | undefined;
while (hasNextDescribePage) {
const parameters = await ssm
.describeParameters({
MaxResults: 10,
NextToken: describeNextToken,
ParameterFilters: [
{
Key: "Path",
Option: "OneLevel",
Values: [integration.path as string]
}
]
})
.promise();
if (parameters.Parameters) {
parameters.Parameters.forEach((parameter) => {
if (parameter.Name) {
const secKey = parameter.Name.substring((integration.path as string).length);
awsParameterStoreSecretsObj[secKey].KeyId = parameter.KeyId;
}
});
}
areParametersKmsKeysFetched = true;
hasNextDescribePage = Boolean(parameters.NextToken);
describeNextToken = parameters.NextToken;
}
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((error as any).code === "AccessDeniedException") {
logger.error(
`AWS Parameter Store Error [integration=${integration.id}]: double check AWS account permissions (refer to the Infisical docs)`
);
}
response = {
isSynced: false,
syncMessage: (error as AWSError)?.message || "Error syncing with AWS Parameter Store"
};
}
}
// Identify secrets to create
// don't use Promise.all() and promise map here
// it will cause rate limit
@ -620,7 +659,7 @@ const syncSecretsAWSParameterStore = async ({
// -> create secret
if (secrets[key].value) {
logger.info(
`getIntegrationSecrets: create secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
`getIntegrationSecrets: create secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}]`
);
await ssm
.putParameter({
@ -648,7 +687,7 @@ const syncSecretsAWSParameterStore = async ({
} catch (err) {
logger.error(
err,
`getIntegrationSecrets: create secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
`getIntegrationSecrets: create secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}]`
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((err as any).code === "AccessDeniedException") {
@ -667,16 +706,23 @@ const syncSecretsAWSParameterStore = async ({
// case: secret exists in AWS parameter store
} else {
logger.info(
`getIntegrationSecrets: update secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
`getIntegrationSecrets: update secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}]`
);
// -> update secret
if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) {
const shouldUpdateKms =
areParametersKmsKeysFetched &&
Boolean(metadata.kmsKeyId) &&
awsParameterStoreSecretsObj[key].KeyId !== metadata.kmsKeyId;
// we ensure that the KMS key configured in the integration is applied for ALL parameters on AWS
if (shouldUpdateKms || awsParameterStoreSecretsObj[key].Value !== secrets[key].value) {
await ssm
.putParameter({
Name: `${integration.path}${key}`,
Type: "SecureString",
Value: secrets[key].value,
Overwrite: true
Overwrite: true,
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId })
})
.promise();
}
@ -698,7 +744,7 @@ const syncSecretsAWSParameterStore = async ({
} catch (err) {
logger.error(
err,
`getIntegrationSecrets: update secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
`getIntegrationSecrets: update secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}]`
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((err as any).code === "AccessDeniedException") {
@ -728,11 +774,11 @@ const syncSecretsAWSParameterStore = async ({
for (const key in awsParameterStoreSecretsObj) {
if (Object.hasOwn(awsParameterStoreSecretsObj, key)) {
logger.info(
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=2]`
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [step=2]`
);
if (!(key in secrets)) {
logger.info(
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=3]`
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [step=3]`
);
// case:
// -> delete secret
@ -742,7 +788,7 @@ const syncSecretsAWSParameterStore = async ({
})
.promise();
logger.info(
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=4]`
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [step=4]`
);
}
await new Promise((resolve) => {

@ -58,7 +58,7 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png)
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select 'AWS ElastiCache'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-elasti-cache.png)
@ -116,7 +116,7 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
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)
![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png)
<Tip>
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
@ -125,7 +125,7 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
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-redis.png)
![Provision Lease](/images/platform/dynamic-secrets/lease-values.png)
</Step>
</Steps>
@ -133,11 +133,11 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
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)
![Provision Lease](/images/platform/dynamic-secrets/lease-data.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)
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret

@ -23,7 +23,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png)
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select 'Elasticsearch'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-elastic-search.png)
@ -99,7 +99,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
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)
![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png)
<Tip>
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
@ -108,7 +108,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
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)
![Provision Lease](/images/platform/dynamic-secrets/lease-values.png)
</Step>
</Steps>
@ -116,11 +116,11 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
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)
![Provision Lease](/images/platform/dynamic-secrets/lease-data.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)
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret

@ -0,0 +1,116 @@
---
title: "RabbitMQ"
description: "Learn how to dynamically generate RabbitMQ user credentials."
---
The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credentials on demand based on configured role.
## Prerequisites
1. Ensure that the `management` plugin is enabled on your RabbitMQ instance. This is required for the dynamic secret to work.
## Set up Dynamic Secrets with RabbitMQ
<Steps>
<Step title="Open Secret Overview Dashboard">
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select 'RabbitMQ'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-rabbit-mq.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
</ParamField>
<ParamField path="Max TTL" type="string" required>
Maximum time-to-live for a generated secret.
</ParamField>
<ParamField path="Host" type="string" required>
Your RabbitMQ host. This must be in HTTP format. _(Example: http://your-cluster-ip)_
</ParamField>
<ParamField path="Port" type="string" required>
The port that the RabbitMQ management plugin is listening on. This is `15672` by default.
</ParamField>
<ParamField path="Virtual host name" type="string" required>
The name of the virtual host that the user will be assigned to. This defaults to `/`.
</ParamField>
<ParamField path="Virtual host permissions (Read/Write/Configure)" type="string" required>
The permissions that the user will have on the virtual host. This defaults to `.*`.
The three permission fields all take a regular expression _(regex)_, that should match resource names for which the user is granted read / write / configuration permissions
</ParamField>
<ParamField path="Username" type="string" required>
The username of the user that will be used to provision new dynamic secret leases.
</ParamField>
<ParamField path="Password" type="string" required>
The password of the user that will be used to provision new dynamic secret leases.
</ParamField>
<ParamField path="CA(SSL)" type="string">
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>
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-input-modal-rabbit-mq.png)
</Step>
<Step title="Click `Submit`">
After submitting the form, you will see a dynamic secret created in the dashboard.
<Note>
If this step fails, you may have to add the CA certificate.
</Note>
</Step>
<Step title="Generate dynamic secrets">
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.png)
<Tip>
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
</Tip>
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.png)
</Step>
</Steps>
## 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.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.png)
<Warning>
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
</Warning>

@ -16,7 +16,7 @@ Create a user with the required permission in your Redis instance. This user wil
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png)
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select 'Redis'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-redis.png)
@ -78,7 +78,7 @@ Create a user with the required permission in your Redis instance. This user wil
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)
![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png)
<Tip>
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
@ -87,7 +87,7 @@ Create a user with the required permission in your Redis instance. This user wil
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-redis.png)
![Provision Lease](/images/platform/dynamic-secrets/lease-values.png)
</Step>
</Steps>
@ -95,11 +95,11 @@ Create a user with the required permission in your Redis instance. This user wil
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)
![Provision Lease](/images/platform/dynamic-secrets/lease-data.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)
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret

@ -93,7 +93,12 @@ In the following steps, we explore how to create and use identities to access th
- Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an acccess token in seconds. This value will be referenced at renewal time.
- Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses.
- Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address.
<Info>
The `subject`, `audiences`, and `claims` fields support glob pattern matching; however, we highly recommend using hardcoded values whenever possible.
</Info>
</Step>
<Step title="Adding an identity to a project">
To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project.

@ -92,8 +92,8 @@ In the following steps, we explore how to create and use identities to access th
- Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an acccess token in seconds. This value will be referenced at renewal time.
- Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses.
- Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address.
<Tip>If you are unsure about what to configure for the subject, audience, and claims fields you can use [github/actions-oidc-debugger](https://github.com/github/actions-oidc-debugger) to get the appropriate values. Alternatively, you can fetch the JWT from the workflow and inspect the fields manually.</Tip>
<Info>The `subject`, `audiences`, and `claims` fields support glob pattern matching; however, we highly recommend using hardcoded values whenever possible.</Info>
</Step>
<Step title="Adding an identity to a project">
To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project.

@ -0,0 +1,247 @@
---
title: "Kubernetes Issuer"
sidebarTitle: "Certificates for Kubernetes"
description: "Learn how to automatically provision and manage TLS certificates for in Kubernetes using Infisical PKI"
---
## Concept
The Infisical PKI Issuer is an installable Kubernetes [cert-manager](https://cert-manager.io/) controller that uses Infisical PKI to sign certificate requests. The issuer is perfect for getting X.509 certificates for ingresses and other Kubernetes resources and capable of automatically renewing certificates as needed.
As part of the workflow, you install `cert-manager`, the Infisical PKI Issuer, and configure resources to represent the connection details to your Infisical PKI and the certificates you wish to issue. Each issued certificate and corresponding private key is made available in a Kubernetes secret.
We recommend reading the [cert-manager documentation](https://cert-manager.io/docs/) for a fuller understanding of all the moving parts.
## Workflow
A typical workflow for using the Infisical PKI Issuer to issue certificates for your Kubernetes resources consists of the following steps:
1. Creating a machine identity in Infisical.
2. Creating a Kubernetes secret to store the credentials of the machine identity.
3. Installing `cert-manager` into your Kubernetes cluster.
4. Installing the Infisical PKI Issuer controller into your Kubernetes cluster.
5. Creating an `Issuer` or `ClusterIssuer` resource in your Kubernetes cluster to represent the Infisical PKI issuer you wish to use.
6. Creating a `Certificate` resource in your Kubernetes cluster to represent a certificate you wish to issue. As part of this step, you specify the Kubernetes `Secret` to create and store the issued certificate and private key.
7. Consuming the issued certificate across your Kubernetes resources from the specified Kubernetes `Secret`.
## Guide
In the following steps, we explore how to install the Infisical PKI Issuer using [kubectl](https://github.com/kubernetes/kubectl) and use it to obtain certificates for your Kubernetes resources.
<Steps>
<Step title="Create an identity in Infisical">
Follow the instructions [here](/documentation/platform/identities/universal-auth) to configure a [machine identity](/documentation/platform/identities/machine-identities) in Infisical with Universal Auth.
By the end of this step, you should have a **Client ID** and **Client Secret** on hand as part of the Universal Auth configuration for the Infisical PKI Issuer to authenticate with Infisical; this will be useful in steps 4 and 5.
<Note>
Currently, the Infisical PKI Issuer only supports authenticating with Infisical via the [Universal Auth](/documentation/platform/identities/universal-auth) authentication method.
We're planning to add support for [Kubernetes Auth](/documentation/platform/identities/kubernetes-auth) in the near future.
</Note>
</Step>
<Step title="Install cert-manager">
Install `cert-manager` into your Kubernetes cluster by following the instructions [here](https://cert-manager.io/docs/installation/) or by running the following command:
```bash
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.3/cert-manager.yaml
```
</Step>
<Step title="Install the Issuer Controller">
Install the Infisical PKI Issuer controller into your Kubernetes cluster by running the following command:
```bash
kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical-issuer/main/build/install.yaml
```
</Step>
<Step title="Create Kubernetes Secret for Infisical PKI Issuer">
Start by creating a Kubernetes `Secret` containing the **Client Secret** from step 1. As mentioned previously, this will be used by the Infisical PKI issuer to authenticate with Infisical.
<Tabs>
<Tab title="kubectl command">
```bash
kubectl create secret generic issuer-infisical-client-secret \
--namespace <namespace_you_want_to_issue_certificates_in> \
--from-literal=clientSecret=<client_secret>
```
</Tab>
<Tab title="Configuration file">
```yaml secret-issuer.yaml
apiVersion: v1
kind: Secret
metadata:
name: issuer-infisical-client-secret
namespace: <namespace_you_want_to_issue_certificates_in>
data:
clientSecret: <client_secret>
```
```bash
kubectl apply -f secret-issuer.yaml
```
</Tab>
</Tabs>
</Step>
<Step title="Create Infisical PKI Issuer">
Next, create the Infisical PKI Issuer by filling out `url`, `clientId`, either `caId` or `certificateTemplateId`, and applying the following configuration file for the `Issuer` resource.
This configuration file specifies the connection details to your Infisical PKI CA to be used for issuing certificates.
```yaml infisical-issuer.yaml
apiVersion: infisical-issuer.infisical.com/v1alpha1
kind: Issuer
metadata:
name: issuer-infisical
namespace: <namespace_you_want_to_issue_certificates_in>
spec:
url: "https://app.infisical.com" # the URL of your Infisical instance
caId: <ca_id> # the ID of the CA you want to use to issue certificates
certificateTemplateId: <certificate_template_id> # the ID of the certificate template you want to use to issue certificates against
authentication:
universalAuth:
clientId: <client_id> # the Client ID from step 1
secretRef: # reference to the Secret created in step 4
name: "issuer-infisical-client-secret"
key: "clientSecret"
```
```
kubectl apply -f infisical-issuer.yaml
```
<Warning>
The Infisical PKI Issuer supports issuing certificates against a specific CA or a specific certificate template.
For this reason, you should only fill in the `caId` or the `certificateTemplateId` field but not both.
We recommend using the `certificateTemplateId` field to issue certificates against a specific [certificate template](/documentation/platform/pki/certificate-templates)
since templates let you enforce constraints on issued certificates and may have alerting policies bound to them.
</Warning>
You can check that the issuer was created successfully by running the following command:
```bash
kubectl get issuers.infisical-issuer.infisical.com -n <namespace_of_issuer> -o wide
```
```bash
NAME AGE
issuer-infisical 21h
```
<Note>
An `Issuer` is a namespaced resource, and it is not possible to issue certificates from an `Issuer` in a different namespace.
This means you will need to create an `Issuer` in each namespace you wish to obtain `Certificates` in.
If you want to create a single `Issuer` that can be consumed in multiple namespaces, you should consider creating a `ClusterIssuer` resource. This is almost identical to the `Issuer` resource, however is non-namespaced so it can be used to issue `Certificates` across all namespaces.
You can read more about the `Issuer` and `ClusterIssuer` resources [here](https://cert-manager.io/docs/configuration/).
</Note>
</Step>
<Step title="Create Certificate">
Finally, create a `Certificate` by applying the following configuration file.
This configuration file specifies the details of the (end-entity/leaf) certificate to be issued.
```yaml certificate-issuer.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: certificate-by-issuer
namespace: <namespace_you_want_to_issue_certificates_in>
spec:
commonName: certificate-by-issuer.example.com # the common name for the certificate
secretName: certificate-by-issuer # the name of the Kubernetes Secret to create and store the certificate and private key in
issuerRef:
name: issuer-infisical
group: infisical-issuer.infisical.com
kind: Issuer
privateKey: # the algorithm and key size to use
algorithm: ECDSA
size: 256
duration: 48h # the ttl for the certificate
renewBefore: 12h # the time before the certificate expiry that the certificate should be automatically renewed
```
The above sample configuration file specifies a certificate to be issued with the common name `certificate-by-issuer.example.com` and ECDSA private key using the P-256 curve, valid for 48 hours; the certificate will be automatically renewed by `cert-manager` 12 hours before expiry.
The certificate is issued by the issuer `issuer-infisical` created in the previous step and the resulting certificate and private key will be stored in a secret named `certificate-by-issuer`.
Note that the full list of the fields supported on the `Certificate` resource can be found in the API reference documentation [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec).
You can check that the certificate was created successfully by running the following command:
```bash
kubectl get certificates -n <namespace_of_your_certificate> -o wide
```
```bash
NAME READY SECRET ISSUER STATUS AGE
certificate-by-issuer True certificate-by-issuer issuer-infisical Certificate is up to date and has not expired 20h
```
</Step>
<Step title="Use Certificate in Kubernetes Secret">
Since the actual certificate and private key are stored in a Kubernetes secret, we can check that the secret was created successfully by running the following command:
```bash
kubectl get secret certificate-by-issuer -n <namespace_of_your_certificate>
```
```bash
NAME TYPE DATA AGE
certificate-by-issuer kubernetes.io/tls 2 26h
```
We can `describe` the secret to get more information about it:
```bash
kubectl describe secret certificate-by-issuer -n default
```
```bash
Name: certificate-by-issuer
Namespace: default
Labels: controller.cert-manager.io/fao=true
Annotations: cert-manager.io/alt-names:
cert-manager.io/certificate-name: certificate-by-issuer
cert-manager.io/common-name: certificate-by-issuer.example.com
cert-manager.io/ip-sans:
cert-manager.io/issuer-group: infisical-issuer.infisical.com
cert-manager.io/issuer-kind: Issuer
cert-manager.io/issuer-name: issuer-infisical
cert-manager.io/uri-sans:
Type: kubernetes.io/tls
Data
====
tls.key: 227 bytes
tls.crt: 912 bytes
```
We can decode the certificate and print it out using `openssl`:
```bash
kubectl get secret certificate-by-issuer -n default -o jsonpath='{.data.tls\.crt}' | base64 --decode | openssl x509 -text -noout
```
In any case, the certificate is ready to be used as Kubernetes Secret by your Kubernetes resources.
</Step>
</Steps>
## FAQ
<AccordionGroup>
<Accordion title="What fields can be configured on the Certificate resource?">
The full list of the fields supported on the `Certificate` resource can be found in the API reference documentation [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec).
<Note>
Currently, not all fields are supported by the Infisical PKI Issuer.
</Note>
</Accordion>
<Accordion title="Can certificates be renewed automatically?">
Yes. `cert-manager` will automatically renew certificates according to the `renewBefore` threshold of expiry as
specified in the corresponding `Certificate` resource.
You can read more about the `renewBefore` field [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec).
</Accordion>
</AccordionGroup>

Binary file not shown.

Before

(image error) Size: 200 KiB

Binary file not shown.

Before

(image error) Size: 106 KiB

After

(image error) Size: 200 KiB

Binary file not shown.

After

(image error) Size: 460 KiB

Binary file not shown.

Before

(image error) Size: 136 KiB

Binary file not shown.

Before

(image error) Size: 31 KiB

After

(image error) Size: 481 KiB

Binary file not shown.

After

(image error) Size: 436 KiB

Binary file not shown.

Before

(image error) Size: 137 KiB

Binary file not shown.

Before

(image error) Size: 114 KiB

After

(image error) Size: 473 KiB

Binary file not shown.

Before

(image error) Size: 149 KiB

Binary file not shown.

Before

(image error) Size: 138 KiB

Binary file not shown.

Before

(image error) Size: 106 KiB

After

(image error) Size: 410 KiB

Binary file not shown.

Before

(image error) Size: 128 KiB

Binary file not shown.

Before

(image error) Size: 40 KiB

After

(image error) Size: 388 KiB

@ -30,6 +30,7 @@ Prerequisites:
"ssm:DeleteParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath",
"ssm:DescribeParameters",
"ssm:DeleteParameters",
"ssm:AddTagsToResource", // if you need to add tags to secrets
"kms:ListKeys", // if you need to specify the KMS key

@ -109,6 +109,7 @@
"documentation/platform/pki/private-ca",
"documentation/platform/pki/certificates",
"documentation/platform/pki/certificate-templates",
"documentation/platform/pki/pki-issuer",
"documentation/platform/pki/est",
"documentation/platform/pki/alerting"
]
@ -164,6 +165,7 @@
"documentation/platform/dynamic-secrets/redis",
"documentation/platform/dynamic-secrets/aws-elasticache",
"documentation/platform/dynamic-secrets/elastic-search",
"documentation/platform/dynamic-secrets/rabbit-mq",
"documentation/platform/dynamic-secrets/aws-iam",
"documentation/platform/dynamic-secrets/mongo-atlas",
"documentation/platform/dynamic-secrets/mongo-db"

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

@ -1,12 +1,13 @@
import { useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input, TextArea } from "@app/components/v2";
import { Button, FormControl, IconButton, Input, TextArea, Tooltip } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import { useAddIdentityOidcAuth, useUpdateIdentityOidcAuth } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
@ -258,7 +259,19 @@ export const IdentityOidcAuthForm = ({
control={control}
name="boundSubject"
render={({ field, fieldState: { error } }) => (
<FormControl label="Subject" isError={Boolean(error)} errorText={error?.message}>
<FormControl
label="Subject"
isError={Boolean(error)}
errorText={error?.message}
icon={
<Tooltip
className="text-center"
content={<span>This field supports glob patterns</span>}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
}
>
<Input {...field} type="text" />
</FormControl>
)}
@ -267,7 +280,19 @@ export const IdentityOidcAuthForm = ({
control={control}
name="boundAudiences"
render={({ field, fieldState: { error } }) => (
<FormControl label="Audiences" isError={Boolean(error)} errorText={error?.message}>
<FormControl
label="Audiences"
isError={Boolean(error)}
errorText={error?.message}
icon={
<Tooltip
className="text-center"
content={<span>This field supports glob patterns</span>}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
}
>
<Input {...field} type="text" placeholder="service1, service2" />
</FormControl>
)}
@ -282,6 +307,16 @@ export const IdentityOidcAuthForm = ({
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Claims" : undefined}
icon={
index === 0 ? (
<Tooltip
className="text-center"
content={<span>This field supports glob patterns</span>}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
) : undefined
}
isError={Boolean(error)}
errorText={error?.message}
>

@ -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: <SiElasticsearch size="2rem" />,
provider: DynamicSecretProviders.ElasticSearch,
title: "Elastic Search"
},
{
icon: <SiRabbitmq size="1.5rem" />,
provider: DynamicSecretProviders.RabbitMq,
title: "RabbitMQ"
}
];
@ -276,6 +282,24 @@ export const CreateDynamicSecretForm = ({
/>
</motion.div>
)}
{wizardStep === WizardSteps.ProviderInputs &&
selectedProvider === DynamicSecretProviders.RabbitMq && (
<motion.div
key="dynamic-rabbit-mq-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<RabbitMqInputForm
onCompleted={handleFormReset}
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
/>
</motion.div>
)}
</AnimatePresence>
</ModalContent>
</Modal>

@ -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<typeof formSchema>;
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<TForm>({
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 (
<div>
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
<div>
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="dynamic-secret" />
</FormControl>
)}
/>
</div>
<div className="w-32">
<Controller
control={control}
name="defaultTTL"
defaultValue="1h"
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Default TTL" />}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div className="w-32">
<Controller
control={control}
name="maxTTL"
defaultValue="24h"
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Max TTL" />}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
</div>
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
</div>
<div className="flex flex-col">
<div className="flex items-center space-x-2">
<Controller
control={control}
name="provider.host"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Host"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input placeholder="https://your-rabbitmq-host.com" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="provider.port"
defaultValue={443}
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="The port on which the RabbitMQ management plugin is running. Default is 15672."
label="Management Port"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} type="number" />
</FormControl>
)}
/>
</div>
<div className="flex w-full items-center gap-2">
<Controller
control={control}
name="provider.username"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Username"
className="w-full"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} autoComplete="off" />
</FormControl>
)}
/>
<Controller
control={control}
name="provider.password"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Password"
className="w-full"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} type="password" autoComplete="new-password" />
</FormControl>
)}
/>
</div>
<div>
<FormLabel label="Virtual Host" className="mb-2" />
<div className="mt-2 flex items-center justify-evenly gap-2">
<Controller
control={control}
name="provider.virtualHost.name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name"
tooltipText="The virtual host to which the user will be assigned. Default is /."
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input placeholder="/virtual-host" {...field} autoComplete="off" />
</FormControl>
)}
/>
<Controller
control={control}
name="provider.virtualHost.permissions.read"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Read Permissions"
tooltipText="A regular expression matching resource names for which the user is granted read permissions."
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input placeholder=".*" {...field} autoComplete="off" />
</FormControl>
)}
/>
<Controller
control={control}
name="provider.virtualHost.permissions.write"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Write Permissions"
tooltipText="A regular expression matching resource names for which the user is granted write permissions."
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input placeholder=".*" {...field} autoComplete="off" />
</FormControl>
)}
/>
<Controller
control={control}
name="provider.virtualHost.permissions.configure"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Configure Permissions"
tooltipText="A regular expression matching resource names for which the user is granted configure permissions."
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input placeholder=".*" {...field} autoComplete="off" />
</FormControl>
)}
/>
</div>
</div>
<div className="flex flex-col">
<FormLabel
isOptional
className="mb-2"
label="Tags"
tooltipText={
<div className="space-y-4">
<p>Select which tag(s) to assign the users provisioned by Infisical.</p>
<p>
There is a wide range of in-built roles in RabbitMQ. Some include,
management, policymaker, monitoring, administrator. <br />
<Link passHref href="https://www.rabbitmq.com/docs/management#permissions">
<a target="_blank" rel="noopener noreferrer">
<span className="cursor-pointer text-primary-400">
Read more about management tags 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">
{selectedTags.map((_, i) => (
<Controller
control={control}
name={`provider.tags.${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 tag name, (management, policymaker, etc.)"
className="mb-0 flex-grow"
{...field}
/>
<IconButton
isDisabled={selectedTags.length === 1}
ariaLabel="delete key"
className="h-9"
variant="outline_bg"
onClick={() => {
if (selectedTags && selectedTags?.length > 1) {
setValue(
"provider.tags",
selectedTags.filter((__, idx) => idx !== i)
);
}
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</FormControl>
)}
/>
))}
</div>
</div>
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
className="mb-4"
variant="outline_bg"
onClick={() => {
setValue("provider.tags", [...selectedTags, ""]);
}}
>
Add Tag
</Button>
</div>
<div>
<Controller
control={control}
name="provider.ca"
render={({ field, fieldState: { error } }) => (
<FormControl
isOptional
label="CA (SSL)"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<SecretInput
{...field}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/>
</FormControl>
)}
/>
</div>
</div>
</div>
</div>
<div className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}>
Submit
</Button>
<Button variant="outline_bg" onClick={onCancel}>
Cancel
</Button>
</div>
</form>
</div>
);
};

@ -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 (
<div>
<OutputDisplay label="Username" value={DB_USERNAME} />
<OutputDisplay
label="Password"
value={DB_PASSWORD}
helperText="Important: Copy these credentials now. You will not be able to see them again after you close the modal."
/>
</div>
);
}
if (provider === DynamicSecretProviders.ElasticSearch) {
const { DB_USERNAME, DB_PASSWORD } = data as {
DB_USERNAME: string;

@ -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 = ({
/>
</motion.div>
)}
{dynamicSecretDetails?.type === DynamicSecretProviders.RabbitMq && (
<motion.div
key="rabbit-mq-edit"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<EditDynamicSecretRabbitMqForm
onClose={onClose}
projectSlug={projectSlug}
secretPath={secretPath}
dynamicSecret={dynamicSecretDetails}
environment={environment}
/>
</motion.div>
)}
</AnimatePresence>
);
};

@ -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<typeof formSchema>;
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<TForm>({
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 (
<div>
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
<div>
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="newName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="dynamic-secret" />
</FormControl>
)}
/>
</div>
<div className="w-32">
<Controller
control={control}
name="defaultTTL"
defaultValue="1h"
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Default TTL" />}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div className="w-32">
<Controller
control={control}
name="maxTTL"
defaultValue="24h"
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Max TTL" />}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
</div>
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
</div>
<div className="flex flex-col">
<div className="flex items-center space-x-2">
<Controller
control={control}
name="inputs.host"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Host"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input placeholder="https://your-rabbitmq-host.com" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="inputs.port"
defaultValue={443}
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="The port on which the RabbitMQ management plugin is running. Default is 15672."
label="Management Port"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} type="number" />
</FormControl>
)}
/>
</div>
<div className="flex w-full items-center gap-2">
<Controller
control={control}
name="inputs.username"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Username"
className="w-full"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} autoComplete="off" />
</FormControl>
)}
/>
<Controller
control={control}
name="inputs.password"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Password"
className="w-full"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} type="password" autoComplete="new-password" />
</FormControl>
)}
/>
</div>
<div>
<FormLabel label="Virtual Host" className="mb-2" />
<div className="mt-2 flex items-center justify-evenly gap-2">
<Controller
control={control}
name="inputs.virtualHost.name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name"
tooltipText="The virtual host to which the user will be assigned. Default is /."
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input placeholder="/virtual-host" {...field} autoComplete="off" />
</FormControl>
)}
/>
<Controller
control={control}
name="inputs.virtualHost.permissions.read"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Read Permissions"
tooltipText="A regular expression matching resource names for which the user is granted read permissions."
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input placeholder=".*" {...field} autoComplete="off" />
</FormControl>
)}
/>
<Controller
control={control}
name="inputs.virtualHost.permissions.write"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Write Permissions"
tooltipText="A regular expression matching resource names for which the user is granted write permissions."
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input placeholder=".*" {...field} autoComplete="off" />
</FormControl>
)}
/>
<Controller
control={control}
name="inputs.virtualHost.permissions.configure"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Configure Permissions"
tooltipText="A regular expression matching resource names for which the user is granted configure permissions."
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input placeholder=".*" {...field} autoComplete="off" />
</FormControl>
)}
/>
</div>
</div>
<div className="flex flex-col">
<FormLabel
isOptional
className="mb-2"
label="Tags"
tooltipText={
<div className="space-y-4">
<p>Select which tag(s) to assign the users provisioned by Infisical.</p>
<p>
There is a wide range of in-built roles in RabbitMQ. Some include,
management, policymaker, monitoring, administrator. <br />
<Link passHref href="https://www.rabbitmq.com/docs/management#permissions">
<a target="_blank" rel="noopener noreferrer">
<span className="cursor-pointer text-primary-400">
Read more about management tags 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">
{selectedTags.map((_, i) => (
<Controller
control={control}
name={`inputs.tags.${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 tag name, (management, policymaker, etc.)"
className="mb-0 flex-grow"
{...field}
/>
<IconButton
isDisabled={selectedTags.length === 1}
ariaLabel="delete key"
className="h-9"
variant="outline_bg"
onClick={() => {
if (selectedTags && selectedTags?.length > 1) {
setValue(
"inputs.tags",
selectedTags.filter((__, idx) => idx !== i)
);
}
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</FormControl>
)}
/>
))}
</div>
</div>
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
className="mb-4"
variant="outline_bg"
onClick={() => {
setValue("inputs.tags", [...selectedTags, ""]);
}}
>
Add Tag
</Button>
</div>
<div>
<Controller
control={control}
name="inputs.ca"
render={({ field, fieldState: { error } }) => (
<FormControl
isOptional
label="CA (SSL)"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<SecretInput
{...field}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/>
</FormControl>
)}
/>
</div>
</div>
</div>
</div>
<div className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}>
Submit
</Button>
<Button variant="outline_bg" onClick={onClose}>
Cancel
</Button>
</div>
</form>
</div>
);
};