diff --git a/.husky/pre-commit b/.husky/pre-commit index 4f18d2521..9a9f7b9e4 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,6 +1,12 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" +# Check if infisical is installed +if ! command -v infisical >/dev/null 2>&1; then + echo "\nError: Infisical CLI is not installed. Please install the Infisical CLI before comitting.\n You can refer to the documentation at https://infisical.com/docs/cli/overview\n\n" + exit 1 +fi + npx lint-staged infisical scan git-changes --staged -v diff --git a/backend/src/db/migrations/20241121131344_make-identity-metadata-not-nullable-again.ts b/backend/src/db/migrations/20241121131344_make-identity-metadata-not-nullable-again.ts new file mode 100644 index 000000000..fb58c02df --- /dev/null +++ b/backend/src/db/migrations/20241121131344_make-identity-metadata-not-nullable-again.ts @@ -0,0 +1,20 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.IdentityMetadata, "value")) { + await knex(TableName.IdentityMetadata).whereNull("value").delete(); + await knex.schema.alterTable(TableName.IdentityMetadata, (t) => { + t.string("value", 1020).notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.IdentityMetadata, "value")) { + await knex.schema.alterTable(TableName.IdentityMetadata, (t) => { + t.string("value", 1020).alter(); + }); + } +} diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts index f70985379..e51462be6 100644 --- a/backend/src/ee/services/dynamic-secret/providers/index.ts +++ b/backend/src/ee/services/dynamic-secret/providers/index.ts @@ -13,6 +13,7 @@ import { RabbitMqProvider } from "./rabbit-mq"; import { RedisDatabaseProvider } from "./redis"; import { SapHanaProvider } from "./sap-hana"; import { SqlDatabaseProvider } from "./sql-database"; +import { TotpProvider } from "./totp"; export const buildDynamicSecretProviders = () => ({ [DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(), @@ -27,5 +28,6 @@ export const buildDynamicSecretProviders = () => ({ [DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(), [DynamicSecretProviders.Ldap]: LdapProvider(), [DynamicSecretProviders.SapHana]: SapHanaProvider(), - [DynamicSecretProviders.Snowflake]: SnowflakeProvider() + [DynamicSecretProviders.Snowflake]: SnowflakeProvider(), + [DynamicSecretProviders.Totp]: TotpProvider() }); diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index d98215fd4..d803aab5b 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -17,6 +17,17 @@ export enum LdapCredentialType { Static = "static" } +export enum TotpConfigType { + URL = "url", + MANUAL = "manual" +} + +export enum TotpAlgorithm { + SHA1 = "sha1", + SHA256 = "sha256", + SHA512 = "sha512" +} + export const DynamicSecretRedisDBSchema = z.object({ host: z.string().trim().toLowerCase(), port: z.number(), @@ -221,6 +232,34 @@ export const LdapSchema = z.union([ }) ]); +export const DynamicSecretTotpSchema = z.discriminatedUnion("configType", [ + z.object({ + configType: z.literal(TotpConfigType.URL), + url: z + .string() + .url() + .trim() + .min(1) + .refine((val) => { + const urlObj = new URL(val); + const secret = urlObj.searchParams.get("secret"); + + return Boolean(secret); + }, "OTP URL must contain secret field") + }), + z.object({ + configType: z.literal(TotpConfigType.MANUAL), + secret: z + .string() + .trim() + .min(1) + .transform((val) => val.replace(/\s+/g, "")), + period: z.number().optional(), + algorithm: z.nativeEnum(TotpAlgorithm).optional(), + digits: z.number().optional() + }) +]); + export enum DynamicSecretProviders { SqlDatabase = "sql-database", Cassandra = "cassandra", @@ -234,7 +273,8 @@ export enum DynamicSecretProviders { AzureEntraID = "azure-entra-id", Ldap = "ldap", SapHana = "sap-hana", - Snowflake = "snowflake" + Snowflake = "snowflake", + Totp = "totp" } export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ @@ -250,7 +290,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }), z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }), z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }), - z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }) + z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }), + z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }) ]); export type TDynamicProviderFns = { diff --git a/backend/src/ee/services/dynamic-secret/providers/totp.ts b/backend/src/ee/services/dynamic-secret/providers/totp.ts new file mode 100644 index 000000000..4e3ab6eb2 --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/totp.ts @@ -0,0 +1,92 @@ +import { authenticator } from "otplib"; +import { HashAlgorithms } from "otplib/core"; + +import { BadRequestError } from "@app/lib/errors"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +import { DynamicSecretTotpSchema, TDynamicProviderFns, TotpConfigType } from "./models"; + +export const TotpProvider = (): TDynamicProviderFns => { + const validateProviderInputs = async (inputs: unknown) => { + const providerInputs = await DynamicSecretTotpSchema.parseAsync(inputs); + + return providerInputs; + }; + + const validateConnection = async () => { + return true; + }; + + const create = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + + const entityId = alphaNumericNanoId(32); + const authenticatorInstance = authenticator.clone(); + + let secret: string; + let period: number | null | undefined; + let digits: number | null | undefined; + let algorithm: HashAlgorithms | null | undefined; + + if (providerInputs.configType === TotpConfigType.URL) { + const urlObj = new URL(providerInputs.url); + secret = urlObj.searchParams.get("secret") as string; + const periodFromUrl = urlObj.searchParams.get("period"); + const digitsFromUrl = urlObj.searchParams.get("digits"); + const algorithmFromUrl = urlObj.searchParams.get("algorithm"); + + if (periodFromUrl) { + period = +periodFromUrl; + } + + if (digitsFromUrl) { + digits = +digitsFromUrl; + } + + if (algorithmFromUrl) { + algorithm = algorithmFromUrl.toLowerCase() as HashAlgorithms; + } + } else { + secret = providerInputs.secret; + period = providerInputs.period; + digits = providerInputs.digits; + algorithm = providerInputs.algorithm as unknown as HashAlgorithms; + } + + if (digits) { + authenticatorInstance.options = { digits }; + } + + if (algorithm) { + authenticatorInstance.options = { algorithm }; + } + + if (period) { + authenticatorInstance.options = { step: period }; + } + + return { + entityId, + data: { TOTP: authenticatorInstance.generate(secret), TIME_REMAINING: authenticatorInstance.timeRemaining() } + }; + }; + + const revoke = async (_inputs: unknown, entityId: string) => { + return { entityId }; + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const renew = async (_inputs: unknown, _entityId: string) => { + throw new BadRequestError({ + message: "Lease renewal is not supported for TOTPs" + }); + }; + + return { + validateProviderInputs, + validateConnection, + create, + revoke, + renew + }; +}; diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index 8ad58f528..730ad3bbc 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -127,14 +127,15 @@ export const permissionDALFactory = (db: TDbClient) => { const getProjectPermission = async (userId: string, projectId: string) => { try { + const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId"); const docs = await db .replicaNode()(TableName.Users) .where(`${TableName.Users}.id`, userId) - .leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) .leftJoin(TableName.GroupProjectMembership, (queryBuilder) => { void queryBuilder .on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId])) - .andOn(`${TableName.GroupProjectMembership}.groupId`, `${TableName.UserGroupMembership}.groupId`); + // @ts-expect-error akhilmhdh: this is valid knexjs query. Its just ts type argument is missing it + .andOnIn(`${TableName.GroupProjectMembership}.groupId`, subQueryUserGroups); }) .leftJoin( TableName.GroupProjectMembershipRole, diff --git a/backend/src/ee/services/permission/permission-types.ts b/backend/src/ee/services/permission/permission-types.ts index 8df85054d..1ad0b205b 100644 --- a/backend/src/ee/services/permission/permission-types.ts +++ b/backend/src/ee/services/permission/permission-types.ts @@ -1,14 +1,7 @@ import picomatch from "picomatch"; import { z } from "zod"; -export enum PermissionConditionOperators { - $IN = "$in", - $ALL = "$all", - $REGEX = "$regex", - $EQ = "$eq", - $NEQ = "$ne", - $GLOB = "$glob" -} +import { PermissionConditionOperators } from "@app/lib/casl"; export const PermissionConditionSchema = { [PermissionConditionOperators.$IN]: z.string().trim().min(1).array(), diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 591cdd343..c6e574fb1 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -1,10 +1,10 @@ import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability"; import { z } from "zod"; -import { conditionsMatcher } from "@app/lib/casl"; +import { conditionsMatcher, PermissionConditionOperators } from "@app/lib/casl"; import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; -import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types"; +import { PermissionConditionSchema } from "./permission-types"; export enum ProjectPermissionActions { Read = "read", diff --git a/backend/src/lib/casl/index.ts b/backend/src/lib/casl/index.ts index 71625e181..ad4bf028f 100644 --- a/backend/src/lib/casl/index.ts +++ b/backend/src/lib/casl/index.ts @@ -54,3 +54,12 @@ export const isAtLeastAsPrivileged = (permissions1: MongoAbility, permissions2: return set1.size >= set2.size; }; + +export enum PermissionConditionOperators { + $IN = "$in", + $ALL = "$all", + $REGEX = "$regex", + $EQ = "$eq", + $NEQ = "$ne", + $GLOB = "$glob" +} diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index e60e245eb..0cbf30f09 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -1,4 +1,4 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, PureAbility } from "@casl/ability"; import fastifyPlugin from "fastify-plugin"; import jwt from "jsonwebtoken"; import { ZodError } from "zod"; @@ -77,7 +77,13 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider requestId: req.id, statusCode: HttpStatusCodes.Forbidden, error: "PermissionDenied", - message: `You are not allowed to ${error.action} on ${error.subjectType} - ${JSON.stringify(error.subject)}` + message: `You are not allowed to ${error.action} on ${error.subjectType}`, + details: (error.ability as PureAbility).rulesFor(error.action as string, error.subjectType).map((el) => ({ + action: el.action, + inverted: el.inverted, + subject: el.subject, + conditions: el.conditions + })) }); } else if (error instanceof ForbiddenRequestError) { void res.status(HttpStatusCodes.Forbidden).send({ diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index de4a1d848..bbbe57631 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -51,6 +51,7 @@ export const DefaultResponseErrorsSchema = { requestId: z.string(), statusCode: z.literal(403), message: z.string(), + details: z.any().optional(), error: z.string() }), 500: z.object({ diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index 39f2f6589..847030d76 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -385,8 +385,8 @@ export const identityTokenAuthServiceFactory = ({ actorOrgId }: TUpdateTokenAuthTokenDTO) => { const foundToken = await identityAccessTokenDAL.findOne({ - id: tokenId, - authMethod: IdentityAuthMethod.TOKEN_AUTH + [`${TableName.IdentityAccessToken}.id` as "id"]: tokenId, + [`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH }); if (!foundToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` }); @@ -444,8 +444,8 @@ export const identityTokenAuthServiceFactory = ({ }: TRevokeTokenAuthTokenDTO) => { const identityAccessToken = await identityAccessTokenDAL.findOne({ [`${TableName.IdentityAccessToken}.id` as "id"]: tokenId, - isAccessTokenRevoked: false, - authMethod: IdentityAuthMethod.TOKEN_AUTH + [`${TableName.IdentityAccessToken}.isAccessTokenRevoked` as "isAccessTokenRevoked"]: false, + [`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH }); if (!identityAccessToken) throw new NotFoundError({ diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 197c1f940..39b045288 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -3075,7 +3075,7 @@ const syncSecretsTerraformCloud = async ({ }) => { // get secrets from Terraform Cloud const terraformSecrets = ( - await request.get<{ data: { attributes: { key: string; value: string }; id: string }[] }>( + await request.get<{ data: { attributes: { key: string; value: string; sensitive: boolean }; id: string }[] }>( `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars`, { headers: { @@ -3089,7 +3089,7 @@ const syncSecretsTerraformCloud = async ({ ...obj, [secret.attributes.key]: secret }), - {} as Record + {} as Record ); const secretsToAdd: { [key: string]: string } = {}; @@ -3170,7 +3170,8 @@ const syncSecretsTerraformCloud = async ({ attributes: { key, value: secrets[key]?.value, - category: integration.targetService + category: integration.targetService, + sensitive: true } } }, @@ -3183,7 +3184,11 @@ const syncSecretsTerraformCloud = async ({ } ); // case: secret exists in Terraform Cloud - } else if (secrets[key]?.value !== terraformSecrets[key].attributes.value) { + } else if ( + // we now set secrets to sensitive in Terraform Cloud, this checks if existing secrets are not sensitive and updates them accordingly + !terraformSecrets[key].attributes.sensitive || + secrets[key]?.value !== terraformSecrets[key].attributes.value + ) { // -> update secret await request.patch( `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${terraformSecrets[key].id}`, @@ -3193,7 +3198,8 @@ const syncSecretsTerraformCloud = async ({ id: terraformSecrets[key].id, attributes: { ...terraformSecrets[key], - value: secrets[key]?.value + value: secrets[key]?.value, + sensitive: true } } }, diff --git a/docs/cli/overview.mdx b/docs/cli/overview.mdx index 397d1f474..bab5cbcc4 100644 --- a/docs/cli/overview.mdx +++ b/docs/cli/overview.mdx @@ -3,7 +3,7 @@ title: 'Install' description: "Infisical's CLI is one of the best way to manage environments and secrets. Install it here" --- -The Infisical CLI is powerful command line tool that can be used to retrieve, modify, export and inject secrets into any process or application as environment variables. +The Infisical CLI is a powerful command line tool that can be used to retrieve, modify, export and inject secrets into any process or application as environment variables. You can use it across various environments, whether it's local development, CI/CD, staging, or production. ## Installation diff --git a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx index 225b884cb..2cf4edc0e 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx @@ -69,7 +69,7 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -131,12 +131,12 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa ## 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. +This will allow you to 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. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx index 7a3976e0f..730e2b287 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx @@ -66,7 +66,7 @@ Replace **\** with your AWS account id and **\** w - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -138,12 +138,12 @@ Replace **\** with your AWS account id and **\** w ## 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 lease details and delete the lease ahead of its expiration time. +This will allow you to see the lease details and delete the lease ahead of its expiration time. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx b/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx index 8a71772b1..515efabeb 100644 --- a/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx +++ b/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx @@ -98,7 +98,7 @@ Click on Add assignments. Search for the application name you created and select - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -151,12 +151,12 @@ Click on Add assignments. Search for the application name you created and select ## 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. +This will allow you to 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. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/cassandra.mdx b/docs/documentation/platform/dynamic-secrets/cassandra.mdx index fd46c8288..e7ec4f69d 100644 --- a/docs/documentation/platform/dynamic-secrets/cassandra.mdx +++ b/docs/documentation/platform/dynamic-secrets/cassandra.mdx @@ -39,7 +39,7 @@ The above configuration allows user creation and granting permissions. - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -116,12 +116,12 @@ The above configuration allows user creation and granting permissions. ## 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 lease details and delete the lease ahead of its expiration time. +This will allow you to see the lease details and delete the lease ahead of its expiration time. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx index 0b2897790..0e1bc5104 100644 --- a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx +++ b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx @@ -34,7 +34,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -114,12 +114,12 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch ## 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. +This will allow you to 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. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/ldap.mdx b/docs/documentation/platform/dynamic-secrets/ldap.mdx index ac06a7576..a1731432c 100644 --- a/docs/documentation/platform/dynamic-secrets/ldap.mdx +++ b/docs/documentation/platform/dynamic-secrets/ldap.mdx @@ -31,7 +31,7 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -171,7 +171,7 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) diff --git a/docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx b/docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx index f9352f2e5..5eda1669e 100644 --- a/docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx +++ b/docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx @@ -30,7 +30,7 @@ Create a project scopped API Key with the required permission in your Mongo Atla - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -101,12 +101,12 @@ Create a project scopped API Key with the required permission in your Mongo Atla ## 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. +This will allow you to 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. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/mongo-db.mdx b/docs/documentation/platform/dynamic-secrets/mongo-db.mdx index f34d578dc..ec384f7a9 100644 --- a/docs/documentation/platform/dynamic-secrets/mongo-db.mdx +++ b/docs/documentation/platform/dynamic-secrets/mongo-db.mdx @@ -31,7 +31,7 @@ Create a user with the required permission in your MongoDB instance. This user w - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -103,12 +103,12 @@ Create a user with the required permission in your MongoDB instance. This user w ## 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. +This will allow you to 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. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/mssql.mdx b/docs/documentation/platform/dynamic-secrets/mssql.mdx index fb666adca..aca42c5c4 100644 --- a/docs/documentation/platform/dynamic-secrets/mssql.mdx +++ b/docs/documentation/platform/dynamic-secrets/mssql.mdx @@ -28,7 +28,7 @@ Create a user with the required permission in your SQL instance. This user will - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -105,12 +105,12 @@ Create a user with the required permission in your SQL instance. This user will ## 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 the lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete the 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. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/mysql.mdx b/docs/documentation/platform/dynamic-secrets/mysql.mdx index d85f4b7bb..da39c0a56 100644 --- a/docs/documentation/platform/dynamic-secrets/mysql.mdx +++ b/docs/documentation/platform/dynamic-secrets/mysql.mdx @@ -27,7 +27,7 @@ Create a user with the required permission in your SQL instance. This user will - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -102,12 +102,12 @@ Create a user with the required permission in your SQL instance. This user will ## 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. +This will allow you to 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. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/oracle.mdx b/docs/documentation/platform/dynamic-secrets/oracle.mdx index a6fb68913..e8fa86028 100644 --- a/docs/documentation/platform/dynamic-secrets/oracle.mdx +++ b/docs/documentation/platform/dynamic-secrets/oracle.mdx @@ -27,7 +27,7 @@ Create a user with the required permission in your SQL instance. This user will - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -102,12 +102,12 @@ Create a user with the required permission in your SQL instance. This user will ## 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. +This will allow you to 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. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/postgresql.mdx b/docs/documentation/platform/dynamic-secrets/postgresql.mdx index ebc19b011..5216d87af 100644 --- a/docs/documentation/platform/dynamic-secrets/postgresql.mdx +++ b/docs/documentation/platform/dynamic-secrets/postgresql.mdx @@ -28,7 +28,7 @@ Create a user with the required permission in your SQL instance. This user will - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -105,12 +105,12 @@ Create a user with the required permission in your SQL instance. This user will ## 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 the lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete the 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. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx b/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx index f8649b727..6ac5ac069 100644 --- a/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx +++ b/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx @@ -28,7 +28,7 @@ The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credential - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -103,12 +103,12 @@ The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credential ## 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. +This will allow you to 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. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/redis.mdx b/docs/documentation/platform/dynamic-secrets/redis.mdx index cb2e6a17e..43fbc6b61 100644 --- a/docs/documentation/platform/dynamic-secrets/redis.mdx +++ b/docs/documentation/platform/dynamic-secrets/redis.mdx @@ -27,7 +27,7 @@ Create a user with the required permission in your Redis instance. This user wil - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -93,12 +93,12 @@ Create a user with the required permission in your Redis instance. This user wil ## 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. +This will allow you to 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. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/sap-hana.mdx b/docs/documentation/platform/dynamic-secrets/sap-hana.mdx index 3c2a837d3..668777549 100644 --- a/docs/documentation/platform/dynamic-secrets/sap-hana.mdx +++ b/docs/documentation/platform/dynamic-secrets/sap-hana.mdx @@ -30,7 +30,7 @@ The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database c - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -106,13 +106,13 @@ The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database c ## 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 lease details and delete the lease ahead of its expiration time. +This will allow you to see the lease details and delete the lease ahead of its expiration time. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/snowflake.mdx b/docs/documentation/platform/dynamic-secrets/snowflake.mdx index f5e06ba76..75db96c8f 100644 --- a/docs/documentation/platform/dynamic-secrets/snowflake.mdx +++ b/docs/documentation/platform/dynamic-secrets/snowflake.mdx @@ -109,7 +109,7 @@ Infisical's Snowflake dynamic secrets allow you to generate Snowflake user crede ## 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 lease details and delete the lease ahead of its expiration time. +This will allow you to see the lease details and delete the lease ahead of its expiration time. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) diff --git a/docs/documentation/platform/dynamic-secrets/totp.mdx b/docs/documentation/platform/dynamic-secrets/totp.mdx new file mode 100644 index 000000000..201a402e1 --- /dev/null +++ b/docs/documentation/platform/dynamic-secrets/totp.mdx @@ -0,0 +1,70 @@ +--- +title: "TOTP" +description: "Learn how to dynamically generate time-based one-time passwords." +--- + +The Infisical TOTP dynamic secret allows you to generate time-based one-time passwords on demand. + +## Prerequisite + +- Infisical requires either an OTP url or a secret key from a TOTP provider. + +## Set up Dynamic Secrets with TOTP + + + + 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-totp.png) + + + + Name by which you want the secret to be referenced + + + There are two supported configuration types - `url` and `manual`. + + When `url` is selected, you can configure the TOTP generator using the OTP URL. + + When `manual` is selected, you can configure the TOTP generator using the secret key along with other configurations like period, number of digits, and algorithm. + + + OTP URL in `otpauth://` format used to generate TOTP codes. + + + Base32 encoded secret used to generate TOTP codes. + + + Time interval in seconds between generating new TOTP codes. + + + Number of digits to generate in each TOTP code. + + + Hash algorithm to use when generating TOTP codes. The supported algorithms are sha1, sha256, and sha512. + + + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-url.png) + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-manual.png) + + + + After submitting the form, you will see a dynamic secret created in the dashboard. + + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand TOTPs. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + + Once you click the `Generate` button, a new secret lease will be generated and the TOTP will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/totp-lease-value.png) + + + diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-modal-totp.png b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-totp.png new file mode 100644 index 000000000..53326ebe2 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-totp.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-manual.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-manual.png new file mode 100644 index 000000000..788852d85 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-manual.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-url.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-url.png new file mode 100644 index 000000000..ede474a59 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-url.png differ diff --git a/docs/images/platform/dynamic-secrets/totp-lease-value.png b/docs/images/platform/dynamic-secrets/totp-lease-value.png new file mode 100644 index 000000000..af2ffe8e1 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/totp-lease-value.png differ diff --git a/docs/integrations/frameworks/terraform.mdx b/docs/integrations/frameworks/terraform.mdx index e7a60ac7e..0f057d918 100644 --- a/docs/integrations/frameworks/terraform.mdx +++ b/docs/integrations/frameworks/terraform.mdx @@ -1,8 +1,9 @@ --- -title: "Terraform" +title: "Terraform Provider" description: "Learn how to fetch Secrets From Infisical With Terraform." +url: "https://registry.terraform.io/providers/Infisical/infisical/latest/docs" --- - +{/* This guide provides step-by-step guidance on how to fetch secrets from Infisical using Terraform. ## Prerequisites @@ -98,4 +99,4 @@ Terraform will now fetch your secrets from Infisical and display them as output ## Conclusion -You have now successfully set up and used the Infisical provider with Terraform to fetch secrets. For more information, visit the [Infisical documentation](https://registry.terraform.io/providers/Infisical/infisical/latest/docs). +You have now successfully set up and used the Infisical provider with Terraform to fetch secrets. For more information, visit the [Infisical documentation](https://registry.terraform.io/providers/Infisical/infisical/latest/docs). */} diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx index c39041375..8ea24d65f 100644 --- a/docs/integrations/platforms/kubernetes.mdx +++ b/docs/integrations/platforms/kubernetes.mdx @@ -94,7 +94,7 @@ spec: projectSlug: new-ob-em envSlug: dev # "dev", "staging", "prod", etc.. secretsPath: "/" # Root is "/" - recursive: true # Wether or not to use recursive mode (Fetches all secrets in an environment from a given secret path, and all folders inside the path) / defaults to false + recursive: true # Whether or not to use recursive mode (Fetches all secrets in an environment from a given secret path, and all folders inside the path) / defaults to false credentialsRef: secretName: universal-auth-credentials secretNamespace: default diff --git a/docs/mint.json b/docs/mint.json index f070ae88a..e4e487915 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -189,7 +189,8 @@ "documentation/platform/dynamic-secrets/azure-entra-id", "documentation/platform/dynamic-secrets/ldap", "documentation/platform/dynamic-secrets/sap-hana", - "documentation/platform/dynamic-secrets/snowflake" + "documentation/platform/dynamic-secrets/snowflake", + "documentation/platform/dynamic-secrets/totp" ] }, "documentation/platform/project-templates", diff --git a/frontend/src/components/notifications/Notifications.tsx b/frontend/src/components/notifications/Notifications.tsx index befe79e4f..23b4eebaa 100644 --- a/frontend/src/components/notifications/Notifications.tsx +++ b/frontend/src/components/notifications/Notifications.tsx @@ -4,13 +4,15 @@ import { Id, toast, ToastContainer, ToastOptions, TypeOptions } from "react-toas export type TNotification = { title?: string; text: ReactNode; + children?: ReactNode; }; -export const NotificationContent = ({ title, text }: TNotification) => { +export const NotificationContent = ({ title, text, children }: TNotification) => { return (
{title &&
{title}
} -
{text}
+
{text}
+ {children &&
{children}
}
); }; @@ -23,7 +25,13 @@ export const createNotification = ( position: "bottom-right", ...toastProps, theme: "dark", - type: myProps?.type || "info", + type: myProps?.type || "info" }); -export const NotificationContainer = () => ; +export const NotificationContainer = () => ( + +); diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 307ef74af..8f10d5f21 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -33,6 +33,15 @@ export enum PermissionConditionOperators { $GLOB = "$glob" } +export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = { + [PermissionConditionOperators.$EQ]: "equal to", + [PermissionConditionOperators.$IN]: "contains", + [PermissionConditionOperators.$ALL]: "contains all", + [PermissionConditionOperators.$NEQ]: "not equal to", + [PermissionConditionOperators.$GLOB]: "matches glob pattern", + [PermissionConditionOperators.$REGEX]: "matches regex pattern" +}; + export type TPermissionConditionOperators = { [PermissionConditionOperators.$IN]: string[]; [PermissionConditionOperators.$ALL]: string[]; diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 567e1d0b2..5221b5033 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -9,7 +9,7 @@ export type TGetAuditLogsFilter = { eventMetadata?: Record; actorType?: ActorType; projectId?: string; - actorId?: string; // user ID format + actor?: string; // user ID format startDate?: Date; endDate?: Date; limit: number; diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 7ac8d4147..35b08df32 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -28,7 +28,8 @@ export enum DynamicSecretProviders { AzureEntraId = "azure-entra-id", Ldap = "ldap", SapHana = "sap-hana", - Snowflake = "snowflake" + Snowflake = "snowflake", + Totp = "totp" } export enum SqlProviders { @@ -230,6 +231,21 @@ export type TDynamicSecretProvider = revocationStatement: string; renewStatement?: string; }; + } + | { + type: DynamicSecretProviders.Totp; + inputs: + | { + configType: "url"; + url: string; + } + | { + configType: "manual"; + secret: string; + period?: number; + algorithm?: string; + digits?: number; + }; }; export type TCreateDynamicSecretDTO = { projectSlug: string; diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index 9126559d3..08fc84ffa 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -1,3 +1,4 @@ +import { PureAbility } from "@casl/ability"; import { ZodIssue } from "zod"; export type { TAccessApprovalPolicy } from "./accessApproval/types"; @@ -53,10 +54,21 @@ export type TApiErrors = requestId: string; error: ApiErrorTypes.ValidationError; message: ZodIssue[]; + statusCode: 401; + } + | { + requestId: string; + error: ApiErrorTypes.UnauthorizedError; + message: string; + statusCode: 401; + } + | { + requestId: string; + error: ApiErrorTypes.ForbiddenError; + message: string; + details: PureAbility["rules"]; statusCode: 403; } - | { requestId: string; error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 403 } - | { requestId: string; error: ApiErrorTypes.UnauthorizedError; message: string; statusCode: 401 } | { requestId: string; statusCode: 400; diff --git a/frontend/src/reactQuery.tsx b/frontend/src/reactQuery.tsx index 464efe105..c897c6e02 100644 --- a/frontend/src/reactQuery.tsx +++ b/frontend/src/reactQuery.tsx @@ -3,6 +3,14 @@ import axios from "axios"; import { createNotification } from "@app/components/notifications"; +// akhilmhdh: doing individual imports to avoid cyclic import error +import { Button } from "./components/v2/Button"; +import { Modal, ModalContent, ModalTrigger } from "./components/v2/Modal"; +import { Table, TableContainer, TBody, Td, Th, THead, Tr } from "./components/v2/Table"; +import { + formatedConditionsOperatorNames, + PermissionConditionOperators +} from "./context/ProjectPermissionContext/types"; import { ApiErrorTypes, TApiErrors } from "./hooks/api/types"; // this is saved in react-query cache @@ -10,41 +18,161 @@ export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"]; export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"]; export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"]; +const camelCaseToSpaces = (input: string) => { + return input.replace(/([a-z])([A-Z])/g, "$1 $2"); +}; + export const queryClient = new QueryClient({ mutationCache: new MutationCache({ onError: (error) => { if (axios.isAxiosError(error)) { const serverResponse = error.response?.data as TApiErrors; if (serverResponse?.error === ApiErrorTypes.ValidationError) { - createNotification({ - title: "Validation Error", - type: "error", - text: ( -
- {serverResponse.message?.map(({ message, path }) => ( -
-
- Field {path.join(".")} {message.toLowerCase()} -
-
- ))} -
Request ID: {serverResponse.requestId}
-
- ) - }); + createNotification( + { + title: "Validation Error", + type: "error", + text: ( +
+

Please check the input and try again.

+

Request ID: {serverResponse.requestId}

+
+ ), + children: ( + + + + + + + + + + + + + + + {serverResponse.message?.map(({ message, path }) => ( + + + + + ))} + +
FieldIssue
{path.join(".")}{message.toLowerCase()}
+
+
+
+ ) + }, + { closeOnClick: false } + ); return; } + if (serverResponse?.error === ApiErrorTypes.ForbiddenError) { + createNotification( + { + title: "Forbidden Access", + type: "error", - const title = - // eslint-disable-next-line no-nested-ternary - serverResponse.statusCode === 403 - ? "Forbidden Access" - : serverResponse.statusCode === 401 - ? "Unauthorized Access" - : "Bad Request"; + text: `${serverResponse.message} [requestId=${serverResponse.requestId}]`, + children: serverResponse?.details?.length ? ( + + + + + +
+ {serverResponse.details?.map((el, index) => { + const hasConditions = Object.keys(el.conditions || {}).length; + return ( +
+
+ {el.inverted ? "Cannot" : "Can"}{" "} + + {el.action.toString().replaceAll(",", ", ")} + {" "} + {el.subject.toString()} {hasConditions && "with conditions:"} +
+ {hasConditions && ( +
    + {Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => { + const operators = ( + el.conditions as Record< + string, + | string + | { [K in PermissionConditionOperators]: string | string[] } + > + )[field]; + const formattedFieldName = camelCaseToSpaces(field).toLowerCase(); + if (typeof operators === "string") { + return ( +
  • + + {formattedFieldName} + {" "} + equal to{" "} + {operators} +
  • + ); + } + + return Object.keys(operators).map((operator, operatorIndex) => ( +
  • + + {formattedFieldName} + {" "} + + { + formatedConditionsOperatorNames[ + operator as PermissionConditionOperators + ] + } + {" "} + + {operators[ + operator as PermissionConditionOperators + ].toString()} + +
  • + )); + })} +
+ )} +
+ ); + })} +
+
+
+ ) : undefined + }, + { closeOnClick: false } + ); + return; + } createNotification({ - title, + title: "Bad Request", type: "error", text: `${serverResponse.message} [requestId=${serverResponse.requestId}]` }); diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index 0ccfbc466..916244a5a 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -13,6 +13,14 @@ html { @apply rounded-md; } +.Toastify__toast-body { + @apply items-start; +} + +.Toastify__toast-icon { + @apply w-4 pt-1; +} + .rdp-day, .rdp-nav_button { @apply rounded-md hover:text-mineshaft-500; diff --git a/frontend/src/views/Org/AuditLogsPage/components/LogsSection.tsx b/frontend/src/views/Org/AuditLogsPage/components/LogsSection.tsx index dc1fb03ef..968841f3b 100644 --- a/frontend/src/views/Org/AuditLogsPage/components/LogsSection.tsx +++ b/frontend/src/views/Org/AuditLogsPage/components/LogsSection.tsx @@ -98,7 +98,7 @@ export const LogsSection = ({ userAgentType, startDate, endDate, - actorId: actor + actor }} /> , provider: DynamicSecretProviders.Snowflake, title: "Snowflake" + }, + { + icon: , + provider: DynamicSecretProviders.Totp, + title: "TOTP" } ]; @@ -405,6 +411,24 @@ export const CreateDynamicSecretForm = ({ /> )} + {wizardStep === WizardSteps.ProviderInputs && + selectedProvider === DynamicSecretProviders.Totp && ( + + + + )} diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx new file mode 100644 index 000000000..29f4282cc --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx @@ -0,0 +1,314 @@ +import { Controller, useForm } from "react-hook-form"; +import Link from "next/link"; +import { faArrowUpRightFromSquare, faBookOpen } 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, Input, Select, SelectItem } from "@app/components/v2"; +import { useCreateDynamicSecret } from "@app/hooks/api"; +import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; + +enum ConfigType { + URL = "url", + MANUAL = "manual" +} + +enum TotpAlgorithm { + SHA1 = "sha1", + SHA256 = "sha256", + SHA512 = "sha512" +} + +const formSchema = z.object({ + provider: z.discriminatedUnion("configType", [ + z.object({ + configType: z.literal(ConfigType.URL), + url: z + .string() + .url() + .trim() + .min(1) + .refine((val) => { + const urlObj = new URL(val); + const secret = urlObj.searchParams.get("secret"); + + return Boolean(secret); + }, "OTP URL must contain secret field") + }), + z.object({ + configType: z.literal(ConfigType.MANUAL), + secret: z + .string() + .trim() + .min(1) + .transform((val) => val.replace(/\s+/g, "")), + period: z.number().optional(), + algorithm: z.nativeEnum(TotpAlgorithm).optional(), + digits: z.number().optional() + }) + ]), + name: z + .string() + .trim() + .min(1) + .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 TotpInputForm = ({ + onCompleted, + onCancel, + environment, + secretPath, + projectSlug +}: Props) => { + const { + control, + watch, + formState: { isSubmitting }, + handleSubmit + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + provider: { + configType: ConfigType.URL + } + } + }); + + const selectedConfigType = watch("provider.configType"); + + const createDynamicSecret = useCreateDynamicSecret(); + + const handleCreateDynamicSecret = async ({ name, provider }: TForm) => { + // wait till previous request is finished + if (createDynamicSecret.isLoading) return; + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Totp, inputs: provider }, + maxTTL: "24h", + name, + path: secretPath, + defaultTTL: "1m", + projectSlug, + environmentSlug: environment + }); + onCompleted(); + } catch (err) { + createNotification({ + type: "error", + text: err instanceof Error ? err.message : "Failed to create dynamic secret" + }); + } + }; + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+
+
+ Configuration + + +
+ + Docs + +
+
+ +
+
+ ( + + + + )} + /> + {selectedConfigType === ConfigType.URL && ( + ( + + + + )} + /> + )} + {selectedConfigType === ConfigType.MANUAL && ( + <> + ( + + + + )} + /> +
+ ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + + + )} + /> +
+

