diff --git a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts index 2ead58bef..3247ef21b 100644 --- a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts +++ b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts @@ -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 diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts index ad7de408f..6ae22c869 100644 --- a/backend/src/ee/services/dynamic-secret/providers/index.ts +++ b/backend/src/ee/services/dynamic-secret/providers/index.ts @@ -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() }); diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index ae8f25581..f23a60df7 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -56,6 +56,26 @@ export const DynamicSecretElasticSearchSchema = z.object({ ca: z.string().optional() }); +export const DynamicSecretRabbitMqSchema = z.object({ + host: z.string().trim().min(1), + port: z.number(), + tags: z.array(z.string().trim()).default([]), + + username: z.string().trim().min(1), + password: z.string().trim().min(1), + + ca: z.string().optional(), + + virtualHost: z.object({ + name: z.string().trim().min(1), + permissions: z.object({ + read: z.string().trim().min(1), + write: z.string().trim().min(1), + configure: z.string().trim().min(1) + }) + }) +}); + export const DynamicSecretSqlDBSchema = z.object({ client: z.nativeEnum(SqlProviders), host: z.string().trim().toLowerCase(), @@ -154,7 +174,8 @@ export enum DynamicSecretProviders { AwsElastiCache = "aws-elasticache", MongoAtlas = "mongo-db-atlas", ElasticSearch = "elastic-search", - MongoDB = "mongo-db" + MongoDB = "mongo-db", + RabbitMq = "rabbit-mq" } export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ @@ -165,7 +186,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.AwsElastiCache), inputs: DynamicSecretAwsElastiCacheSchema }), z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }), z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }), - z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }) + z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }), + z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }) ]); export type TDynamicProviderFns = { diff --git a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts new file mode 100644 index 000000000..15492cd2d --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts @@ -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 { + 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) => { + 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 + }; +}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index f33456bd5..762e22e54 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -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, diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 3c14b851e..77ee70e57 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -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( diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index 2558b4970..91ae85982 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -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"] diff --git a/backend/src/server/routes/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts index 61a0c74e5..b5f523a54 100644 --- a/backend/src/server/routes/v3/login-router.ts +++ b/backend/src/server/routes/v3/login-router.ts @@ -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 diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 91bf40198..fb38c024e 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -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; @@ -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, diff --git a/backend/src/services/certificate-authority/certificate-authority-validators.ts b/backend/src/services/certificate-authority/certificate-authority-validators.ts index a6d6c8c23..16e7dcf49 100644 --- a/backend/src/services/certificate-authority/certificate-authority-validators.ts +++ b/backend/src/services/certificate-authority/certificate-authority-validators.ts @@ -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() diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-fns.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-fns.ts new file mode 100644 index 000000000..c6d65d836 --- /dev/null +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-fns.ts @@ -0,0 +1,4 @@ +import picomatch from "picomatch"; + +export const doesFieldValueMatchOidcPolicy = (fieldValue: string, policyValue: string) => + policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue); diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index 457e93058..4c687e86c 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -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; 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)[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." }); diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index d0fa2279a..97525b032 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -567,8 +567,8 @@ const syncSecretsAWSParameterStore = async ({ }); ssm.config.update(config); - const metadata = z.record(z.any()).parse(integration.metadata || {}); - const awsParameterStoreSecretsObj: Record = {}; + const metadata = IntegrationMetadataSchema.parse(integration.metadata); + const awsParameterStoreSecretsObj: Record = {}; 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) => { diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index 22a4ca65b..fa5176d89 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -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 +} diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index c4bbd0175..1bad5e327 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -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 diff --git a/cli/packages/util/exec.go b/cli/packages/util/exec.go new file mode 100644 index 000000000..2cdb50f42 --- /dev/null +++ b/cli/packages/util/exec.go @@ -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 +} diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index 69a310efa..b758ebd9d 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -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[:])) +} diff --git a/docs/cli/commands/run.mdx b/docs/cli/commands/run.mdx index 74aa84947..bfa4bd693 100644 --- a/docs/cli/commands/run.mdx +++ b/docs/cli/commands/run.mdx @@ -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= --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= --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. ``` 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= + # Example + export INFISICAL_TOKEN= ``` - - + @@ -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 ``` - ### Flags - + + 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 + ``` + + + 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 ``` - @@ -172,3 +180,19 @@ $ infisical run -- npm run dev + + +## 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. + + + 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. + \ No newline at end of file diff --git a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx index c37de08ff..225b884cb 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx @@ -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. - ![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) ![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) 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) @@ -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) Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret diff --git a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx index cdcb6ce2b..0b2897790 100644 --- a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx +++ b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx @@ -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. - ![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) ![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) 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) @@ -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) Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret diff --git a/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx b/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx new file mode 100644 index 000000000..f8649b727 --- /dev/null +++ b/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx @@ -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 + + + + Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-rabbit-mq.png) + + + + Name by which you want the secret to be referenced + + + + Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + + + + Maximum time-to-live for a generated secret. + + + + Your RabbitMQ host. This must be in HTTP format. _(Example: http://your-cluster-ip)_ + + + + The port that the RabbitMQ management plugin is listening on. This is `15672` by default. + + + + The name of the virtual host that the user will be assigned to. This defaults to `/`. + + + + 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 + + + + + The username of the user that will be used to provision new dynamic secret leases. + + + + The password of the user that will be used to provision new dynamic secret leases. + + + + A CA may be required if your DB requires it for incoming connections. This is often the case when connecting to a managed service. + + + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-input-modal-rabbit-mq.png) + + + + + After submitting the form, you will see a dynamic secret created in the dashboard. + + + If this step fails, you may have to add the CA certificate. + + + + + 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) + + + Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret. + + + + 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) + + + +## 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) + + + Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret + diff --git a/docs/documentation/platform/dynamic-secrets/redis.mdx b/docs/documentation/platform/dynamic-secrets/redis.mdx index 1270e6959..cb2e6a17e 100644 --- a/docs/documentation/platform/dynamic-secrets/redis.mdx +++ b/docs/documentation/platform/dynamic-secrets/redis.mdx @@ -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. - ![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) ![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) 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) @@ -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) Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret diff --git a/docs/documentation/platform/identities/oidc-auth/general.mdx b/docs/documentation/platform/identities/oidc-auth/general.mdx index ac9b4f2d7..776d175a4 100644 --- a/docs/documentation/platform/identities/oidc-auth/general.mdx +++ b/docs/documentation/platform/identities/oidc-auth/general.mdx @@ -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. + + The `subject`, `audiences`, and `claims` fields support glob pattern matching; however, we highly recommend using hardcoded values whenever possible. + + + To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project. diff --git a/docs/documentation/platform/identities/oidc-auth/github.mdx b/docs/documentation/platform/identities/oidc-auth/github.mdx index 4a9e2c671..47352a339 100644 --- a/docs/documentation/platform/identities/oidc-auth/github.mdx +++ b/docs/documentation/platform/identities/oidc-auth/github.mdx @@ -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. - 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. + The `subject`, `audiences`, and `claims` fields support glob pattern matching; however, we highly recommend using hardcoded values whenever possible. To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project. diff --git a/docs/documentation/platform/sso/azure.mdx b/docs/documentation/platform/sso/azure.mdx index cbd5a7d0e..fb4d4418f 100644 --- a/docs/documentation/platform/sso/azure.mdx +++ b/docs/documentation/platform/sso/azure.mdx @@ -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) - + 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) - \ No newline at end of file + diff --git a/docs/images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png b/docs/images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png deleted file mode 100644 index 537f20e73..000000000 Binary files a/docs/images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png and /dev/null differ diff --git a/docs/images/platform/dynamic-secrets/add-dynamic-secret-button.png b/docs/images/platform/dynamic-secrets/add-dynamic-secret-button.png index 8d0fd3ecc..537f20e73 100644 Binary files a/docs/images/platform/dynamic-secrets/add-dynamic-secret-button.png and b/docs/images/platform/dynamic-secrets/add-dynamic-secret-button.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-input-modal-rabbit-mq.png b/docs/images/platform/dynamic-secrets/dynamic-secret-input-modal-rabbit-mq.png new file mode 100644 index 000000000..41508b465 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-input-modal-rabbit-mq.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png deleted file mode 100644 index a2fd14d41..000000000 Binary files a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png and /dev/null differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png index e97554415..3d2e32e8a 100644 Binary files a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png and b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-rabbit-mq-modal.png b/docs/images/platform/dynamic-secrets/dynamic-secret-rabbit-mq-modal.png new file mode 100644 index 000000000..885a17a88 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-rabbit-mq-modal.png differ diff --git a/docs/images/platform/dynamic-secrets/lease-data-redis.png b/docs/images/platform/dynamic-secrets/lease-data-redis.png deleted file mode 100644 index f3fd51637..000000000 Binary files a/docs/images/platform/dynamic-secrets/lease-data-redis.png and /dev/null differ diff --git a/docs/images/platform/dynamic-secrets/lease-data.png b/docs/images/platform/dynamic-secrets/lease-data.png index aecd8c11d..9562da1b5 100644 Binary files a/docs/images/platform/dynamic-secrets/lease-data.png and b/docs/images/platform/dynamic-secrets/lease-data.png differ diff --git a/docs/images/platform/dynamic-secrets/lease-values-elastic-search.png b/docs/images/platform/dynamic-secrets/lease-values-elastic-search.png deleted file mode 100644 index 7d5685a81..000000000 Binary files a/docs/images/platform/dynamic-secrets/lease-values-elastic-search.png and /dev/null differ diff --git a/docs/images/platform/dynamic-secrets/lease-values-redis.png b/docs/images/platform/dynamic-secrets/lease-values-redis.png deleted file mode 100644 index 95d4a4ffd..000000000 Binary files a/docs/images/platform/dynamic-secrets/lease-values-redis.png and /dev/null differ diff --git a/docs/images/platform/dynamic-secrets/lease-values.png b/docs/images/platform/dynamic-secrets/lease-values.png index d552845f8..962bd76ec 100644 Binary files a/docs/images/platform/dynamic-secrets/lease-values.png and b/docs/images/platform/dynamic-secrets/lease-values.png differ diff --git a/docs/images/platform/dynamic-secrets/provision-lease-redis.png b/docs/images/platform/dynamic-secrets/provision-lease-redis.png deleted file mode 100644 index f5237d058..000000000 Binary files a/docs/images/platform/dynamic-secrets/provision-lease-redis.png and /dev/null differ diff --git a/docs/images/platform/dynamic-secrets/provision-lease.png b/docs/images/platform/dynamic-secrets/provision-lease.png index f144a5ae2..96b0505b9 100644 Binary files a/docs/images/platform/dynamic-secrets/provision-lease.png and b/docs/images/platform/dynamic-secrets/provision-lease.png differ diff --git a/docs/integrations/cloud/aws-parameter-store.mdx b/docs/integrations/cloud/aws-parameter-store.mdx index d53c557fb..467789647 100644 --- a/docs/integrations/cloud/aws-parameter-store.mdx +++ b/docs/integrations/cloud/aws-parameter-store.mdx @@ -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 diff --git a/docs/mint.json b/docs/mint.json index f85a6441a..3fb414e0c 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -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" diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index fcec4f2ef..27c7c87d2 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -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; }, diff --git a/frontend/src/hooks/api/auth/types.ts b/frontend/src/hooks/api/auth/types.ts index 3664e8e96..f51f7b091 100644 --- a/frontend/src/hooks/api/auth/types.ts +++ b/frontend/src/hooks/api/auth/types.ts @@ -145,3 +145,7 @@ export type IssueBackupPrivateKeyDTO = { export type GetBackupEncryptedPrivateKeyDTO = { verificationToken: string; }; + +export enum UserAgentType { + CLI = "cli" +} diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index e7e9e3318..32c9b15d0 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -23,7 +23,8 @@ export enum DynamicSecretProviders { AwsElastiCache = "aws-elasticache", MongoAtlas = "mongo-db-atlas", ElasticSearch = "elastic-search", - MongoDB = "mongo-db" + MongoDB = "mongo-db", + RabbitMq = "rabbit-mq" } export enum SqlProviders { @@ -155,6 +156,27 @@ export type TDynamicSecretProvider = apiKeyId: string; }; }; + } + | { + type: DynamicSecretProviders.RabbitMq; + inputs: { + host: string; + port: number; + + username: string; + password: string; + + tags: string[]; + virtualHost: { + name: string; + permissions: { + configure: string; + write: string; + read: string; + }; + }; + ca?: string; + }; }; export type TCreateDynamicSecretDTO = { diff --git a/frontend/src/pages/login/select-organization.tsx b/frontend/src/pages/login/select-organization.tsx index cf953664d..13ec3e414 100644 --- a/frontend/src/pages/login/select-organization.tsx +++ b/frontend/src/pages/login/select-organization.tsx @@ -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"); diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx index 30663d470..fdfb17e0d 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx @@ -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 } }) => ( - + This field supports glob patterns} + > + + + } + > )} @@ -267,7 +280,19 @@ export const IdentityOidcAuthForm = ({ control={control} name="boundAudiences" render={({ field, fieldState: { error } }) => ( - + This field supports glob patterns} + > + + + } + > )} @@ -282,6 +307,16 @@ export const IdentityOidcAuthForm = ({ This field supports glob patterns} + > + + + ) : undefined + } isError={Boolean(error)} errorText={error?.message} > diff --git a/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx b/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx index 4bff13225..9d5d20337 100644 --- a/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx +++ b/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx @@ -36,7 +36,7 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => { {(isAllowed) => { return ( - + { ); }} /> - {/* {!ca && ( */} -
- - -
- {/* )} */} +
+ + +
diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx index e76d2ce33..b1cce918f 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx @@ -59,25 +59,6 @@ export const CertificateTemplatesSection = ({ caId }: Props) => { return (
- {/*
-

Certificate Templates

- - {(isAllowed) => ( - - )} - -
*/}

Certificate Templates

{ const { subscription } = useSubscription(); const { data, isLoading } = useGetCaCertTemplates(caId); - - // const { data, isLoading } = useListWorkspaceCertificateTemplates({ - // workspaceId: currentWorkspace?.id ?? "" - // }); return (
@@ -59,7 +55,6 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => { Name - {/* Certificate Authority */} @@ -70,7 +65,6 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => { return ( {certificateTemplate.name} - {/* {certificateTemplate.caName} */} diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx index 19c3191a4..90eb74012 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { DiRedis } from "react-icons/di"; -import { SiApachecassandra, SiElasticsearch, SiMongodb } from "react-icons/si"; +import { SiApachecassandra, SiElasticsearch, SiMongodb, SiRabbitmq } from "react-icons/si"; import { faAws } from "@fortawesome/free-brands-svg-icons"; import { faDatabase } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -15,6 +15,7 @@ import { CassandraInputForm } from "./CassandraInputForm"; import { ElasticSearchInputForm } from "./ElasticSearchInputForm"; import { MongoAtlasInputForm } from "./MongoAtlasInputForm"; import { MongoDBDatabaseInputForm } from "./MongoDBInputForm"; +import { RabbitMqInputForm } from "./RabbitMqInputForm"; import { RedisInputForm } from "./RedisInputForm"; import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm"; @@ -71,6 +72,11 @@ const DYNAMIC_SECRET_LIST = [ icon: , provider: DynamicSecretProviders.ElasticSearch, title: "Elastic Search" + }, + { + icon: , + provider: DynamicSecretProviders.RabbitMq, + title: "RabbitMQ" } ]; @@ -276,6 +282,24 @@ export const CreateDynamicSecretForm = ({ /> )} + {wizardStep === WizardSteps.ProviderInputs && + selectedProvider === DynamicSecretProviders.RabbitMq && ( + + + + )} diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx new file mode 100644 index 000000000..dae0cd478 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx @@ -0,0 +1,421 @@ +import { Controller, useForm } from "react-hook-form"; +import Link from "next/link"; +import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import ms from "ms"; +import { z } from "zod"; + +import { TtlFormLabel } from "@app/components/features"; +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, FormLabel, IconButton, Input, SecretInput } from "@app/components/v2"; +import { useCreateDynamicSecret } from "@app/hooks/api"; +import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; + +const formSchema = z.object({ + provider: z.object({ + host: z.string().trim().min(1), + port: z.coerce.number(), // important: this is the management plugin port + + username: z.string(), + password: z.string(), + + tags: z.array(z.string().trim()), + virtualHost: z.object({ + name: z.string().trim().min(1), + permissions: z.object({ + read: z.string().trim().min(1), + write: z.string().trim().min(1), + configure: z.string().trim().min(1) + }) + }), + ca: z.string().optional() + }), + defaultTTL: z.string().superRefine((val, ctx) => { + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") +}); +type TForm = z.infer; + +type Props = { + onCompleted: () => void; + onCancel: () => void; + secretPath: string; + projectSlug: string; + environment: string; +}; + +export const RabbitMqInputForm = ({ + onCompleted, + onCancel, + environment, + secretPath, + projectSlug +}: Props) => { + const { + control, + formState: { isSubmitting }, + handleSubmit, + setValue, + watch + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + provider: { + port: 15672, + virtualHost: { + name: "/", + permissions: { + read: ".*", + write: ".*", + configure: ".*" + } + }, + tags: [] + } + } + }); + + const createDynamicSecret = useCreateDynamicSecret(); + + const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + // wait till previous request is finished + if (createDynamicSecret.isLoading) return; + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.RabbitMq, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment + }); + onCompleted(); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }; + + const selectedTags = watch("provider.tags"); + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Configuration +
+
+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ +
+ ( + + + + )} + /> + + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+
+ +
+ +

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

+

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

+

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

+
+ } + /> +
+ {selectedTags.map((_, i) => ( + ( + +
+ + { + if (selectedTags && selectedTags?.length > 1) { + setValue( + "provider.tags", + selectedTags.filter((__, idx) => idx !== i) + ); + } + }} + > + + +
+
+ )} + /> + ))} +
+
+
+ +
+
+ ( + + + + )} + /> +
+
+
+
+
+ + +
+ +
+ ); +}; diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx index 100e2de91..85be20fc1 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx @@ -140,6 +140,24 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => { ); } + if (provider === DynamicSecretProviders.RabbitMq) { + const { DB_USERNAME, DB_PASSWORD } = data as { + DB_USERNAME: string; + DB_PASSWORD: string; + }; + + return ( +
+ + +
+ ); + } + if (provider === DynamicSecretProviders.ElasticSearch) { const { DB_USERNAME, DB_PASSWORD } = data as { DB_USERNAME: string; diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx index 61efdb42e..8f95bcc94 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx @@ -10,6 +10,7 @@ import { EditDynamicSecretCassandraForm } from "./EditDynamicSecretCassandraForm import { EditDynamicSecretElasticSearchForm } from "./EditDynamicSecretElasticSearchForm"; import { EditDynamicSecretMongoAtlasForm } from "./EditDynamicSecretMongoAtlasForm"; import { EditDynamicSecretMongoDBForm } from "./EditDynamicSecretMongoDBForm"; +import { EditDynamicSecretRabbitMqForm } from "./EditDynamicSecretRabbitMqForm"; import { EditDynamicSecretRedisProviderForm } from "./EditDynamicSecretRedisProviderForm"; import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm"; @@ -183,6 +184,24 @@ export const EditDynamicSecretForm = ({ /> )} + + {dynamicSecretDetails?.type === DynamicSecretProviders.RabbitMq && ( + + + + )} ); }; diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRabbitMqForm.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRabbitMqForm.tsx new file mode 100644 index 000000000..a28c257b0 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRabbitMqForm.tsx @@ -0,0 +1,421 @@ +import { Controller, useForm } from "react-hook-form"; +import Link from "next/link"; +import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import ms from "ms"; +import { z } from "zod"; + +import { TtlFormLabel } from "@app/components/features"; +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, FormLabel, IconButton, Input, SecretInput } from "@app/components/v2"; +import { useUpdateDynamicSecret } from "@app/hooks/api"; +import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; + +const formSchema = z.object({ + inputs: z.object({ + host: z.string().trim().min(1), + port: z.coerce.number(), // important: this is the management plugin port + + username: z.string(), + password: z.string(), + + tags: z.array(z.string().trim()), + virtualHost: z.object({ + name: z.string().trim().min(1), + permissions: z.object({ + read: z.string().trim().min(1), + write: z.string().trim().min(1), + configure: z.string().trim().min(1) + }) + }), + ca: z.string().optional() + }), + defaultTTL: z.string().superRefine((val, ctx) => { + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + newName: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") +}); +type TForm = z.infer; + +type Props = { + onClose: () => void; + dynamicSecret: TDynamicSecret & { inputs: unknown }; + secretPath: string; + environment: string; + projectSlug: string; +}; +export const EditDynamicSecretRabbitMqForm = ({ + onClose, + dynamicSecret, + secretPath, + environment, + projectSlug +}: Props) => { + const { + control, + formState: { isSubmitting }, + handleSubmit, + setValue, + watch + } = useForm({ + resolver: zodResolver(formSchema), + values: { + defaultTTL: dynamicSecret.defaultTTL, + maxTTL: dynamicSecret.maxTTL, + newName: dynamicSecret.name, + inputs: { + ...(dynamicSecret.inputs as TForm["inputs"]) + } + } + }); + + const updateDynamicSecret = useUpdateDynamicSecret(); + + const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => { + // wait till previous request is finished + if (updateDynamicSecret.isLoading) return; + try { + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs, + newName: newName === dynamicSecret.name ? undefined : newName + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to update dynamic secret" + }); + } + }; + + const selectedTags = watch("inputs.tags"); + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Configuration +
+
+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ +
+ ( + + + + )} + /> + + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+
+ +
+ +

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

+

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

+

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

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