mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-11 12:11:38 +00:00
Compare commits
46 Commits
docs-updat
...
daniel/pat
Author | SHA1 | Date | |
---|---|---|---|
464a3ccd53 | |||
71c9c0fa1e | |||
2b977eeb33 | |||
a692148597 | |||
64bfa4f334 | |||
e3eb14bfd9 | |||
24b50651c9 | |||
d7b494c6f8 | |||
93208afb36 | |||
1a084d8fcf | |||
dd4f133c6c | |||
c41d27e1ae | |||
1866ed8d23 | |||
7b3b232dde | |||
9d618b4ae9 | |||
5330ab2171 | |||
662e588c22 | |||
90057d80ff | |||
1eda7aaaac | |||
00dcadbc08 | |||
7a7289ebd0 | |||
e5d4677fd6 | |||
bce3f3d676 | |||
300372fa98 | |||
47a4f8bae9 | |||
863719f296 | |||
7317dc1cf5 | |||
75df898e78 | |||
0de6add3f7 | |||
0c008b6393 | |||
0c3894496c | |||
35fbd5d49d | |||
d03b453e3d | |||
96e331b678 | |||
d4d468660d | |||
75a4965928 | |||
660c09ded4 | |||
b5287d91c0 | |||
6a17763237 | |||
f2bd3daea2 | |||
9a62efea4f | |||
3be3d807d2 | |||
9f7ea3c4e5 | |||
e67218f170 | |||
269c40c67c | |||
ba1fd8a3f7 |
@ -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 = {
|
||||||
|
92
backend/src/ee/services/dynamic-secret/providers/totp.ts
Normal file
92
backend/src/ee/services/dynamic-secret/providers/totp.ts
Normal file
@ -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";
|
||||||
@ -64,7 +64,13 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
void res.status(HttpStatusCodes.Forbidden).send({
|
void res.status(HttpStatusCodes.Forbidden).send({
|
||||||
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({
|
||||||
|
@ -47,6 +47,7 @@ export const DefaultResponseErrorsSchema = {
|
|||||||
403: z.object({
|
403: z.object({
|
||||||
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({
|
||||||
|
@ -9,6 +9,7 @@ import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
|||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema";
|
import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema";
|
||||||
|
import { Integrations } from "@app/services/integration-auth/integration-list";
|
||||||
import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
import {} from "../sanitizedSchemas";
|
import {} from "../sanitizedSchemas";
|
||||||
@ -206,6 +207,33 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
|||||||
id: req.params.integrationId
|
id: req.params.integrationId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (integration.region) {
|
||||||
|
integration.metadata = {
|
||||||
|
...(integration.metadata || {}),
|
||||||
|
region: integration.region
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
integration.integration === Integrations.AWS_SECRET_MANAGER ||
|
||||||
|
integration.integration === Integrations.AWS_PARAMETER_STORE
|
||||||
|
) {
|
||||||
|
const awsRoleDetails = await server.services.integration.getIntegrationAWSIamRole({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.integrationId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (awsRoleDetails) {
|
||||||
|
integration.metadata = {
|
||||||
|
...(integration.metadata || {}),
|
||||||
|
awsIamRole: awsRoleDetails.role
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { integration };
|
return { integration };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -182,7 +182,12 @@ export const identityProjectServiceFactory = ({
|
|||||||
|
|
||||||
// validate custom roles input
|
// validate custom roles input
|
||||||
const customInputRoles = roles.filter(
|
const customInputRoles = roles.filter(
|
||||||
({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
|
({ role }) =>
|
||||||
|
!Object.values(ProjectMembershipRole)
|
||||||
|
// we don't want to include custom in this check;
|
||||||
|
// this unintentionally enables setting slug to custom which is reserved
|
||||||
|
.filter((r) => r !== ProjectMembershipRole.Custom)
|
||||||
|
.includes(role as ProjectMembershipRole)
|
||||||
);
|
);
|
||||||
const hasCustomRole = Boolean(customInputRoles.length);
|
const hasCustomRole = Boolean(customInputRoles.length);
|
||||||
const customRoles = hasCustomRole
|
const customRoles = hasCustomRole
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -9,6 +9,7 @@ import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth
|
|||||||
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
|
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
|
||||||
import { deleteIntegrationSecrets } from "../integration-auth/integration-delete-secret";
|
import { deleteIntegrationSecrets } from "../integration-auth/integration-delete-secret";
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
import { TSecretQueueFactory } from "../secret/secret-queue";
|
import { TSecretQueueFactory } from "../secret/secret-queue";
|
||||||
@ -237,6 +238,46 @@ export const integrationServiceFactory = ({
|
|||||||
return { ...integration, envId: integration.environment.id };
|
return { ...integration, envId: integration.environment.id };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getIntegrationAWSIamRole = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => {
|
||||||
|
const integration = await integrationDAL.findById(id);
|
||||||
|
|
||||||
|
if (!integration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Integration with ID '${id}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
integration?.projectId || "",
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||||
|
|
||||||
|
const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId);
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: integration.projectId
|
||||||
|
});
|
||||||
|
let awsIamRole: string | null = null;
|
||||||
|
if (integrationAuth.encryptedAwsAssumeIamRoleArn) {
|
||||||
|
const awsAssumeRoleArn = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn)
|
||||||
|
}).toString();
|
||||||
|
if (awsAssumeRoleArn) {
|
||||||
|
const [, role] = awsAssumeRoleArn.split(":role/");
|
||||||
|
awsIamRole = role;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
role: awsIamRole
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const deleteIntegration = async ({
|
const deleteIntegration = async ({
|
||||||
actorId,
|
actorId,
|
||||||
id,
|
id,
|
||||||
@ -329,6 +370,7 @@ export const integrationServiceFactory = ({
|
|||||||
deleteIntegration,
|
deleteIntegration,
|
||||||
listIntegrationByProject,
|
listIntegrationByProject,
|
||||||
getIntegration,
|
getIntegration,
|
||||||
|
getIntegrationAWSIamRole,
|
||||||
syncIntegration
|
syncIntegration
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -280,7 +280,12 @@ export const projectMembershipServiceFactory = ({
|
|||||||
|
|
||||||
// validate custom roles input
|
// validate custom roles input
|
||||||
const customInputRoles = roles.filter(
|
const customInputRoles = roles.filter(
|
||||||
({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
|
({ role }) =>
|
||||||
|
!Object.values(ProjectMembershipRole)
|
||||||
|
// we don't want to include custom in this check;
|
||||||
|
// this unintentionally enables setting slug to custom which is reserved
|
||||||
|
.filter((r) => r !== ProjectMembershipRole.Custom)
|
||||||
|
.includes(role as ProjectMembershipRole)
|
||||||
);
|
);
|
||||||
const hasCustomRole = Boolean(customInputRoles.length);
|
const hasCustomRole = Boolean(customInputRoles.length);
|
||||||
if (hasCustomRole) {
|
if (hasCustomRole) {
|
||||||
|
@ -191,6 +191,10 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
return project;
|
return project;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
throw new DatabaseError({ error, name: "Find all projects" });
|
throw new DatabaseError({ error, name: "Find all projects" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -240,6 +244,10 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
return project;
|
return project;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError || error instanceof UnauthorizedError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
throw new DatabaseError({ error, name: "Find project by slug" });
|
throw new DatabaseError({ error, name: "Find project by slug" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -260,7 +268,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
throw new BadRequestError({ message: "Invalid filter type" });
|
throw new BadRequestError({ message: "Invalid filter type" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof BadRequestError) {
|
if (error instanceof BadRequestError || error instanceof NotFoundError || error instanceof UnauthorizedError) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
throw new DatabaseError({ error, name: `Failed to find project by ${filter.type}` });
|
throw new DatabaseError({ error, name: `Failed to find project by ${filter.type}` });
|
||||||
|
@ -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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
70
docs/documentation/platform/dynamic-secrets/totp.mdx
Normal file
70
docs/documentation/platform/dynamic-secrets/totp.mdx
Normal file
@ -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 |
BIN
docs/images/platform/dynamic-secrets/totp-lease-value.png
Normal file
BIN
docs/images/platform/dynamic-secrets/totp-lease-value.png
Normal file
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
|
||||||
|
@ -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;
|
||||||
|
@ -57,6 +57,9 @@ export type TIntegration = {
|
|||||||
shouldMaskSecrets?: boolean;
|
shouldMaskSecrets?: boolean;
|
||||||
shouldProtectSecrets?: boolean;
|
shouldProtectSecrets?: boolean;
|
||||||
shouldEnableDelete?: boolean;
|
shouldEnableDelete?: boolean;
|
||||||
|
|
||||||
|
awsIamRole?: string;
|
||||||
|
region?: 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";
|
||||||
@ -52,9 +53,14 @@ export type TApiErrors =
|
|||||||
| {
|
| {
|
||||||
error: ApiErrorTypes.ValidationError;
|
error: ApiErrorTypes.ValidationError;
|
||||||
message: ZodIssue[];
|
message: ZodIssue[];
|
||||||
|
statusCode: 401;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
error: ApiErrorTypes.ForbiddenError;
|
||||||
|
message: string;
|
||||||
|
details: PureAbility["rules"];
|
||||||
statusCode: 403;
|
statusCode: 403;
|
||||||
}
|
}
|
||||||
| { error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 401 }
|
|
||||||
| {
|
| {
|
||||||
statusCode: 400;
|
statusCode: 400;
|
||||||
message: string;
|
message: string;
|
||||||
|
@ -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,35 +18,151 @@ 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: "Please check the input and try again.",
|
||||||
{serverResponse.message?.map(({ message, path }) => (
|
children: (
|
||||||
<div className="flex space-y-2" key={path.join(".")}>
|
<Modal>
|
||||||
<div>
|
<ModalTrigger>
|
||||||
Field <i>{path.join(".")}</i> {message.toLowerCase()}
|
<Button variant="outline_bg" size="xs">
|
||||||
</div>
|
Show more
|
||||||
</div>
|
</Button>
|
||||||
))}
|
</ModalTrigger>
|
||||||
</div>
|
<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.statusCode === 401) {
|
if (serverResponse?.error === ApiErrorTypes.ForbiddenError) {
|
||||||
createNotification({
|
createNotification(
|
||||||
title: "Forbidden Access",
|
{
|
||||||
type: "error",
|
title: "Forbidden Access",
|
||||||
text: serverResponse.message
|
type: "error",
|
||||||
});
|
text: serverResponse.message,
|
||||||
|
children: serverResponse?.details?.length ? (
|
||||||
|
<Modal>
|
||||||
|
<ModalTrigger>
|
||||||
|
<Button variant="outline_bg" size="xs">
|
||||||
|
Show more
|
||||||
|
</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;
|
return;
|
||||||
}
|
}
|
||||||
createNotification({ title: "Bad Request", type: "error", text: serverResponse.message });
|
createNotification({ title: "Bad Request", type: "error", text: serverResponse.message });
|
||||||
|
@ -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;
|
||||||
|
@ -26,7 +26,9 @@ const metadataMappings: Record<keyof NonNullable<TIntegrationWithEnv["metadata"]
|
|||||||
shouldDisableDelete: "AWS Secret Deletion Disabled",
|
shouldDisableDelete: "AWS Secret Deletion Disabled",
|
||||||
shouldMaskSecrets: "GitLab Secrets Masking Enabled",
|
shouldMaskSecrets: "GitLab Secrets Masking Enabled",
|
||||||
shouldProtectSecrets: "GitLab Secret Protection Enabled",
|
shouldProtectSecrets: "GitLab Secret Protection Enabled",
|
||||||
shouldEnableDelete: "GitHub Secret Deletion Enabled"
|
shouldEnableDelete: "GitHub Secret Deletion Enabled",
|
||||||
|
awsIamRole: "AWS IAM Role",
|
||||||
|
region: "Region"
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const IntegrationSettingsSection = ({ integration }: Props) => {
|
export const IntegrationSettingsSection = ({ integration }: Props) => {
|
||||||
|
@ -98,7 +98,7 @@ export const LogsSection = ({
|
|||||||
userAgentType,
|
userAgentType,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
actorId: actor
|
actor
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
|
@ -50,11 +50,34 @@ export const IdentityRoleDetailsSection = ({
|
|||||||
const handleRoleDelete = async () => {
|
const handleRoleDelete = async () => {
|
||||||
const { id } = popUp?.deleteRole?.data as TProjectRole;
|
const { id } = popUp?.deleteRole?.data as TProjectRole;
|
||||||
try {
|
try {
|
||||||
const updatedRole = identityMembershipDetails?.roles?.filter((el) => el.id !== id);
|
const updatedRoles = identityMembershipDetails?.roles?.filter((el) => el.id !== id);
|
||||||
await updateIdentityWorkspaceRole({
|
await updateIdentityWorkspaceRole({
|
||||||
workspaceId: currentWorkspace?.id || "",
|
workspaceId: currentWorkspace?.id || "",
|
||||||
identityId: identityMembershipDetails.identity.id,
|
identityId: identityMembershipDetails.identity.id,
|
||||||
roles: updatedRole
|
roles: updatedRoles.map(
|
||||||
|
({
|
||||||
|
role,
|
||||||
|
customRoleSlug,
|
||||||
|
isTemporary,
|
||||||
|
temporaryMode,
|
||||||
|
temporaryRange,
|
||||||
|
temporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime
|
||||||
|
}) => ({
|
||||||
|
role: role === "custom" ? customRoleSlug : role,
|
||||||
|
...(isTemporary
|
||||||
|
? {
|
||||||
|
isTemporary,
|
||||||
|
temporaryMode,
|
||||||
|
temporaryRange,
|
||||||
|
temporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
isTemporary
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
createNotification({ type: "success", text: "Successfully removed role" });
|
createNotification({ type: "success", text: "Successfully removed role" });
|
||||||
handlePopUpClose("deleteRole");
|
handlePopUpClose("deleteRole");
|
||||||
|
@ -61,10 +61,33 @@ export const MemberRoleDetailsSection = ({
|
|||||||
const handleRoleDelete = async () => {
|
const handleRoleDelete = async () => {
|
||||||
const { id } = popUp?.deleteRole?.data as TProjectRole;
|
const { id } = popUp?.deleteRole?.data as TProjectRole;
|
||||||
try {
|
try {
|
||||||
const updatedRole = membershipDetails?.roles?.filter((el) => el.id !== id);
|
const updatedRoles = membershipDetails?.roles?.filter((el) => el.id !== id);
|
||||||
await updateUserWorkspaceRole({
|
await updateUserWorkspaceRole({
|
||||||
workspaceId: currentWorkspace?.id || "",
|
workspaceId: currentWorkspace?.id || "",
|
||||||
roles: updatedRole,
|
roles: updatedRoles.map(
|
||||||
|
({
|
||||||
|
role,
|
||||||
|
customRoleSlug,
|
||||||
|
isTemporary,
|
||||||
|
temporaryMode,
|
||||||
|
temporaryRange,
|
||||||
|
temporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime
|
||||||
|
}) => ({
|
||||||
|
role: role === "custom" ? customRoleSlug : role,
|
||||||
|
...(isTemporary
|
||||||
|
? {
|
||||||
|
isTemporary,
|
||||||
|
temporaryMode,
|
||||||
|
temporaryRange,
|
||||||
|
temporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
isTemporary
|
||||||
|
})
|
||||||
|
})
|
||||||
|
),
|
||||||
membershipId: membershipDetails.id
|
membershipId: membershipDetails.id
|
||||||
});
|
});
|
||||||
createNotification({ type: "success", text: "Successfully removed role" });
|
createNotification({ type: "success", text: "Successfully removed role" });
|
||||||
@ -215,7 +238,10 @@ export const MemberRoleDetailsSection = ({
|
|||||||
title="Roles"
|
title="Roles"
|
||||||
subTitle="Select one or more of the pre-defined or custom roles to configure project permissions."
|
subTitle="Select one or more of the pre-defined or custom roles to configure project permissions."
|
||||||
>
|
>
|
||||||
<MemberRoleModify projectMember={membershipDetails} onOpenUpgradeModal={onOpenUpgradeModal} />
|
<MemberRoleModify
|
||||||
|
projectMember={membershipDetails}
|
||||||
|
onOpenUpgradeModal={onOpenUpgradeModal}
|
||||||
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,7 +22,11 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { policyDetails } from "@app/helpers/policies";
|
import { policyDetails } from "@app/helpers/policies";
|
||||||
import { useCreateSecretApprovalPolicy, useListWorkspaceGroups, useUpdateSecretApprovalPolicy } from "@app/hooks/api";
|
import {
|
||||||
|
useCreateSecretApprovalPolicy,
|
||||||
|
useListWorkspaceGroups,
|
||||||
|
useUpdateSecretApprovalPolicy
|
||||||
|
} from "@app/hooks/api";
|
||||||
import {
|
import {
|
||||||
useCreateAccessApprovalPolicy,
|
useCreateAccessApprovalPolicy,
|
||||||
useUpdateAccessApprovalPolicy
|
useUpdateAccessApprovalPolicy
|
||||||
@ -46,7 +50,11 @@ const formSchema = z
|
|||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
secretPath: z.string().optional(),
|
secretPath: z.string().optional(),
|
||||||
approvals: z.number().min(1),
|
approvals: z.number().min(1),
|
||||||
approvers: z.object({type: z.nativeEnum(ApproverType), id: z.string()}).array().min(1).default([]),
|
approvers: z
|
||||||
|
.object({ type: z.nativeEnum(ApproverType), id: z.string() })
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.default([]),
|
||||||
policyType: z.nativeEnum(PolicyType),
|
policyType: z.nativeEnum(PolicyType),
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel)
|
enforcementLevel: z.nativeEnum(EnforcementLevel)
|
||||||
})
|
})
|
||||||
@ -100,6 +108,8 @@ export const AccessPolicyForm = ({
|
|||||||
|
|
||||||
const policyName = policyDetails[watch("policyType")]?.name || "Policy";
|
const policyName = policyDetails[watch("policyType")]?.name || "Policy";
|
||||||
|
|
||||||
|
const approversRequired = watch("approvals") || 1;
|
||||||
|
|
||||||
const handleCreatePolicy = async (data: TFormSchema) => {
|
const handleCreatePolicy = async (data: TFormSchema) => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
|
|
||||||
@ -169,12 +179,6 @@ export const AccessPolicyForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatEnforcementLevel = (level: EnforcementLevel) => {
|
|
||||||
if (level === EnforcementLevel.Hard) return "Hard";
|
|
||||||
if (level === EnforcementLevel.Soft) return "Soft";
|
|
||||||
return level;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||||
<ModalContent title={isEditMode ? `Edit ${policyName}` : "Create Policy"}>
|
<ModalContent title={isEditMode ? `Edit ${policyName}` : "Create Policy"}>
|
||||||
@ -257,14 +261,15 @@ export const AccessPolicyForm = ({
|
|||||||
name="secretPath"
|
name="secretPath"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Secret Path"
|
tooltipText="Secret paths support glob patterns. For example, '/**' will match all paths."
|
||||||
isError={Boolean(error)}
|
label="Secret Path"
|
||||||
errorText={error?.message}
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input {...field} value={field.value || ""} />
|
<Input {...field} value={field.value || ""} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="approvals"
|
name="approvals"
|
||||||
@ -295,9 +300,11 @@ export const AccessPolicyForm = ({
|
|||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
tooltipText="Determines the level of enforcement for required approvers of a request"
|
tooltipText="Determines the level of enforcement for required approvers of a request"
|
||||||
helperText={
|
helperText={
|
||||||
field.value === EnforcementLevel.Hard
|
<div className="ml-1">
|
||||||
? "All approvers must approve the request."
|
{field.value === EnforcementLevel.Hard
|
||||||
: "All approvers must approve the request; however, the requester can bypass approval requirements in emergencies."
|
? `Hard enforcement requires at least ${approversRequired} approver(s) to approve the request.`
|
||||||
|
: `At least ${approversRequired} approver(s) must approve the request; however, the requester can bypass approval requirements in emergencies.`}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
@ -307,12 +314,8 @@ export const AccessPolicyForm = ({
|
|||||||
>
|
>
|
||||||
{Object.values(EnforcementLevel).map((level) => {
|
{Object.values(EnforcementLevel).map((level) => {
|
||||||
return (
|
return (
|
||||||
<SelectItem
|
<SelectItem value={level} key={`enforcement-level-${level}`}>
|
||||||
value={level}
|
<span className="capitalize">{level}</span>
|
||||||
key={`enforcement-level-${level}`}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
{formatEnforcementLevel(level)}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -320,7 +323,12 @@ export const AccessPolicyForm = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<p>Approvers</p>
|
<div className="mb-2">
|
||||||
|
<p>Approvers</p>
|
||||||
|
<p className="font-inter text-xs text-mineshaft-300 opacity-90">
|
||||||
|
Select members or groups that are allowed to approve requests from this policy.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="approvers"
|
name="approvers"
|
||||||
@ -334,7 +342,11 @@ export const AccessPolicyForm = ({
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Input
|
<Input
|
||||||
isReadOnly
|
isReadOnly
|
||||||
value={value?.filter((e) => e.type=== ApproverType.User).length ? `${value.filter((e) => e.type=== ApproverType.User).length} selected` : "None"}
|
value={
|
||||||
|
value?.filter((e) => e.type === ApproverType.User).length
|
||||||
|
? `${value.filter((e) => e.type === ApproverType.User).length} selected`
|
||||||
|
: "None"
|
||||||
|
}
|
||||||
className="text-left"
|
className="text-left"
|
||||||
/>
|
/>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@ -347,15 +359,22 @@ export const AccessPolicyForm = ({
|
|||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{members.map(({ user }) => {
|
{members.map(({ user }) => {
|
||||||
const { id: userId } = user;
|
const { id: userId } = user;
|
||||||
const isChecked = value?.filter((el: {id: string, type: ApproverType}) => el.id === userId && el.type === ApproverType.User).length > 0;
|
const isChecked =
|
||||||
|
value?.filter(
|
||||||
|
(el: { id: string; type: ApproverType }) =>
|
||||||
|
el.id === userId && el.type === ApproverType.User
|
||||||
|
).length > 0;
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(evt) => {
|
onClick={(evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
onChange(
|
onChange(
|
||||||
isChecked
|
isChecked
|
||||||
? value?.filter((el: {id: string, type: ApproverType}) => el.id !== userId && el.type !== ApproverType.User)
|
? value?.filter(
|
||||||
: [...(value || []), {id:userId, type: ApproverType.User}]
|
(el: { id: string; type: ApproverType }) =>
|
||||||
|
el.id !== userId && el.type !== ApproverType.User
|
||||||
|
)
|
||||||
|
: [...(value || []), { id: userId, type: ApproverType.User }]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
key={`create-policy-members-${userId}`}
|
key={`create-policy-members-${userId}`}
|
||||||
@ -384,7 +403,13 @@ export const AccessPolicyForm = ({
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Input
|
<Input
|
||||||
isReadOnly
|
isReadOnly
|
||||||
value={value?.filter((e) => e.type=== ApproverType.Group).length ? `${value?.filter((e) => e.type=== ApproverType.Group).length} selected` : "None"}
|
value={
|
||||||
|
value?.filter((e) => e.type === ApproverType.Group).length
|
||||||
|
? `${
|
||||||
|
value?.filter((e) => e.type === ApproverType.Group).length
|
||||||
|
} selected`
|
||||||
|
: "None"
|
||||||
|
}
|
||||||
className="text-left"
|
className="text-left"
|
||||||
/>
|
/>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@ -395,28 +420,36 @@ export const AccessPolicyForm = ({
|
|||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
Select groups that are allowed to approve requests
|
Select groups that are allowed to approve requests
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{groups && groups.map(({ group }) => {
|
{groups &&
|
||||||
const { id } = group;
|
groups.map(({ group }) => {
|
||||||
const isChecked = value?.filter((el: {id: string, type: ApproverType}) => el.id === id && el.type === ApproverType.Group).length > 0;
|
const { id } = group;
|
||||||
|
const isChecked =
|
||||||
|
value?.filter(
|
||||||
|
(el: { id: string; type: ApproverType }) =>
|
||||||
|
el.id === id && el.type === ApproverType.Group
|
||||||
|
).length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(evt) => {
|
onClick={(evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
onChange(
|
onChange(
|
||||||
isChecked
|
isChecked
|
||||||
? value?.filter((el: {id: string, type: ApproverType}) => el.id !== id && el.type !== ApproverType.Group)
|
? value?.filter(
|
||||||
: [...(value || []), {id, type: ApproverType.Group}]
|
(el: { id: string; type: ApproverType }) =>
|
||||||
);
|
el.id !== id && el.type !== ApproverType.Group
|
||||||
}}
|
)
|
||||||
key={`create-policy-members-${id}`}
|
: [...(value || []), { id, type: ApproverType.Group }]
|
||||||
iconPos="right"
|
);
|
||||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
}}
|
||||||
>
|
key={`create-policy-members-${id}`}
|
||||||
{group.name}
|
iconPos="right"
|
||||||
</DropdownMenuItem>
|
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
);
|
>
|
||||||
})}
|
{group.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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