+ The period, digits, and algorithm values can remain at their defaults unless + your TOTP provider specifies otherwise. +

+ + )} +
+
+
+
+ + +
+
+
+ ); +}; diff --git a/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx index 06abc08e6..09a905b2d 100644 --- a/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -154,7 +154,7 @@ export const CreateSecretForm = ({ isMulti name="tagIds" isDisabled={!canReadTags} - isLoading={isTagsLoading} + isLoading={isTagsLoading && canReadTags} options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))} value={field.value} onChange={field.onChange} diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx index d4ba30158..902d157e8 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx @@ -1,6 +1,6 @@ -import { ReactNode } from "react"; +import { ReactNode, useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; -import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { faCheck, faClock, faCopy } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { AnimatePresence, motion } from "framer-motion"; @@ -9,8 +9,16 @@ import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, IconButton, Input, SecretInput, Tooltip } from "@app/components/v2"; -import { useTimedReset } from "@app/hooks"; +import { + Button, + FormControl, + IconButton, + Input, + SecretInput, + Spinner, + Tooltip +} from "@app/components/v2"; +import { useTimedReset, useToggle } from "@app/hooks"; import { useCreateDynamicSecretLease } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; @@ -54,7 +62,76 @@ const OutputDisplay = ({ ); }; -const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => { +const TotpOutputDisplay = ({ + totp, + remainingSeconds, + triggerLeaseRegeneration +}: { + totp: string; + remainingSeconds: number; + triggerLeaseRegeneration: (details: { ttl?: string }) => Promise; +}) => { + const [remainingTime, setRemainingTime] = useState(remainingSeconds); + const [shouldShowRegenerate, setShouldShowRegenerate] = useToggle(false); + + useEffect(() => { + setRemainingTime(remainingSeconds); + setShouldShowRegenerate.off(); + + // Set up countdown interval + const intervalId = setInterval(() => { + setRemainingTime((prevTime) => { + if (prevTime <= 1) { + clearInterval(intervalId); + setShouldShowRegenerate.on(); + return 0; + } + return prevTime - 1; + }); + }, 1000); + + // Cleanup interval on unmount or when totp changes + return () => clearInterval(intervalId); + }, [totp, remainingSeconds]); + + return ( +
+ + {remainingTime > 0 ? ( +
+ + + Expires in {remainingTime} {remainingTime > 1 ? "seconds" : "second"} + +
+ ) : ( +
+ + Expired +
+ )} + {shouldShowRegenerate && ( + + )} +
+ ); +}; + +const renderOutputForm = ( + provider: DynamicSecretProviders, + data: unknown, + triggerLeaseRegeneration: (details: { ttl?: string }) => Promise +) => { if ( provider === DynamicSecretProviders.SqlDatabase || provider === DynamicSecretProviders.Cassandra || @@ -242,11 +319,29 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => { ); } + if (provider === DynamicSecretProviders.Totp) { + const { TOTP, TIME_REMAINING } = data as { + TOTP: string; + TIME_REMAINING: number; + }; + + return ( + + ); + } + return null; }; const formSchema = z.object({ - ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number") + ttl: z + .string() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .optional() }); type TForm = z.infer; @@ -259,6 +354,8 @@ type Props = { secretPath: string; }; +const PROVIDERS_WITH_AUTOGENERATE_SUPPORT = [DynamicSecretProviders.Totp]; + export const CreateDynamicSecretLease = ({ onClose, projectSlug, @@ -277,6 +374,9 @@ export const CreateDynamicSecretLease = ({ ttl: "1h" } }); + const [isPreloading, setIsPreloading] = useToggle( + PROVIDERS_WITH_AUTOGENERATE_SUPPORT.includes(provider) + ); const createDynamicSecretLease = useCreateDynamicSecretLease(); @@ -290,10 +390,13 @@ export const CreateDynamicSecretLease = ({ ttl, dynamicSecretName }); + createNotification({ type: "success", text: "Successfully leased dynamic secret" }); + + setIsPreloading.off(); } catch (error) { console.log(error); createNotification({ @@ -303,8 +406,23 @@ export const CreateDynamicSecretLease = ({ } }; + const handleLeaseRegeneration = async (data: { ttl?: string }) => { + setIsPreloading.on(); + handleDynamicSecretLeaseCreate(data); + }; + + useEffect(() => { + if (provider === DynamicSecretProviders.Totp) { + handleDynamicSecretLeaseCreate({}); + } + }, [provider]); + const isOutputMode = Boolean(createDynamicSecretLease?.data); + if (isPreloading) { + return ; + } + return (
@@ -350,7 +468,11 @@ export const CreateDynamicSecretLease = ({ animate={{ opacity: 1, translateX: 0 }} exit={{ opacity: 0, translateX: 30 }} > - {renderOutputForm(provider, createDynamicSecretLease.data?.data)} + {renderOutputForm( + provider, + createDynamicSecretLease.data?.data, + handleLeaseRegeneration + )} )} diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx index 8953ab2bc..22057a90c 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx @@ -101,10 +101,20 @@ export const DynamicSecretListView = ({ role="button" tabIndex={0} onKeyDown={(evt) => { + // no lease view for TOTP because it's irrelevant + if (secret.type === DynamicSecretProviders.Totp) { + return; + } + if (evt.key === "Enter" && !isRevoking) handlePopUpOpen("dynamicSecretLeases", secret.id); }} onClick={() => { + // no lease view for TOTP because it's irrelevant + if (secret.type === DynamicSecretProviders.Totp) { + return; + } + if (!isRevoking) { handlePopUpOpen("dynamicSecretLeases", secret.id); } diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx index 7c37ed7b2..d260b9f40 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx @@ -17,6 +17,7 @@ import { EditDynamicSecretRedisProviderForm } from "./EditDynamicSecretRedisProv import { EditDynamicSecretSapHanaForm } from "./EditDynamicSecretSapHanaForm"; import { EditDynamicSecretSnowflakeForm } from "./EditDynamicSecretSnowflakeForm"; import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm"; +import { EditDynamicSecretTotpForm } from "./EditDynamicSecretTotpForm"; type Props = { onClose: () => void; @@ -276,6 +277,23 @@ export const EditDynamicSecretForm = ({ /> )} + {dynamicSecretDetails?.type === DynamicSecretProviders.Totp && ( + + + + )} ); }; diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretTotpForm.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretTotpForm.tsx new file mode 100644 index 000000000..1e2e01c82 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretTotpForm.tsx @@ -0,0 +1,318 @@ +import { Controller, useForm } from "react-hook-form"; +import Link from "next/link"; +import { faArrowUpRightFromSquare, faBookOpen } 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, Input, Select, SelectItem } from "@app/components/v2"; +import { useUpdateDynamicSecret } from "@app/hooks/api"; +import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; + +enum ConfigType { + URL = "url", + MANUAL = "manual" +} + +enum TotpAlgorithm { + SHA1 = "sha1", + SHA256 = "sha256", + SHA512 = "sha512" +} + +const formSchema = z.object({ + inputs: z + .discriminatedUnion("configType", [ + z.object({ + configType: z.literal(ConfigType.URL), + url: z + .string() + .url() + .trim() + .min(1) + .refine((val) => { + const urlObj = new URL(val); + const secret = urlObj.searchParams.get("secret"); + + return Boolean(secret); + }, "OTP URL must contain secret field") + }), + z.object({ + configType: z.literal(ConfigType.MANUAL), + secret: z + .string() + .trim() + .min(1) + .transform((val) => val.replace(/\s+/g, "")), + period: z.number().optional(), + algorithm: z.nativeEnum(TotpAlgorithm).optional(), + digits: z.number().optional() + }) + ]) + .optional(), + newName: z + .string() + .trim() + .min(1) + .refine((val) => val.toLowerCase() === val, "Must be lowercase") +}); +type TForm = z.infer; + +type Props = { + onClose: () => void; + dynamicSecret: TDynamicSecret & { inputs: unknown }; + secretPath: string; + projectSlug: string; + environment: string; +}; + +export const EditDynamicSecretTotpForm = ({ + onClose, + dynamicSecret, + environment, + secretPath, + projectSlug +}: Props) => { + const { + control, + formState: { isSubmitting }, + watch, + handleSubmit + } = useForm({ + resolver: zodResolver(formSchema), + values: { + newName: dynamicSecret.name, + inputs: dynamicSecret.inputs as TForm["inputs"] + } + }); + + const selectedConfigType = watch("inputs.configType"); + const updateDynamicSecret = useUpdateDynamicSecret(); + + const handleUpdateDynamicSecret = async ({ inputs, 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: { + inputs, + newName: newName === dynamicSecret.name ? undefined : newName + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); + } catch (err) { + createNotification({ + type: "error", + text: err instanceof Error ? err.message : "Failed to update dynamic secret" + }); + } + }; + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+
+
+ Configuration + + +
+ + Docs + +
+
+ +
+
+ ( + + + + )} + /> + {selectedConfigType === ConfigType.URL && ( + ( + + + + )} + /> + )} + {selectedConfigType === ConfigType.MANUAL && ( + <> + ( + + + + )} + /> +
+ ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + + + )} + /> +
+

+ The period, digits, and algorithm values can remain at their defaults unless + your TOTP provider specifies otherwise. +

+ + )} +
+
+
+
+ + +
+
+
+ ); +}; diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx index ec5b064c1..be9ee77ad 100644 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx @@ -867,7 +867,7 @@ export const SecretOverviewPage = () => {
setScrollOffset(e.currentTarget.scrollLeft)} - className="thin-scrollbar" + className="thin-scrollbar rounded-b-none" > diff --git a/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx index 0fd953a1f..91a6e766f 100644 --- a/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -255,7 +255,7 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }: isMulti name="tagIds" isDisabled={!canReadTags} - isLoading={isTagsLoading} + isLoading={isTagsLoading && canReadTags} options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))} value={field.value} onChange={field.onChange} diff --git a/npm/README.md b/npm/README.md index b64b02ccd..febd52eec 100644 --- a/npm/README.md +++ b/npm/README.md @@ -1,10 +1,11 @@ -

Infisical

+

Infisical CLI

-

The open-source secret management platform: Sync secrets/configs across your team/infrastructure and prevent secret leaks.

+

Embrace shift-left security with the Infisical CLI and strengthen your DevSecOps practices by seamlessly managing secrets across your workflows, pipelines, and applications.

Slack | + Node.js SDK | Infisical Cloud | Self-Hosting | Docs | @@ -12,7 +13,6 @@ Hiring (Remote/SF)

-

Infisical is released under the MIT license. @@ -36,10 +36,7 @@ ### Introduction -**[Infisical](https://infisical.com)** is the open source secret management platform that teams use to centralize their application configuration and secrets like API keys and database credentials as well as manage their internal PKI. - -We're on a mission to make security tooling more accessible to everyone, not just security teams, and that means redesigning the entire developer experience from ground up. - +The Infisical CLI is a powerful command line tool that can be used to retrieve, modify, export and inject secrets into any process or application as environment variables. You can use it across various environments, whether it’s local development, CI/CD, staging, or production. ### Installation