Merge remote-tracking branch 'origin/main' into feat/add-key-usages-for-template-and-cert

This commit is contained in:
Sheen Capadngan
2024-09-11 00:22:04 +08:00
54 changed files with 1876 additions and 234 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -468,7 +468,7 @@ export const registerRoutes = async (
projectMembershipDAL
});
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, tokenDAL: authTokenDAL });
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL });
const passwordService = authPaswordServiceFactory({
tokenService,
smtpService,

View File

@@ -45,7 +45,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
.describe(CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm),
requireTemplateForIssuance: z
.boolean()
.default(true)
.default(false)
.describe(CERTIFICATE_AUTHORITIES.CREATE.requireTemplateForIssuance)
})
.refine(

View File

@@ -101,7 +101,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
.refine(
(data) =>
(data.caId !== undefined && data.certificateTemplateId === undefined) ||
(data.caId === undefined && data.pkiCollectionId === undefined && data.certificateTemplateId !== undefined),
(data.caId === undefined && data.certificateTemplateId !== undefined),
{
message: "Either CA ID or Certificate Template ID must be present, but not both",
path: ["caId", "certificateTemplateId"]
@@ -192,7 +192,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
.refine(
(data) =>
(data.caId !== undefined && data.certificateTemplateId === undefined) ||
(data.caId === undefined && data.pkiCollectionId === undefined && data.certificateTemplateId !== undefined),
(data.caId === undefined && data.certificateTemplateId !== undefined),
{
message: "Either CA ID or Certificate Template ID must be present, but not both",
path: ["caId", "certificateTemplateId"]

View File

@@ -42,7 +42,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
},
schema: {
body: z.object({
organizationId: z.string().trim()
organizationId: z.string().trim(),
userAgent: z.enum(["cli"]).optional()
}),
response: {
200: z.object({
@@ -53,7 +54,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
handler: async (req, res) => {
const cfg = getConfig();
const tokens = await server.services.login.selectOrganization({
userAgent: req.headers["user-agent"],
userAgent: req.body.userAgent ?? req.headers["user-agent"],
authJwtToken: req.headers.authorization,
organizationId: req.body.organizationId,
ipAddress: req.realIp

View File

@@ -12,7 +12,6 @@ import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/erro
import { logger } from "@app/lib/logger";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TTokenDALFactory } from "../auth-token/auth-token-dal";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types";
import { TOrgDALFactory } from "../org/org-dal";
@@ -34,7 +33,6 @@ type TAuthLoginServiceFactoryDep = {
orgDAL: TOrgDALFactory;
tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService;
tokenDAL: TTokenDALFactory;
};
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
@@ -42,8 +40,7 @@ export const authLoginServiceFactory = ({
userDAL,
tokenService,
smtpService,
orgDAL,
tokenDAL
orgDAL
}: TAuthLoginServiceFactoryDep) => {
/*
* Private
@@ -376,8 +373,6 @@ export const authLoginServiceFactory = ({
});
}
await tokenDAL.incrementTokenSessionVersion(user.id, decodedToken.tokenVersionId);
const tokens = await generateUserTokens({
authMethod: decodedToken.authMethod,
user,

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,22 +4,27 @@ Copyright (c) 2023 Infisical Inc.
package cmd
import (
"errors"
"fmt"
"os"
"os/exec"
"os/signal"
"runtime"
"strings"
"sync"
"syscall"
"time"
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/fatih/color"
"github.com/posthog/posthog-go"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var ErrManualSignalInterrupt = errors.New("signal: interrupt")
var watcherWaitGroup = new(sync.WaitGroup)
// runCmd represents the run command
var runCmd = &cobra.Command{
Example: `
@@ -77,11 +82,35 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
command, err := cmd.Flags().GetString("command")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
watchMode, err := cmd.Flags().GetBool("watch")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
watchModeInterval, err := cmd.Flags().GetInt("watch-interval")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
// If the --watch flag has been set, the --watch-interval flag should also be set
if watchMode && watchModeInterval < 5 {
util.HandleError(fmt.Errorf("watch interval must be at least 5 seconds, you passed %d seconds", watchModeInterval))
}
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
if err != nil {
util.HandleError(err, "Unable to parse flag")
@@ -116,108 +145,50 @@ var runCmd = &cobra.Command{
Recursive: recursive,
}
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
request.InfisicalToken = token.Token
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
request.UniversalAuthAccessToken = token.Token
}
secrets, err := util.GetAllEnvironmentVariables(request, projectConfigDir)
injectableEnvironment, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, shouldExpandSecrets, token)
if err != nil {
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
}
if secretOverriding {
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL)
log.Debug().Msgf("injecting the following environment variables into shell: %v", injectableEnvironment.Variables)
if watchMode {
executeCommandWithWatchMode(command, args, watchModeInterval, request, projectConfigDir, shouldExpandSecrets, secretOverriding, token)
} else {
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED)
}
if cmd.Flags().Changed("command") {
command := cmd.Flag("command").Value.String()
err = executeMultipleCommandWithEnvs(command, injectableEnvironment.SecretsCount, injectableEnvironment.Variables)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if shouldExpandSecrets {
authParams := models.ExpandSecretsAuthentication{}
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
authParams.InfisicalToken = token.Token
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
authParams.UniversalAuthAccessToken = token.Token
}
secrets = util.ExpandSecrets(secrets, authParams, projectConfigDir)
}
secretsByKey := getSecretsByKeys(secrets)
environmentVariables := make(map[string]string)
// add all existing environment vars
for _, s := range os.Environ() {
kv := strings.SplitN(s, "=", 2)
key := kv[0]
value := kv[1]
environmentVariables[key] = value
}
// check to see if there are any reserved key words in secrets to inject
filterReservedEnvVars(secretsByKey)
// now add infisical secrets
for k, v := range secretsByKey {
environmentVariables[k] = v.Value
}
// turn it back into a list of envs
var env []string
for key, value := range environmentVariables {
s := key + "=" + value
env = append(env, s)
}
log.Debug().Msgf("injecting the following environment variables into shell: %v", env)
Telemetry.CaptureEvent("cli-command:run",
posthog.NewProperties().
Set("secretsCount", len(secrets)).
Set("environment", environmentName).
Set("isUsingServiceToken", token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER).
Set("isUsingUniversalAuthToken", token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER).
Set("single-command", strings.Join(args, " ")).
Set("multi-command", cmd.Flag("command").Value.String()).
Set("version", util.CLI_VERSION))
if cmd.Flags().Changed("command") {
command := cmd.Flag("command").Value.String()
err = executeMultipleCommandWithEnvs(command, len(secretsByKey), env)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
} else {
err = executeSingleCommandWithEnvs(args, len(secretsByKey), env)
if err != nil {
fmt.Println(err)
os.Exit(1)
} else {
err = executeSingleCommandWithEnvs(args, injectableEnvironment.SecretsCount, injectableEnvironment.Variables)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
}
},
}
var (
reservedEnvVars = []string{
"HOME", "PATH", "PS1", "PS2",
"PWD", "EDITOR", "XAUTHORITY", "USER",
"TERM", "TERMINFO", "SHELL", "MAIL",
}
reservedEnvVarPrefixes = []string{
"XDG_",
"LC_",
}
)
func filterReservedEnvVars(env map[string]models.SingleEnvironmentVariable) {
var (
reservedEnvVars = []string{
"HOME", "PATH", "PS1", "PS2",
"PWD", "EDITOR", "XAUTHORITY", "USER",
"TERM", "TERMINFO", "SHELL", "MAIL",
}
reservedEnvVarPrefixes = []string{
"XDG_",
"LC_",
}
)
for _, reservedEnvName := range reservedEnvVars {
if _, ok := env[reservedEnvName]; ok {
delete(env, reservedEnvName)
@@ -237,13 +208,15 @@ func filterReservedEnvVars(env map[string]models.SingleEnvironmentVariable) {
func init() {
rootCmd.AddCommand(runCmd)
runCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token")
runCmd.Flags().String("token", "", "fetch secrets using service token or machine identity access token")
runCmd.Flags().String("projectId", "", "manually set the project ID to fetch secrets from when using machine identity based auth")
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
runCmd.Flags().Bool("include-imports", true, "Import linked secrets ")
runCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders")
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
runCmd.Flags().StringP("env", "e", "dev", "set the environment (dev, prod, etc.) from which your secrets should be pulled from")
runCmd.Flags().Bool("expand", true, "parse shell parameter expansions in your secrets")
runCmd.Flags().Bool("include-imports", true, "import linked secrets ")
runCmd.Flags().Bool("recursive", false, "fetch secrets from all sub-folders")
runCmd.Flags().Bool("secret-overriding", true, "prioritizes personal secrets, if any, with the same name over shared secrets")
runCmd.Flags().Bool("watch", false, "enable reload of application when secrets change")
runCmd.Flags().Int("watch-interval", 10, "interval in seconds to check for secret changes")
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ")
runCmd.Flags().String("path", "/", "get secrets within a folder path")
@@ -263,7 +236,7 @@ func executeSingleCommandWithEnvs(args []string, secretsCount int, env []string)
cmd.Stderr = os.Stderr
cmd.Env = env
return execCmd(cmd)
return execBasicCmd(cmd)
}
func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []string) error {
@@ -286,11 +259,10 @@ func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []
log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", secretsCount))
log.Debug().Msgf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand)
return execCmd(cmd)
return execBasicCmd(cmd)
}
// Credit: inspired by AWS Valut
func execCmd(cmd *exec.Cmd) error {
func execBasicCmd(cmd *exec.Cmd) error {
sigChannel := make(chan os.Signal, 1)
signal.Notify(sigChannel)
@@ -314,3 +286,217 @@ func execCmd(cmd *exec.Cmd) error {
os.Exit(waitStatus.ExitStatus())
return nil
}
func waitForExitCommand(cmd *exec.Cmd) (int, error) {
if err := cmd.Wait(); err != nil {
// ignore errors
cmd.Process.Signal(os.Kill) // #nosec G104
if exitError, ok := err.(*exec.ExitError); ok {
return exitError.ExitCode(), exitError
}
return 2, err
}
waitStatus, ok := cmd.ProcessState.Sys().(syscall.WaitStatus)
if !ok {
return 2, fmt.Errorf("unexpected ProcessState type, expected syscall.WaitStatus, got %T", waitStatus)
}
return waitStatus.ExitStatus(), nil
}
func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInterval int, request models.GetAllSecretsParameters, projectConfigDir string, expandSecrets bool, secretOverriding bool, token *models.TokenDetails) {
var cmd *exec.Cmd
var err error
var lastSecretsFetch time.Time
var lastUpdateEvent time.Time
var watchMutex sync.Mutex
var processMutex sync.Mutex
var beingTerminated = false
var currentETag string
if err != nil {
util.HandleError(err, "Failed to fetch secrets")
}
runCommandWithWatcher := func(environmentVariables models.InjectableEnvironmentResult) {
currentETag = environmentVariables.ETag
secretsFetchedAt := time.Now()
if secretsFetchedAt.After(lastSecretsFetch) {
lastSecretsFetch = secretsFetchedAt
}
shouldRestartProcess := cmd != nil
// terminate the old process before starting a new one
if shouldRestartProcess {
log.Info().Msg(color.HiMagentaString("[HOT RELOAD] Environment changes detected. Reloading process..."))
beingTerminated = true
log.Debug().Msgf(color.HiMagentaString("[HOT RELOAD] Sending SIGTERM to PID %d", cmd.Process.Pid))
if e := cmd.Process.Signal(syscall.SIGTERM); e != nil {
log.Error().Err(e).Msg(color.HiMagentaString("[HOT RELOAD] Failed to send SIGTERM"))
}
// wait up to 10 sec for the process to exit
for i := 0; i < 10; i++ {
if !util.IsProcessRunning(cmd.Process) {
// process has been killed so we break out
break
}
if i == 5 {
log.Debug().Msg(color.HiMagentaString("[HOT RELOAD] Still waiting for process exit status"))
}
time.Sleep(time.Second)
}
// SIGTERM may not work on Windows so we try SIGKILL
if util.IsProcessRunning(cmd.Process) {
log.Debug().Msg(color.HiMagentaString("[HOT RELOAD] Process still hasn't fully exited, attempting SIGKILL"))
if e := cmd.Process.Kill(); e != nil {
log.Error().Err(e).Msg(color.HiMagentaString("[HOT RELOAD] Failed to send SIGKILL"))
}
}
cmd = nil
} else {
// If `cmd` is nil, we know this is the first time we are starting the process
log.Info().Msg(color.HiMagentaString("[HOT RELOAD] Watching for secret changes..."))
}
processMutex.Lock()
if lastUpdateEvent.After(secretsFetchedAt) {
processMutex.Unlock()
return
}
beingTerminated = false
watcherWaitGroup.Add(1)
// start the process
log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", environmentVariables.SecretsCount))
cmd, err = util.RunCommand(commandFlag, args, environmentVariables.Variables, false)
if err != nil {
defer watcherWaitGroup.Done()
util.HandleError(err)
}
go func() {
defer processMutex.Unlock()
defer watcherWaitGroup.Done()
exitCode, err := waitForExitCommand(cmd)
// ignore errors if we are being terminated
if !beingTerminated {
if err != nil {
if strings.HasPrefix(err.Error(), "exec") || strings.HasPrefix(err.Error(), "fork/exec") {
log.Error().Err(err).Msg("Failed to execute command")
}
if err.Error() != ErrManualSignalInterrupt.Error() {
log.Error().Err(err).Msg("Process exited with error")
}
}
os.Exit(exitCode)
}
}()
}
recheckSecretsChannel := make(chan bool, 1)
recheckSecretsChannel <- true
// a simple goroutine that triggers the recheckSecretsChan every watch interval (defaults to 10 seconds)
go func() {
for {
time.Sleep(time.Duration(watchModeInterval) * time.Second)
recheckSecretsChannel <- true
}
}()
for {
<-recheckSecretsChannel
watchMutex.Lock()
newEnvironmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, expandSecrets, token)
if err != nil {
log.Error().Err(err).Msg("[HOT RELOAD] Failed to fetch secrets")
continue
}
if newEnvironmentVariables.ETag != currentETag {
runCommandWithWatcher(newEnvironmentVariables)
} else {
log.Debug().Msg("[HOT RELOAD] No changes detected in secrets, not reloading process")
}
watchMutex.Unlock()
}
}
func fetchAndFormatSecretsForShell(request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, shouldExpandSecrets bool, token *models.TokenDetails) (models.InjectableEnvironmentResult, error) {
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
request.InfisicalToken = token.Token
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
request.UniversalAuthAccessToken = token.Token
}
secrets, err := util.GetAllEnvironmentVariables(request, projectConfigDir)
if err != nil {
return models.InjectableEnvironmentResult{}, err
}
if secretOverriding {
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL)
} else {
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED)
}
if shouldExpandSecrets {
authParams := models.ExpandSecretsAuthentication{}
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
authParams.InfisicalToken = token.Token
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
authParams.UniversalAuthAccessToken = token.Token
}
secrets = util.ExpandSecrets(secrets, authParams, projectConfigDir)
}
secretsByKey := getSecretsByKeys(secrets)
environmentVariables := make(map[string]string)
// add all existing environment vars
for _, s := range os.Environ() {
kv := strings.SplitN(s, "=", 2)
key := kv[0]
value := kv[1]
environmentVariables[key] = value
}
// check to see if there are any reserved key words in secrets to inject
filterReservedEnvVars(secretsByKey)
// now add infisical secrets
for k, v := range secretsByKey {
environmentVariables[k] = v.Value
}
env := make([]string, 0, len(environmentVariables))
for key, value := range environmentVariables {
env = append(env, key+"="+value)
}
return models.InjectableEnvironmentResult{
Variables: env,
ETag: util.GenerateETagFromSecrets(secrets),
SecretsCount: len(secretsByKey),
}, nil
}

View File

@@ -104,6 +104,12 @@ type GetAllSecretsParameters struct {
Recursive bool
}
type InjectableEnvironmentResult struct {
Variables []string
ETag string
SecretsCount int
}
type GetAllFoldersParameters struct {
WorkspaceId string
Environment string

92
cli/packages/util/exec.go Normal file
View File

@@ -0,0 +1,92 @@
package util
import (
"fmt"
"os"
"os/exec"
"os/signal"
"runtime"
"syscall"
)
func RunCommand(singleCommand string, args []string, env []string, waitForExit bool) (*exec.Cmd, error) {
var c *exec.Cmd
var err error
if singleCommand != "" {
c, err = RunCommandFromString(singleCommand, env, waitForExit)
} else {
c, err = RunCommandFromArgs(args, env, waitForExit)
}
return c, err
}
func IsProcessRunning(p *os.Process) bool {
err := p.Signal(syscall.Signal(0))
return err == nil
}
// For "infisical run -- COMMAND"
func RunCommandFromArgs(args []string, env []string, waitForExit bool) (*exec.Cmd, error) {
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = env
err := execCommand(cmd, waitForExit)
return cmd, err
}
func execCommand(cmd *exec.Cmd, waitForExit bool) error {
sigChannel := make(chan os.Signal, 1)
signal.Notify(sigChannel)
if err := cmd.Start(); err != nil {
return err
}
go func() {
for {
sig := <-sigChannel
_ = cmd.Process.Signal(sig) // process all sigs
}
}()
if !waitForExit {
return nil
}
if err := cmd.Wait(); err != nil {
_ = cmd.Process.Signal(os.Kill)
return fmt.Errorf("failed to wait for command termination: %v", err)
}
waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus)
os.Exit(waitStatus.ExitStatus())
return nil
}
// For "infisical run --command=COMMAND"
func RunCommandFromString(command string, env []string, waitForExit bool) (*exec.Cmd, error) {
shell := [2]string{"sh", "-c"}
if runtime.GOOS == "windows" {
shell = [2]string{"cmd", "/C"}
} else {
currentShell := os.Getenv("SHELL")
if currentShell != "" {
shell[0] = currentShell
}
}
cmd := exec.Command(shell[0], shell[1], command) // #nosec G204 nosemgrep: semgrep_configs.prohibit-exec-command
cmd.Env = env
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := execCommand(cmd, waitForExit)
return cmd, err
}

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"math/rand"
"os"
@@ -298,3 +299,16 @@ func GenerateRandomString(length int) string {
}
return string(b)
}
func GenerateETagFromSecrets(secrets []models.SingleEnvironmentVariable) string {
sortedSecrets := SortSecretsByKeys(secrets)
content := []byte{}
for _, secret := range sortedSecrets {
content = append(content, []byte(secret.Key)...)
content = append(content, []byte(secret.Value)...)
}
hash := sha256.Sum256(content)
return fmt.Sprintf(`"%s"`, hex.EncodeToString(hash[:]))
}

View File

@@ -47,20 +47,20 @@ $ infisical run -- npm run dev
Used to fetch secrets via a [machine identity](/documentation/platform/identities/machine-identities) apposed to logged in credentials. Simply, export this variable in the terminal before running this command.
```bash
# Example
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=<identity-client-id> --client-secret=<identity-client-secret> --silent --plain) # --plain flag will output only the token, so it can be fed to an environment variable. --silent will disable any update messages.
# Example
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=<identity-client-id> --client-secret=<identity-client-secret> --silent --plain) # --plain flag will output only the token, so it can be fed to an environment variable. --silent will disable any update messages.
```
<Info>
Alternatively, you may use service tokens.
Please note, however, that service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities). They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
```bash
# Example
export INFISICAL_TOKEN=<service-token>
# Example
export INFISICAL_TOKEN=<service-token>
```
</Info>
</Info>
</Accordion>
<Accordion title="INFISICAL_DISABLE_UPDATE_CHECK">
@@ -69,22 +69,30 @@ $ infisical run -- npm run dev
To use, simply export this variable in the terminal before running this command.
```bash
# Example
export INFISICAL_DISABLE_UPDATE_CHECK=true
# Example
export INFISICAL_DISABLE_UPDATE_CHECK=true
```
</Accordion>
### Flags
<Accordion title="--project-config-dir">
<Accordion title="--watch">
By passing the `watch` flag, you are telling the CLI to watch for changes that happen in your Infisical project.
If secret changes happen, the command you provided will automatically be restarted with the new environment variables attached.
```bash
# Example
infisical run --watch -- printenv
```
</Accordion>
<Accordion title="--project-config-dir">
Explicitly set the directory where the .infisical.json resides. This is useful for some monorepo setups.
```bash
# Example
infisical run --project-config-dir=/some-dir -- printenv
# Example
infisical run --project-config-dir=/some-dir -- printenv
```
</Accordion>
<Accordion title="--command">
@@ -172,3 +180,19 @@ $ infisical run -- npm run dev
</Accordion>
</Accordion>
## Automatically reload command when secrets change
To automatically reload your command when secrets change, use the `--watch` flag.
```bash
infisical run --watch -- npm run dev
```
This will watch for changes in your secrets and automatically restart your command with the new secrets.
When your command restarts, it will have the new environment variables injeceted into it.
<Note>
Please note that this feature is intended for development purposes. It is not recommended to use this in production environments. Generally it's not recommended to automatically reload your application in production when remote changes are made.
</Note>

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.

View File

@@ -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.

View File

@@ -62,7 +62,7 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO."
![Azure SAML edit certificate signing option](../../../images/sso/azure/edit-saml-certificate-2.png)
</Step>
<Step title="Retrieve Identity Provider (IdP) Information from Okta">
<Step title="Retrieve Identity Provider (IdP) Information from Azure">
In the **Set up Single Sign-On with SAML** screen, copy the **Login URL** and **SAML Certificate** to use when finishing configuring Azure SAML in Infisical.
![Azure SAML identity provider values 1](../../../images/sso/azure/idp-values.png)
@@ -115,4 +115,4 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO."
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This can be a random 32-byte base64 string generated with `openssl rand -base64 32`.
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
</Note>
</Note>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 388 KiB

View File

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

View File

@@ -164,6 +164,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"

View File

@@ -24,6 +24,7 @@ import {
SRP1DTO,
SRPR1Res,
TOauthTokenExchangeDTO,
UserAgentType,
VerifyMfaTokenDTO,
VerifyMfaTokenRes,
VerifySignupInviteDTO
@@ -60,7 +61,10 @@ export const useLogin1 = () => {
});
};
export const selectOrganization = async (data: { organizationId: string }) => {
export const selectOrganization = async (data: {
organizationId: string;
userAgent?: UserAgentType;
}) => {
const { data: res } = await apiRequest.post<{ token: string }>(
"/api/v3/auth/select-organization",
data
@@ -71,11 +75,14 @@ export const selectOrganization = async (data: { organizationId: string }) => {
export const useSelectOrganization = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (details: { organizationId: string }) => {
mutationFn: async (details: { organizationId: string; userAgent?: UserAgentType }) => {
const data = await selectOrganization(details);
SecurityClient.setToken(data.token);
SecurityClient.setProviderAuthToken("");
// If a custom user agent is set, then this session is meant for another consuming application, not the web application.
if (!details.userAgent) {
SecurityClient.setToken(data.token);
SecurityClient.setProviderAuthToken("");
}
return data;
},

View File

@@ -145,3 +145,7 @@ export type IssueBackupPrivateKeyDTO = {
export type GetBackupEncryptedPrivateKeyDTO = {
verificationToken: string;
};
export enum UserAgentType {
CLI = "cli"
}

View File

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

View File

@@ -16,6 +16,7 @@ import { Button, Spinner } from "@app/components/v2";
import { SessionStorageKeys } from "@app/const";
import { useUser } from "@app/context";
import { useGetOrganizations, useLogoutUser, useSelectOrganization } from "@app/hooks/api";
import { UserAgentType } from "@app/hooks/api/auth/types";
import { Organization } from "@app/hooks/api/types";
import { getAuthToken, isLoggedIn } from "@app/reactQuery";
import { navigateUserToOrg } from "@app/views/Login/Login.utils";
@@ -68,7 +69,10 @@ export default function LoginPage() {
return;
}
const { token } = await selectOrg.mutateAsync({ organizationId: organization.id });
const { token } = await selectOrg.mutateAsync({
organizationId: organization.id,
userAgent: callbackPort ? UserAgentType.CLI : undefined
});
if (callbackPort) {
const privateKey = localStorage.getItem("PRIVATE_KEY");

View File

@@ -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}
>

View File

@@ -36,7 +36,7 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Identity}>
{(isAllowed) => {
return (
<Tooltip content="Edit Identity">
<Tooltip content="Edit CA">
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"

View File

@@ -446,26 +446,24 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
);
}}
/>
{/* {!ca && ( */}
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.ca?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("ca", false)}
>
Cancel
</Button>
</div>
{/* )} */}
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.ca?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("ca", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>

View File

@@ -59,25 +59,6 @@ export const CertificateTemplatesSection = ({ caId }: Props) => {
return (
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
{/* <div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Certificate Templates</p>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.CertificateTemplates}
>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("certificateTemplate")}
isDisabled={!isAllowed}
>
Create
</Button>
)}
</ProjectPermissionCan>
</div> */}
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Certificate Templates</h3>
<ProjectPermissionCan

View File

@@ -47,10 +47,6 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => {
const { subscription } = useSubscription();
const { data, isLoading } = useGetCaCertTemplates(caId);
// const { data, isLoading } = useListWorkspaceCertificateTemplates({
// workspaceId: currentWorkspace?.id ?? ""
// });
return (
<div>
@@ -59,7 +55,6 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => {
<THead>
<Tr>
<Th>Name</Th>
{/* <Th>Certificate Authority</Th> */}
<Th />
</Tr>
</THead>
@@ -70,7 +65,6 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => {
return (
<Tr className="h-10" key={`certificate-${certificateTemplate.id}`}>
<Td>{certificateTemplate.name}</Td>
{/* <Td>{certificateTemplate.caName}</Td> */}
<Td className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">

View File

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

View File

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

View File

@@ -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;

View File

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

View File

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