mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-02 16:25:52 +00:00
Merge branch 'heads/main' into daniel/request-ids
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
. "$(dirname -- "$0")/_/husky.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
|
npx lint-staged
|
||||||
|
|
||||||
infisical scan git-changes --staged -v
|
infisical scan git-changes --staged -v
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.IdentityMetadata, "value")) {
|
||||||
|
await knex.schema.alterTable(TableName.IdentityMetadata, (t) => {
|
||||||
|
t.string("value", 1020).alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { RabbitMqProvider } from "./rabbit-mq";
|
|||||||
import { RedisDatabaseProvider } from "./redis";
|
import { RedisDatabaseProvider } from "./redis";
|
||||||
import { SapHanaProvider } from "./sap-hana";
|
import { SapHanaProvider } from "./sap-hana";
|
||||||
import { SqlDatabaseProvider } from "./sql-database";
|
import { SqlDatabaseProvider } from "./sql-database";
|
||||||
|
import { TotpProvider } from "./totp";
|
||||||
|
|
||||||
export const buildDynamicSecretProviders = () => ({
|
export const buildDynamicSecretProviders = () => ({
|
||||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
||||||
@@ -27,5 +28,6 @@ export const buildDynamicSecretProviders = () => ({
|
|||||||
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
|
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
|
||||||
[DynamicSecretProviders.Ldap]: LdapProvider(),
|
[DynamicSecretProviders.Ldap]: LdapProvider(),
|
||||||
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
|
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
|
||||||
[DynamicSecretProviders.Snowflake]: SnowflakeProvider()
|
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
|
||||||
|
[DynamicSecretProviders.Totp]: TotpProvider()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,17 @@ export enum LdapCredentialType {
|
|||||||
Static = "static"
|
Static = "static"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum TotpConfigType {
|
||||||
|
URL = "url",
|
||||||
|
MANUAL = "manual"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TotpAlgorithm {
|
||||||
|
SHA1 = "sha1",
|
||||||
|
SHA256 = "sha256",
|
||||||
|
SHA512 = "sha512"
|
||||||
|
}
|
||||||
|
|
||||||
export const DynamicSecretRedisDBSchema = z.object({
|
export const DynamicSecretRedisDBSchema = z.object({
|
||||||
host: z.string().trim().toLowerCase(),
|
host: z.string().trim().toLowerCase(),
|
||||||
port: z.number(),
|
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 {
|
export enum DynamicSecretProviders {
|
||||||
SqlDatabase = "sql-database",
|
SqlDatabase = "sql-database",
|
||||||
Cassandra = "cassandra",
|
Cassandra = "cassandra",
|
||||||
@@ -234,7 +273,8 @@ export enum DynamicSecretProviders {
|
|||||||
AzureEntraID = "azure-entra-id",
|
AzureEntraID = "azure-entra-id",
|
||||||
Ldap = "ldap",
|
Ldap = "ldap",
|
||||||
SapHana = "sap-hana",
|
SapHana = "sap-hana",
|
||||||
Snowflake = "snowflake"
|
Snowflake = "snowflake",
|
||||||
|
Totp = "totp"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
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.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }),
|
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 = {
|
export type TDynamicProviderFns = {
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -127,14 +127,15 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const getProjectPermission = async (userId: string, projectId: string) => {
|
const getProjectPermission = async (userId: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
|
const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId");
|
||||||
const docs = await db
|
const docs = await db
|
||||||
.replicaNode()(TableName.Users)
|
.replicaNode()(TableName.Users)
|
||||||
.where(`${TableName.Users}.id`, userId)
|
.where(`${TableName.Users}.id`, userId)
|
||||||
.leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
|
||||||
.leftJoin(TableName.GroupProjectMembership, (queryBuilder) => {
|
.leftJoin(TableName.GroupProjectMembership, (queryBuilder) => {
|
||||||
void queryBuilder
|
void queryBuilder
|
||||||
.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId]))
|
.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(
|
.leftJoin(
|
||||||
TableName.GroupProjectMembershipRole,
|
TableName.GroupProjectMembershipRole,
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export enum PermissionConditionOperators {
|
import { PermissionConditionOperators } from "@app/lib/casl";
|
||||||
$IN = "$in",
|
|
||||||
$ALL = "$all",
|
|
||||||
$REGEX = "$regex",
|
|
||||||
$EQ = "$eq",
|
|
||||||
$NEQ = "$ne",
|
|
||||||
$GLOB = "$glob"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PermissionConditionSchema = {
|
export const PermissionConditionSchema = {
|
||||||
[PermissionConditionOperators.$IN]: z.string().trim().min(1).array(),
|
[PermissionConditionOperators.$IN]: z.string().trim().min(1).array(),
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
||||||
import { z } from "zod";
|
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 { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
||||||
|
|
||||||
import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types";
|
import { PermissionConditionSchema } from "./permission-types";
|
||||||
|
|
||||||
export enum ProjectPermissionActions {
|
export enum ProjectPermissionActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
|
|||||||
@@ -54,3 +54,12 @@ export const isAtLeastAsPrivileged = (permissions1: MongoAbility, permissions2:
|
|||||||
|
|
||||||
return set1.size >= set2.size;
|
return set1.size >= set2.size;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum PermissionConditionOperators {
|
||||||
|
$IN = "$in",
|
||||||
|
$ALL = "$all",
|
||||||
|
$REGEX = "$regex",
|
||||||
|
$EQ = "$eq",
|
||||||
|
$NEQ = "$ne",
|
||||||
|
$GLOB = "$glob"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError, PureAbility } from "@casl/ability";
|
||||||
import fastifyPlugin from "fastify-plugin";
|
import fastifyPlugin from "fastify-plugin";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
@@ -77,7 +77,13 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
requestId: req.id,
|
requestId: req.id,
|
||||||
statusCode: HttpStatusCodes.Forbidden,
|
statusCode: HttpStatusCodes.Forbidden,
|
||||||
error: "PermissionDenied",
|
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) {
|
} else if (error instanceof ForbiddenRequestError) {
|
||||||
void res.status(HttpStatusCodes.Forbidden).send({
|
void res.status(HttpStatusCodes.Forbidden).send({
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export const DefaultResponseErrorsSchema = {
|
|||||||
requestId: z.string(),
|
requestId: z.string(),
|
||||||
statusCode: z.literal(403),
|
statusCode: z.literal(403),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
details: z.any().optional(),
|
||||||
error: z.string()
|
error: z.string()
|
||||||
}),
|
}),
|
||||||
500: z.object({
|
500: z.object({
|
||||||
|
|||||||
@@ -385,8 +385,8 @@ export const identityTokenAuthServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
}: TUpdateTokenAuthTokenDTO) => {
|
}: TUpdateTokenAuthTokenDTO) => {
|
||||||
const foundToken = await identityAccessTokenDAL.findOne({
|
const foundToken = await identityAccessTokenDAL.findOne({
|
||||||
id: tokenId,
|
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
|
||||||
authMethod: IdentityAuthMethod.TOKEN_AUTH
|
[`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH
|
||||||
});
|
});
|
||||||
if (!foundToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` });
|
if (!foundToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` });
|
||||||
|
|
||||||
@@ -444,8 +444,8 @@ export const identityTokenAuthServiceFactory = ({
|
|||||||
}: TRevokeTokenAuthTokenDTO) => {
|
}: TRevokeTokenAuthTokenDTO) => {
|
||||||
const identityAccessToken = await identityAccessTokenDAL.findOne({
|
const identityAccessToken = await identityAccessTokenDAL.findOne({
|
||||||
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
|
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
|
||||||
isAccessTokenRevoked: false,
|
[`${TableName.IdentityAccessToken}.isAccessTokenRevoked` as "isAccessTokenRevoked"]: false,
|
||||||
authMethod: IdentityAuthMethod.TOKEN_AUTH
|
[`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH
|
||||||
});
|
});
|
||||||
if (!identityAccessToken)
|
if (!identityAccessToken)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
|
|||||||
@@ -3075,7 +3075,7 @@ const syncSecretsTerraformCloud = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
// get secrets from Terraform Cloud
|
// get secrets from Terraform Cloud
|
||||||
const terraformSecrets = (
|
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`,
|
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
@@ -3089,7 +3089,7 @@ const syncSecretsTerraformCloud = async ({
|
|||||||
...obj,
|
...obj,
|
||||||
[secret.attributes.key]: secret
|
[secret.attributes.key]: secret
|
||||||
}),
|
}),
|
||||||
{} as Record<string, { attributes: { key: string; value: string }; id: string }>
|
{} as Record<string, { attributes: { key: string; value: string; sensitive: boolean }; id: string }>
|
||||||
);
|
);
|
||||||
|
|
||||||
const secretsToAdd: { [key: string]: string } = {};
|
const secretsToAdd: { [key: string]: string } = {};
|
||||||
@@ -3170,7 +3170,8 @@ const syncSecretsTerraformCloud = async ({
|
|||||||
attributes: {
|
attributes: {
|
||||||
key,
|
key,
|
||||||
value: secrets[key]?.value,
|
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
|
// 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
|
// -> update secret
|
||||||
await request.patch(
|
await request.patch(
|
||||||
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${terraformSecrets[key].id}`,
|
`${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,
|
id: terraformSecrets[key].id,
|
||||||
attributes: {
|
attributes: {
|
||||||
...terraformSecrets[key],
|
...terraformSecrets[key],
|
||||||
value: secrets[key]?.value
|
value: secrets[key]?.value,
|
||||||
|
sensitive: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: 'Install'
|
|||||||
description: "Infisical's CLI is one of the best way to manage environments and secrets. Install it here"
|
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.
|
You can use it across various environments, whether it's local development, CI/CD, staging, or production.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Default TTL" type="string" required>
|
<ParamField path="Default TTL" type="string" required>
|
||||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Max TTL" type="string" required>
|
<ParamField path="Max TTL" type="string" required>
|
||||||
@@ -131,12 +131,12 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
|
|||||||
|
|
||||||
## Audit or Revoke Leases
|
## 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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Renew Leases
|
## 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.
|
||||||

|

|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Default TTL" type="string" required>
|
<ParamField path="Default TTL" type="string" required>
|
||||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Max TTL" type="string" required>
|
<ParamField path="Max TTL" type="string" required>
|
||||||
@@ -138,12 +138,12 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
|
|||||||
|
|
||||||
## Audit or Revoke Leases
|
## 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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Renew Leases
|
## 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.
|
||||||

|

|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ Click on Add assignments. Search for the application name you created and select
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Default TTL" type="string" required>
|
<ParamField path="Default TTL" type="string" required>
|
||||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Max TTL" type="string" required>
|
<ParamField path="Max TTL" type="string" required>
|
||||||
@@ -151,12 +151,12 @@ Click on Add assignments. Search for the application name you created and select
|
|||||||
|
|
||||||
## Audit or Revoke Leases
|
## 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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Renew Leases
|
## 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.
|
||||||

|

|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ The above configuration allows user creation and granting permissions.
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Default TTL" type="string" required>
|
<ParamField path="Default TTL" type="string" required>
|
||||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Max TTL" type="string" required>
|
<ParamField path="Max TTL" type="string" required>
|
||||||
@@ -116,12 +116,12 @@ The above configuration allows user creation and granting permissions.
|
|||||||
|
|
||||||
## Audit or Revoke Leases
|
## 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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Renew Leases
|
## 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.
|
||||||

|

|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Default TTL" type="string" required>
|
<ParamField path="Default TTL" type="string" required>
|
||||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Max TTL" type="string" required>
|
<ParamField path="Max TTL" type="string" required>
|
||||||
@@ -114,12 +114,12 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
|
|||||||
|
|
||||||
## Audit or Revoke Leases
|
## 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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Renew Leases
|
## 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.
|
||||||

|

|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Default TTL" type="string" required>
|
<ParamField path="Default TTL" type="string" required>
|
||||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Max TTL" type="string" required>
|
<ParamField path="Max TTL" type="string" required>
|
||||||
@@ -171,7 +171,7 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Default TTL" type="string" required>
|
<ParamField path="Default TTL" type="string" required>
|
||||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Max TTL" type="string" required>
|
<ParamField path="Max TTL" type="string" required>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Create a project scopped API Key with the required permission in your Mongo Atla
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Default TTL" type="string" required>
|
<ParamField path="Default TTL" type="string" required>
|
||||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Max TTL" type="string" required>
|
<ParamField path="Max TTL" type="string" required>
|
||||||
@@ -101,12 +101,12 @@ Create a project scopped API Key with the required permission in your Mongo Atla
|
|||||||
|
|
||||||
## Audit or Revoke Leases
|
## 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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Renew Leases
|
## 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.
|
||||||

|

|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Create a user with the required permission in your MongoDB instance. This user w
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Default TTL" type="string" required>
|
<ParamField path="Default TTL" type="string" required>
|
||||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Max TTL" type="string" required>
|
<ParamField path="Max TTL" type="string" required>
|
||||||
@@ -103,12 +103,12 @@ Create a user with the required permission in your MongoDB instance. This user w
|
|||||||
|
|
||||||
## Audit or Revoke Leases
|
## 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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Renew Leases
|
## 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.
|
||||||

|

|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Default TTL" type="string" required>
|
<ParamField path="Default TTL" type="string" required>
|
||||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Max TTL" type="string" required>
|
<ParamField path="Max TTL" type="string" required>
|
||||||
@@ -105,12 +105,12 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
|
|
||||||
## Audit or Revoke Leases
|
## 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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Renew Leases
|
## 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.
|
||||||

|

|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Default TTL" type="string" required>
|
<ParamField path="Default TTL" type="string" required>
|
||||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Max TTL" type="string" required>
|
<ParamField path="Max TTL" type="string" required>
|
||||||
@@ -102,12 +102,12 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
|
|
||||||
## Audit or Revoke Leases
|
## 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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Renew Leases
|
## 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.
|
||||||

|

|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Default TTL" type="string" required>
|
<ParamField path="Default TTL" type="string" required>
|
||||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Max TTL" type="string" required>
|
<ParamField path="Max TTL" type="string" required>
|
||||||
@@ -102,12 +102,12 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
|
|
||||||
## Audit or Revoke Leases
|
## 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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Renew Leases
|
## 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.
|
||||||

|

|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Default TTL" type="string" required>
|
<ParamField path="Default TTL" type="string" required>
|
||||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Max TTL" type="string" required>
|
<ParamField path="Max TTL" type="string" required>
|
||||||
@@ -105,12 +105,12 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
|
|
||||||
## Audit or Revoke Leases
|
## 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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Renew Leases
|
## 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.
|
||||||

|

|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credential
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Default TTL" type="string" required>
|
<ParamField path="Default TTL" type="string" required>
|
||||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Max TTL" type="string" required>
|
<ParamField path="Max TTL" type="string" required>
|
||||||
@@ -103,12 +103,12 @@ The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credential
|
|||||||
|
|
||||||
## Audit or Revoke Leases
|
## 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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Renew Leases
|
## 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.
|
||||||

|

|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Create a user with the required permission in your Redis instance. This user wil
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Default TTL" type="string" required>
|
<ParamField path="Default TTL" type="string" required>
|
||||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Max TTL" type="string" required>
|
<ParamField path="Max TTL" type="string" required>
|
||||||
@@ -93,12 +93,12 @@ Create a user with the required permission in your Redis instance. This user wil
|
|||||||
|
|
||||||
## Audit or Revoke Leases
|
## 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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Renew Leases
|
## 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.
|
||||||

|

|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database c
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Default TTL" type="string" required>
|
<ParamField path="Default TTL" type="string" required>
|
||||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Max TTL" type="string" required>
|
<ParamField path="Max TTL" type="string" required>
|
||||||
@@ -106,13 +106,13 @@ The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database c
|
|||||||
## Audit or Revoke Leases
|
## 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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Renew Leases
|
## 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.
|
||||||

|

|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ Infisical's Snowflake dynamic secrets allow you to generate Snowflake user crede
|
|||||||
## Audit or Revoke Leases
|
## 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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Open Secret Overview Dashboard">
|
||||||
|
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
|
||||||
|
</Step>
|
||||||
|
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Select TOTP">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Provide the inputs for dynamic secret parameters">
|
||||||
|
<ParamField path="Secret Name" type="string" required>
|
||||||
|
Name by which you want the secret to be referenced
|
||||||
|
</ParamField>
|
||||||
|
<ParamField path="Configuration Type" type="string" required>
|
||||||
|
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.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField path="URL" type="string">
|
||||||
|
OTP URL in `otpauth://` format used to generate TOTP codes.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField path="Secret Key" type="string">
|
||||||
|
Base32 encoded secret used to generate TOTP codes.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField path="Period" type="number">
|
||||||
|
Time interval in seconds between generating new TOTP codes.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField path="Digits" type="number">
|
||||||
|
Number of digits to generate in each TOTP code.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField path="Algorithm" type="string">
|
||||||
|
Hash algorithm to use when generating TOTP codes. The supported algorithms are sha1, sha256, and sha512.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Click 'Submit'">
|
||||||
|
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Generate dynamic secrets">
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Once you click the `Generate` button, a new secret lease will be generated and the TOTP will be shown to you.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 487 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 464 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 408 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 400 KiB |
@@ -1,8 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: "Terraform"
|
title: "Terraform Provider"
|
||||||
description: "Learn how to fetch Secrets From Infisical With Terraform."
|
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.
|
This guide provides step-by-step guidance on how to fetch secrets from Infisical using Terraform.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
@@ -98,4 +99,4 @@ Terraform will now fetch your secrets from Infisical and display them as output
|
|||||||
|
|
||||||
## Conclusion
|
## 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). */}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ spec:
|
|||||||
projectSlug: new-ob-em
|
projectSlug: new-ob-em
|
||||||
envSlug: dev # "dev", "staging", "prod", etc..
|
envSlug: dev # "dev", "staging", "prod", etc..
|
||||||
secretsPath: "/" # Root is "/"
|
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:
|
credentialsRef:
|
||||||
secretName: universal-auth-credentials
|
secretName: universal-auth-credentials
|
||||||
secretNamespace: default
|
secretNamespace: default
|
||||||
|
|||||||
+2
-1
@@ -189,7 +189,8 @@
|
|||||||
"documentation/platform/dynamic-secrets/azure-entra-id",
|
"documentation/platform/dynamic-secrets/azure-entra-id",
|
||||||
"documentation/platform/dynamic-secrets/ldap",
|
"documentation/platform/dynamic-secrets/ldap",
|
||||||
"documentation/platform/dynamic-secrets/sap-hana",
|
"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",
|
"documentation/platform/project-templates",
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ import { Id, toast, ToastContainer, ToastOptions, TypeOptions } from "react-toas
|
|||||||
export type TNotification = {
|
export type TNotification = {
|
||||||
title?: string;
|
title?: string;
|
||||||
text: ReactNode;
|
text: ReactNode;
|
||||||
|
children?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NotificationContent = ({ title, text }: TNotification) => {
|
export const NotificationContent = ({ title, text, children }: TNotification) => {
|
||||||
return (
|
return (
|
||||||
<div className="msg-container">
|
<div className="msg-container">
|
||||||
{title && <div className="text-md mb-1 font-medium">{title}</div>}
|
{title && <div className="text-md mb-1 font-medium">{title}</div>}
|
||||||
<div className={title ? "text-sm" : "text-md"}>{text}</div>
|
<div className={title ? "text-sm text-neutral-400" : "text-md"}>{text}</div>
|
||||||
|
{children && <div className="mt-2">{children}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -23,7 +25,13 @@ export const createNotification = (
|
|||||||
position: "bottom-right",
|
position: "bottom-right",
|
||||||
...toastProps,
|
...toastProps,
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
type: myProps?.type || "info",
|
type: myProps?.type || "info"
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NotificationContainer = () => <ToastContainer pauseOnHover toastClassName="border border-mineshaft-500" style={{ width: "400px" }} />;
|
export const NotificationContainer = () => (
|
||||||
|
<ToastContainer
|
||||||
|
pauseOnHover
|
||||||
|
toastClassName="border border-mineshaft-500"
|
||||||
|
style={{ width: "400px" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ export enum PermissionConditionOperators {
|
|||||||
$GLOB = "$glob"
|
$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 = {
|
export type TPermissionConditionOperators = {
|
||||||
[PermissionConditionOperators.$IN]: string[];
|
[PermissionConditionOperators.$IN]: string[];
|
||||||
[PermissionConditionOperators.$ALL]: string[];
|
[PermissionConditionOperators.$ALL]: string[];
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type TGetAuditLogsFilter = {
|
|||||||
eventMetadata?: Record<string, string>;
|
eventMetadata?: Record<string, string>;
|
||||||
actorType?: ActorType;
|
actorType?: ActorType;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
actorId?: string; // user ID format
|
actor?: string; // user ID format
|
||||||
startDate?: Date;
|
startDate?: Date;
|
||||||
endDate?: Date;
|
endDate?: Date;
|
||||||
limit: number;
|
limit: number;
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ export enum DynamicSecretProviders {
|
|||||||
AzureEntraId = "azure-entra-id",
|
AzureEntraId = "azure-entra-id",
|
||||||
Ldap = "ldap",
|
Ldap = "ldap",
|
||||||
SapHana = "sap-hana",
|
SapHana = "sap-hana",
|
||||||
Snowflake = "snowflake"
|
Snowflake = "snowflake",
|
||||||
|
Totp = "totp"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SqlProviders {
|
export enum SqlProviders {
|
||||||
@@ -230,6 +231,21 @@ export type TDynamicSecretProvider =
|
|||||||
revocationStatement: string;
|
revocationStatement: string;
|
||||||
renewStatement?: string;
|
renewStatement?: string;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: DynamicSecretProviders.Totp;
|
||||||
|
inputs:
|
||||||
|
| {
|
||||||
|
configType: "url";
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
configType: "manual";
|
||||||
|
secret: string;
|
||||||
|
period?: number;
|
||||||
|
algorithm?: string;
|
||||||
|
digits?: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
export type TCreateDynamicSecretDTO = {
|
export type TCreateDynamicSecretDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PureAbility } from "@casl/ability";
|
||||||
import { ZodIssue } from "zod";
|
import { ZodIssue } from "zod";
|
||||||
|
|
||||||
export type { TAccessApprovalPolicy } from "./accessApproval/types";
|
export type { TAccessApprovalPolicy } from "./accessApproval/types";
|
||||||
@@ -53,10 +54,21 @@ export type TApiErrors =
|
|||||||
requestId: string;
|
requestId: string;
|
||||||
error: ApiErrorTypes.ValidationError;
|
error: ApiErrorTypes.ValidationError;
|
||||||
message: ZodIssue[];
|
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;
|
statusCode: 403;
|
||||||
}
|
}
|
||||||
| { requestId: string; error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 403 }
|
|
||||||
| { requestId: string; error: ApiErrorTypes.UnauthorizedError; message: string; statusCode: 401 }
|
|
||||||
| {
|
| {
|
||||||
requestId: string;
|
requestId: string;
|
||||||
statusCode: 400;
|
statusCode: 400;
|
||||||
|
|||||||
+152
-24
@@ -3,6 +3,14 @@ import axios from "axios";
|
|||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
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";
|
import { ApiErrorTypes, TApiErrors } from "./hooks/api/types";
|
||||||
|
|
||||||
// this is saved in react-query cache
|
// 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 MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"];
|
||||||
export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-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({
|
export const queryClient = new QueryClient({
|
||||||
mutationCache: new MutationCache({
|
mutationCache: new MutationCache({
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
const serverResponse = error.response?.data as TApiErrors;
|
const serverResponse = error.response?.data as TApiErrors;
|
||||||
if (serverResponse?.error === ApiErrorTypes.ValidationError) {
|
if (serverResponse?.error === ApiErrorTypes.ValidationError) {
|
||||||
createNotification({
|
createNotification(
|
||||||
title: "Validation Error",
|
{
|
||||||
type: "error",
|
title: "Validation Error",
|
||||||
text: (
|
type: "error",
|
||||||
<div>
|
text: (
|
||||||
{serverResponse.message?.map(({ message, path }) => (
|
<div>
|
||||||
<div className="flex space-y-2" key={path.join(".")}>
|
<p>Please check the input and try again.</p>
|
||||||
<div>
|
<p className="mt-2 text-xs">Request ID: {serverResponse.requestId}</p>
|
||||||
Field <i>{path.join(".")}</i> {message.toLowerCase()}
|
</div>
|
||||||
</div>
|
),
|
||||||
</div>
|
children: (
|
||||||
))}
|
<Modal>
|
||||||
<div className="mt-2">Request ID: {serverResponse.requestId}</div>
|
<ModalTrigger>
|
||||||
</div>
|
<Button variant="outline_bg" size="xs">
|
||||||
)
|
Show more
|
||||||
});
|
</Button>
|
||||||
|
</ModalTrigger>
|
||||||
|
<ModalContent title="Validation Error Details">
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Field</Th>
|
||||||
|
<Th>Issue</Th>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{serverResponse.message?.map(({ message, path }) => (
|
||||||
|
<Tr key={path.join(".")}>
|
||||||
|
<Td>{path.join(".")}</Td>
|
||||||
|
<Td>{message.toLowerCase()}</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ closeOnClick: false }
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (serverResponse?.error === ApiErrorTypes.ForbiddenError) {
|
||||||
|
createNotification(
|
||||||
|
{
|
||||||
|
title: "Forbidden Access",
|
||||||
|
type: "error",
|
||||||
|
|
||||||
const title =
|
text: `${serverResponse.message} [requestId=${serverResponse.requestId}]`,
|
||||||
// eslint-disable-next-line no-nested-ternary
|
children: serverResponse?.details?.length ? (
|
||||||
serverResponse.statusCode === 403
|
<Modal>
|
||||||
? "Forbidden Access"
|
<ModalTrigger>
|
||||||
: serverResponse.statusCode === 401
|
<Button variant="outline_bg" size="xs">
|
||||||
? "Unauthorized Access"
|
Show more
|
||||||
: "Bad Request";
|
</Button>
|
||||||
|
</ModalTrigger>
|
||||||
|
<ModalContent
|
||||||
|
title="Validation Rules"
|
||||||
|
subTitle="Please review the allowed rules below."
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{serverResponse.details?.map((el, index) => {
|
||||||
|
const hasConditions = Object.keys(el.conditions || {}).length;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`Forbidden-error-details-${index + 1}`}
|
||||||
|
className="rounded-md border border-gray-600 p-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{el.inverted ? "Cannot" : "Can"}{" "}
|
||||||
|
<span className="text-yellow-600">
|
||||||
|
{el.action.toString().replaceAll(",", ", ")}
|
||||||
|
</span>{" "}
|
||||||
|
{el.subject.toString()} {hasConditions && "with conditions:"}
|
||||||
|
</div>
|
||||||
|
{hasConditions && (
|
||||||
|
<ul className="flex list-disc flex-col gap-1 pl-5 pt-2 text-sm">
|
||||||
|
{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 (
|
||||||
|
<li
|
||||||
|
key={`Forbidden-error-details-${index + 1}-${
|
||||||
|
fieldIndex + 1
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="font-bold capitalize">
|
||||||
|
{formattedFieldName}
|
||||||
|
</span>{" "}
|
||||||
|
<span className="text-mineshaft-200">equal to</span>{" "}
|
||||||
|
<span className="text-yellow-600">{operators}</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(operators).map((operator, operatorIndex) => (
|
||||||
|
<li
|
||||||
|
key={`Forbidden-error-details-${index + 1}-${
|
||||||
|
fieldIndex + 1
|
||||||
|
}-${operatorIndex + 1}`}
|
||||||
|
>
|
||||||
|
<span className="font-bold capitalize">
|
||||||
|
{formattedFieldName}
|
||||||
|
</span>{" "}
|
||||||
|
<span className="text-mineshaft-200">
|
||||||
|
{
|
||||||
|
formatedConditionsOperatorNames[
|
||||||
|
operator as PermissionConditionOperators
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</span>{" "}
|
||||||
|
<span className="text-yellow-600">
|
||||||
|
{operators[
|
||||||
|
operator as PermissionConditionOperators
|
||||||
|
].toString()}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
));
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
) : undefined
|
||||||
|
},
|
||||||
|
{ closeOnClick: false }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
createNotification({
|
createNotification({
|
||||||
title,
|
title: "Bad Request",
|
||||||
type: "error",
|
type: "error",
|
||||||
text: `${serverResponse.message} [requestId=${serverResponse.requestId}]`
|
text: `${serverResponse.message} [requestId=${serverResponse.requestId}]`
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ html {
|
|||||||
@apply rounded-md;
|
@apply rounded-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Toastify__toast-body {
|
||||||
|
@apply items-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Toastify__toast-icon {
|
||||||
|
@apply w-4 pt-1;
|
||||||
|
}
|
||||||
|
|
||||||
.rdp-day,
|
.rdp-day,
|
||||||
.rdp-nav_button {
|
.rdp-nav_button {
|
||||||
@apply rounded-md hover:text-mineshaft-500;
|
@apply rounded-md hover:text-mineshaft-500;
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export const LogsSection = ({
|
|||||||
userAgentType,
|
userAgentType,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
actorId: actor
|
actor
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
|
|||||||
+25
-1
@@ -11,7 +11,7 @@ import {
|
|||||||
SiSnowflake
|
SiSnowflake
|
||||||
} from "react-icons/si";
|
} from "react-icons/si";
|
||||||
import { faAws } from "@fortawesome/free-brands-svg-icons";
|
import { faAws } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { faDatabase } from "@fortawesome/free-solid-svg-icons";
|
import { faClock, faDatabase } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ import { RabbitMqInputForm } from "./RabbitMqInputForm";
|
|||||||
import { RedisInputForm } from "./RedisInputForm";
|
import { RedisInputForm } from "./RedisInputForm";
|
||||||
import { SapHanaInputForm } from "./SapHanaInputForm";
|
import { SapHanaInputForm } from "./SapHanaInputForm";
|
||||||
import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm";
|
import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm";
|
||||||
|
import { TotpInputForm } from "./TotpInputForm";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
@@ -110,6 +111,11 @@ const DYNAMIC_SECRET_LIST = [
|
|||||||
icon: <SiSnowflake size="1.5rem" />,
|
icon: <SiSnowflake size="1.5rem" />,
|
||||||
provider: DynamicSecretProviders.Snowflake,
|
provider: DynamicSecretProviders.Snowflake,
|
||||||
title: "Snowflake"
|
title: "Snowflake"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FontAwesomeIcon icon={faClock} size="lg" />,
|
||||||
|
provider: DynamicSecretProviders.Totp,
|
||||||
|
title: "TOTP"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -405,6 +411,24 @@ export const CreateDynamicSecretForm = ({
|
|||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
{wizardStep === WizardSteps.ProviderInputs &&
|
||||||
|
selectedProvider === DynamicSecretProviders.Totp && (
|
||||||
|
<motion.div
|
||||||
|
key="dynamic-totp-step"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
>
|
||||||
|
<TotpInputForm
|
||||||
|
onCompleted={handleFormReset}
|
||||||
|
onCancel={handleFormReset}
|
||||||
|
projectSlug={projectSlug}
|
||||||
|
secretPath={secretPath}
|
||||||
|
environment={environment}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
+314
@@ -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<typeof formSchema>;
|
||||||
|
|
||||||
|
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<TForm>({
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="dynamic-secret" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||||
|
Configuration
|
||||||
|
<Link
|
||||||
|
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/totp"
|
||||||
|
passHref
|
||||||
|
>
|
||||||
|
<a target="_blank" rel="noopener noreferrer">
|
||||||
|
<div className="ml-2 mb-1 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
|
Docs
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.configType"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Configuration Type"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(val) => {
|
||||||
|
field.onChange(val);
|
||||||
|
}}
|
||||||
|
dropdownContainerClassName="max-w-full"
|
||||||
|
>
|
||||||
|
<SelectItem value={ConfigType.URL} key="config-type-url">
|
||||||
|
URL
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={ConfigType.MANUAL} key="config-type-manual">
|
||||||
|
Manual
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{selectedConfigType === ConfigType.URL && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.url"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="OTP URL"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="otpauth://" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedConfigType === ConfigType.MANUAL && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.secret"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Key"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.period"
|
||||||
|
defaultValue={30}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Period"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.digits"
|
||||||
|
defaultValue={6}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Digits"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.algorithm"
|
||||||
|
defaultValue={TotpAlgorithm.SHA1}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Algorithm"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
className="w-full"
|
||||||
|
dropdownContainerClassName="max-w-full"
|
||||||
|
onValueChange={(val) => {
|
||||||
|
field.onChange(val);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectItem value={TotpAlgorithm.SHA1} key="algorithm-sha-1">
|
||||||
|
SHA1
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={TotpAlgorithm.SHA256} key="algorithm-sha-256">
|
||||||
|
SHA256
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={TotpAlgorithm.SHA512} key="algorithm-sha-512">
|
||||||
|
SHA512
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="mb-8 text-sm font-normal text-gray-400">
|
||||||
|
The period, digits, and algorithm values can remain at their defaults unless
|
||||||
|
your TOTP provider specifies otherwise.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-center space-x-4">
|
||||||
|
<Button type="submit" isLoading={isSubmitting}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline_bg" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -154,7 +154,7 @@ export const CreateSecretForm = ({
|
|||||||
isMulti
|
isMulti
|
||||||
name="tagIds"
|
name="tagIds"
|
||||||
isDisabled={!canReadTags}
|
isDisabled={!canReadTags}
|
||||||
isLoading={isTagsLoading}
|
isLoading={isTagsLoading && canReadTags}
|
||||||
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
|
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
|
|||||||
+129
-7
@@ -1,6 +1,6 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
@@ -9,8 +9,16 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { TtlFormLabel } from "@app/components/features";
|
import { TtlFormLabel } from "@app/components/features";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, IconButton, Input, SecretInput, Tooltip } from "@app/components/v2";
|
import {
|
||||||
import { useTimedReset } from "@app/hooks";
|
Button,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
SecretInput,
|
||||||
|
Spinner,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useTimedReset, useToggle } from "@app/hooks";
|
||||||
import { useCreateDynamicSecretLease } from "@app/hooks/api";
|
import { useCreateDynamicSecretLease } from "@app/hooks/api";
|
||||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
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<void>;
|
||||||
|
}) => {
|
||||||
|
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 (
|
||||||
|
<div className="h-36">
|
||||||
|
<OutputDisplay label="Time-based one-time password" value={totp} />
|
||||||
|
{remainingTime > 0 ? (
|
||||||
|
<div
|
||||||
|
className={`ml-2 flex items-center text-sm ${
|
||||||
|
remainingTime < 10 ? "text-red-500" : "text-yellow-500"
|
||||||
|
} transition-colors duration-500`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon className="mr-1" icon={faClock} size="sm" />
|
||||||
|
<span>
|
||||||
|
Expires in {remainingTime} {remainingTime > 1 ? "seconds" : "second"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="ml-2 flex items-center text-sm text-red-500">
|
||||||
|
<FontAwesomeIcon className="mr-1" icon={faClock} size="sm" />
|
||||||
|
Expired
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{shouldShowRegenerate && (
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="mt-2"
|
||||||
|
onClick={() => triggerLeaseRegeneration({})}
|
||||||
|
>
|
||||||
|
Regenerate
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderOutputForm = (
|
||||||
|
provider: DynamicSecretProviders,
|
||||||
|
data: unknown,
|
||||||
|
triggerLeaseRegeneration: (details: { ttl?: string }) => Promise<void>
|
||||||
|
) => {
|
||||||
if (
|
if (
|
||||||
provider === DynamicSecretProviders.SqlDatabase ||
|
provider === DynamicSecretProviders.SqlDatabase ||
|
||||||
provider === DynamicSecretProviders.Cassandra ||
|
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 (
|
||||||
|
<TotpOutputDisplay
|
||||||
|
totp={TOTP}
|
||||||
|
remainingSeconds={TIME_REMAINING}
|
||||||
|
triggerLeaseRegeneration={triggerLeaseRegeneration}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formSchema = z.object({
|
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<typeof formSchema>;
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
@@ -259,6 +354,8 @@ type Props = {
|
|||||||
secretPath: string;
|
secretPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PROVIDERS_WITH_AUTOGENERATE_SUPPORT = [DynamicSecretProviders.Totp];
|
||||||
|
|
||||||
export const CreateDynamicSecretLease = ({
|
export const CreateDynamicSecretLease = ({
|
||||||
onClose,
|
onClose,
|
||||||
projectSlug,
|
projectSlug,
|
||||||
@@ -277,6 +374,9 @@ export const CreateDynamicSecretLease = ({
|
|||||||
ttl: "1h"
|
ttl: "1h"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const [isPreloading, setIsPreloading] = useToggle(
|
||||||
|
PROVIDERS_WITH_AUTOGENERATE_SUPPORT.includes(provider)
|
||||||
|
);
|
||||||
|
|
||||||
const createDynamicSecretLease = useCreateDynamicSecretLease();
|
const createDynamicSecretLease = useCreateDynamicSecretLease();
|
||||||
|
|
||||||
@@ -290,10 +390,13 @@ export const CreateDynamicSecretLease = ({
|
|||||||
ttl,
|
ttl,
|
||||||
dynamicSecretName
|
dynamicSecretName
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
type: "success",
|
type: "success",
|
||||||
text: "Successfully leased dynamic secret"
|
text: "Successfully leased dynamic secret"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setIsPreloading.off();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
createNotification({
|
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);
|
const isOutputMode = Boolean(createDynamicSecretLease?.data);
|
||||||
|
|
||||||
|
if (isPreloading) {
|
||||||
|
return <Spinner className="mx-auto h-40 text-mineshaft-700" />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@@ -350,7 +468,11 @@ export const CreateDynamicSecretLease = ({
|
|||||||
animate={{ opacity: 1, translateX: 0 }}
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
exit={{ opacity: 0, translateX: 30 }}
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
>
|
>
|
||||||
{renderOutputForm(provider, createDynamicSecretLease.data?.data)}
|
{renderOutputForm(
|
||||||
|
provider,
|
||||||
|
createDynamicSecretLease.data?.data,
|
||||||
|
handleLeaseRegeneration
|
||||||
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|||||||
+10
@@ -101,10 +101,20 @@ export const DynamicSecretListView = ({
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyDown={(evt) => {
|
onKeyDown={(evt) => {
|
||||||
|
// no lease view for TOTP because it's irrelevant
|
||||||
|
if (secret.type === DynamicSecretProviders.Totp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (evt.key === "Enter" && !isRevoking)
|
if (evt.key === "Enter" && !isRevoking)
|
||||||
handlePopUpOpen("dynamicSecretLeases", secret.id);
|
handlePopUpOpen("dynamicSecretLeases", secret.id);
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
// no lease view for TOTP because it's irrelevant
|
||||||
|
if (secret.type === DynamicSecretProviders.Totp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isRevoking) {
|
if (!isRevoking) {
|
||||||
handlePopUpOpen("dynamicSecretLeases", secret.id);
|
handlePopUpOpen("dynamicSecretLeases", secret.id);
|
||||||
}
|
}
|
||||||
|
|||||||
+18
@@ -17,6 +17,7 @@ import { EditDynamicSecretRedisProviderForm } from "./EditDynamicSecretRedisProv
|
|||||||
import { EditDynamicSecretSapHanaForm } from "./EditDynamicSecretSapHanaForm";
|
import { EditDynamicSecretSapHanaForm } from "./EditDynamicSecretSapHanaForm";
|
||||||
import { EditDynamicSecretSnowflakeForm } from "./EditDynamicSecretSnowflakeForm";
|
import { EditDynamicSecretSnowflakeForm } from "./EditDynamicSecretSnowflakeForm";
|
||||||
import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm";
|
import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm";
|
||||||
|
import { EditDynamicSecretTotpForm } from "./EditDynamicSecretTotpForm";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -276,6 +277,23 @@ export const EditDynamicSecretForm = ({
|
|||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
{dynamicSecretDetails?.type === DynamicSecretProviders.Totp && (
|
||||||
|
<motion.div
|
||||||
|
key="totp-edit"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
>
|
||||||
|
<EditDynamicSecretTotpForm
|
||||||
|
onClose={onClose}
|
||||||
|
projectSlug={projectSlug}
|
||||||
|
secretPath={secretPath}
|
||||||
|
dynamicSecret={dynamicSecretDetails}
|
||||||
|
environment={environment}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+318
@@ -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<typeof formSchema>;
|
||||||
|
|
||||||
|
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<TForm>({
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="newName"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="dynamic-secret" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||||
|
Configuration
|
||||||
|
<Link
|
||||||
|
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/totp"
|
||||||
|
passHref
|
||||||
|
>
|
||||||
|
<a target="_blank" rel="noopener noreferrer">
|
||||||
|
<div className="ml-2 mb-1 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
|
Docs
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.configType"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Configuration Type"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(val) => {
|
||||||
|
field.onChange(val);
|
||||||
|
}}
|
||||||
|
dropdownContainerClassName="max-w-full"
|
||||||
|
>
|
||||||
|
<SelectItem value={ConfigType.URL} key="config-type-url">
|
||||||
|
URL
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={ConfigType.MANUAL} key="config-type-manual">
|
||||||
|
Manual
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{selectedConfigType === ConfigType.URL && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.url"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="OTP URL"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedConfigType === ConfigType.MANUAL && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.secret"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Key"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.period"
|
||||||
|
defaultValue={30}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Period"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.digits"
|
||||||
|
defaultValue={6}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Digits"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.algorithm"
|
||||||
|
defaultValue={TotpAlgorithm.SHA1}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Algorithm"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
className="w-full"
|
||||||
|
dropdownContainerClassName="max-w-full"
|
||||||
|
onValueChange={(val) => {
|
||||||
|
field.onChange(val);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectItem value={TotpAlgorithm.SHA1} key="algorithm-sha-1">
|
||||||
|
SHA1
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={TotpAlgorithm.SHA256} key="algorithm-sha-256">
|
||||||
|
SHA256
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={TotpAlgorithm.SHA512} key="algorithm-sha-512">
|
||||||
|
SHA512
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="mb-8 text-sm font-normal text-gray-400">
|
||||||
|
The period, digits, and algorithm values can remain at their defaults unless
|
||||||
|
your TOTP provider specifies otherwise.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-center space-x-4">
|
||||||
|
<Button type="submit" isLoading={isSubmitting}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline_bg" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -867,7 +867,7 @@ export const SecretOverviewPage = () => {
|
|||||||
<div className="thin-scrollbar mt-4">
|
<div className="thin-scrollbar mt-4">
|
||||||
<TableContainer
|
<TableContainer
|
||||||
onScroll={(e) => setScrollOffset(e.currentTarget.scrollLeft)}
|
onScroll={(e) => setScrollOffset(e.currentTarget.scrollLeft)}
|
||||||
className="thin-scrollbar"
|
className="thin-scrollbar rounded-b-none"
|
||||||
>
|
>
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
|
|||||||
+1
-1
@@ -255,7 +255,7 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
|
|||||||
isMulti
|
isMulti
|
||||||
name="tagIds"
|
name="tagIds"
|
||||||
isDisabled={!canReadTags}
|
isDisabled={!canReadTags}
|
||||||
isLoading={isTagsLoading}
|
isLoading={isTagsLoading && canReadTags}
|
||||||
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
|
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
|
|||||||
+4
-7
@@ -1,10 +1,11 @@
|
|||||||
<h1 align="center">Infisical</h1>
|
<h1 align="center">Infisical CLI</h1>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<p align="center"><b>The open-source secret management platform</b>: Sync secrets/configs across your team/infrastructure and prevent secret leaks.</p>
|
<p align="center"><b>Embrace shift-left security with the Infisical CLI and strengthen your DevSecOps practices by seamlessly managing secrets across your workflows, pipelines, and applications.</b></p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h4 align="center">
|
<h4 align="center">
|
||||||
<a href="https://infisical.com/slack">Slack</a> |
|
<a href="https://infisical.com/slack">Slack</a> |
|
||||||
|
<a href="https://www.npmjs.com/package/@infisical/sdk">Node.js SDK</a> |
|
||||||
<a href="https://infisical.com/">Infisical Cloud</a> |
|
<a href="https://infisical.com/">Infisical Cloud</a> |
|
||||||
<a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> |
|
<a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> |
|
||||||
<a href="https://infisical.com/docs/documentation/getting-started/introduction">Docs</a> |
|
<a href="https://infisical.com/docs/documentation/getting-started/introduction">Docs</a> |
|
||||||
@@ -12,7 +13,6 @@
|
|||||||
<a href="https://infisical.com/careers">Hiring (Remote/SF)</a>
|
<a href="https://infisical.com/careers">Hiring (Remote/SF)</a>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
|
|
||||||
<h4 align="center">
|
<h4 align="center">
|
||||||
<a href="https://github.com/Infisical/infisical/blob/main/LICENSE">
|
<a href="https://github.com/Infisical/infisical/blob/main/LICENSE">
|
||||||
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Infisical is released under the MIT license." />
|
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Infisical is released under the MIT license." />
|
||||||
@@ -36,10 +36,7 @@
|
|||||||
|
|
||||||
### Introduction
|
### 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.
|
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.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user