Compare commits

...

59 Commits

Author SHA1 Message Date
Scott Wilson
cf5f49d14e chore: use toggle order 2024-11-26 17:26:49 -08:00
Scott Wilson
4f4b5be8ea fix: lowercase name compare for sort 2024-11-26 17:25:13 -08:00
Scott Wilson
bbf52c9a48 improvement: add pagination to the project overview page with minor UI adjustments 2024-11-26 11:47:59 -08:00
Daniel Hougaard
7321c237d7 Merge pull request #2792 from Infisical/daniel/dynamic-secret-renewals
fix(dynamic-secrets): renewal 500 error
2024-11-26 01:52:28 +04:00
McPizza
32430a6a16 feat: Add Project Descriptions (#2774)
* feat:  initial backend project description
2024-11-25 21:59:14 +01:00
Daniel Hougaard
463eb0014e fix(dynamic-secrets): renewal 500 error 2024-11-25 20:17:50 +04:00
Daniel Hougaard
21403f6fe5 Merge pull request #2761 from Infisical/daniel/cli-login-domains-fix
fix: allow preset domains for `infisical login`
2024-11-25 16:16:08 +04:00
Daniel Hougaard
2f9e542b31 Merge pull request #2760 from Infisical/daniel/request-ids
feat: request ID support
2024-11-25 16:13:19 +04:00
Daniel Hougaard
089d6812fd Update ldap-fns.ts 2024-11-25 16:00:20 +04:00
Maidul Islam
71c9c0fa1e Merge pull request #2781 from Infisical/daniel/project-slug-500-error
fix: improve project DAL error handling
2024-11-24 19:43:26 -05:00
Daniel Hougaard
2b977eeb33 fix: improve project error handling 2024-11-23 03:42:54 +04:00
McPizza
a692148597 feat(integrations): Add AWS Secrets Manager IAM Role + Region (#2778) 2024-11-23 00:04:33 +01:00
Maidul Islam
64bfa4f334 Merge pull request #2779 from Infisical/fix-delete-project-role
Fix: Prevent Updating Identity/User Project Role to reserved "Custom" Slug
2024-11-22 16:23:22 -05:00
Scott Wilson
e3eb14bfd9 fix: add custom slug check to user 2024-11-22 13:09:47 -08:00
Scott Wilson
24b50651c9 fix: correct update role mapping for identity/user and prevent updating role slug to "custom" 2024-11-22 13:02:00 -08:00
Daniel Hougaard
1cd459fda7 Merge branch 'heads/main' into daniel/request-ids 2024-11-23 00:14:50 +04:00
Daniel Hougaard
38917327d9 feat: request lifecycle request ID 2024-11-22 23:19:07 +04:00
Maidul Islam
d7b494c6f8 Merge pull request #2775 from akhilmhdh/fix/patches-3
fix: db error on token auth and permission issue
2024-11-22 12:43:20 -05:00
=
93208afb36 fix: db error on token auth and permission issue 2024-11-22 22:41:53 +05:30
Maidul Islam
1a084d8fcf add direct link td provider 2024-11-21 21:26:46 -05:00
Sheen
dd4f133c6c Merge pull request #2769 from Infisical/misc/made-identity-metadata-value-not-nullable-again
misc: made identity metadata value not nullable
2024-11-22 01:59:01 +08:00
Sheen Capadngan
c41d27e1ae misc: made identity metadata value not nullable 2024-11-21 21:27:56 +08:00
Sheen
1866ed8d23 Merge pull request #2742 from Infisical/feat/totp-dynamic-secret
feat: TOTP dynamic secret provider
2024-11-21 12:00:12 +08:00
Scott Wilson
7b3b232dde replace loader with spinner 2024-11-20 14:09:26 -08:00
Scott Wilson
9d618b4ae9 minor text revisions/additions and add colors/icons to totp token expiry countdown 2024-11-20 14:01:40 -08:00
Vlad Matsiiako
5330ab2171 Merge pull request #2768 from BnjmnZmmrmn/k8s_integration_docs_typo
fixing small typo in docs/integrations/platforms/kubernetes
2024-11-20 15:49:35 -05:00
Sheen Capadngan
662e588c22 misc: add handling for lease regen 2024-11-21 04:43:21 +08:00
Akhil Mohan
90057d80ff Merge pull request #2767 from akhilmhdh/feat/permission-error
Detail error when permission validation error occurs
2024-11-21 02:00:51 +05:30
Scott Wilson
1eda7aaaac reverse license 2024-11-20 12:14:14 -08:00
Sheen Capadngan
00dcadbc08 misc: added timer 2024-11-21 04:09:19 +08:00
Benjamin Riley Zimmerman
7a7289ebd0 fixing typo in docs/integrations/platforms/kubernetes 2024-11-20 11:50:13 -08:00
Scott Wilson
e5d4677fd6 improvements: minor UI/labeling adjustments, only show tags loading if can read, and remove rounded bottom on overview table 2024-11-20 11:50:10 -08:00
Sheen Capadngan
bce3f3d676 misc: addressed review comments 2024-11-21 02:37:56 +08:00
=
300372fa98 feat: resolve dependency cycle error 2024-11-20 23:59:49 +05:30
Maidul Islam
47a4f8bae9 Merge pull request #2766 from Infisical/omar/eng-1886-make-terraform-integration-secrets-marked-as-sensitive
Improvement(Terraform Cloud Integration): Synced secrets are hidden from Terraform UI
2024-11-20 13:16:45 -05:00
=
863719f296 feat: added action button for notification toast and one action each for forbidden error and validation error details 2024-11-20 22:55:14 +05:30
=
7317dc1cf5 feat: modified error handler to return possible rules for a validation failed rules 2024-11-20 22:50:21 +05:30
Daniel Hougaard
75df898e78 Merge pull request #2762 from Infisical/daniel/cli-installer-readme
chore(cli-installer): readme improvements
2024-11-20 20:29:23 +04:00
McPizza0
0de6add3f7 set all new and existing secrets to be sensitive: true 2024-11-20 17:28:09 +01:00
Daniel Hougaard
0c008b6393 Update README.md 2024-11-20 20:26:00 +04:00
Sheen Capadngan
0c3894496c feat: added support for configuring totp with secret key 2024-11-20 23:40:36 +08:00
Daniel Hougaard
35fbd5d49d Merge pull request #2764 from Infisical/daniel/pre-commit-cli-check
chore: check for CLI installation before pre-commit
2024-11-20 19:01:54 +04:00
Daniel Hougaard
d03b453e3d Merge pull request #2765 from Infisical/daniel/actor-id-mismatch
fix(audit-logs): actor / actor ID mismatch
2024-11-20 18:58:33 +04:00
Daniel Hougaard
96e331b678 fix(audit-logs): actor / actor ID mismatch 2024-11-20 18:50:29 +04:00
Daniel Hougaard
d4d468660d chore: check for CLI installation before pre-commit 2024-11-20 17:29:36 +04:00
Daniel Hougaard
75a4965928 requested changes 2024-11-20 16:23:59 +04:00
Sheen Capadngan
660c09ded4 Merge branch 'feat/totp-dynamic-secret' of https://github.com/Infisical/infisical into feat/totp-dynamic-secret 2024-11-20 18:56:56 +08:00
Sheen Capadngan
b5287d91c0 misc: addressed comments 2024-11-20 18:56:16 +08:00
Scott Wilson
6a17763237 docs: dynamic secret doc typos addressed 2024-11-19 19:58:01 -08:00
Daniel Hougaard
f2bd3daea2 Update README.md 2024-11-20 03:24:05 +04:00
Daniel Hougaard
7f70f96936 fix: allow preset domains for infisical login 2024-11-20 01:06:18 +04:00
Daniel Hougaard
73e0a54518 feat: request ID support 2024-11-20 00:01:25 +04:00
Daniel Hougaard
0d295a2824 fix: application crash on zod api error 2024-11-20 00:00:30 +04:00
Daniel Hougaard
9a62efea4f Merge pull request #2759 from Infisical/docs-update-note
update docs note
2024-11-19 23:51:44 +04:00
Sheen Capadngan
3be3d807d2 misc: added URL string validation 2024-11-18 19:32:57 +08:00
Sheen Capadngan
9f7ea3c4e5 doc: added docs for totp dynamic secret 2024-11-18 19:27:45 +08:00
Sheen Capadngan
e67218f170 misc: finalized option setting logic 2024-11-18 18:34:27 +08:00
Sheen Capadngan
269c40c67c Merge remote-tracking branch 'origin/main' into feat/totp-dynamic-secret 2024-11-18 17:31:19 +08:00
Sheen Capadngan
ba1fd8a3f7 feat: totp dynamic secret 2024-11-16 02:48:28 +08:00
125 changed files with 2598 additions and 1040 deletions

View File

@@ -1,6 +1,12 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Check if infisical is installed
if ! command -v infisical >/dev/null 2>&1; then
echo "\nError: Infisical CLI is not installed. Please install the Infisical CLI before comitting.\n You can refer to the documentation at https://infisical.com/docs/cli/overview\n\n"
exit 1
fi
npx lint-staged
infisical scan git-changes --staged -v

View File

@@ -24,6 +24,7 @@
"@fastify/multipart": "8.3.0",
"@fastify/passport": "^2.4.0",
"@fastify/rate-limit": "^9.0.0",
"@fastify/request-context": "^5.1.0",
"@fastify/session": "^10.7.0",
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",
@@ -5528,6 +5529,15 @@
"toad-cache": "^3.3.0"
}
},
"node_modules/@fastify/request-context": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@fastify/request-context/-/request-context-5.1.0.tgz",
"integrity": "sha512-PM7wrLJOEylVDpxabOFLaYsdAiaa0lpDUcP2HMFJ1JzgiWuC6k4r3duf6Pm9YLnzlGmT+Yp4tkQjqsu7V/pSOA==",
"license": "MIT",
"dependencies": {
"fastify-plugin": "^4.0.0"
}
},
"node_modules/@fastify/send": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz",

View File

@@ -132,6 +132,7 @@
"@fastify/multipart": "8.3.0",
"@fastify/passport": "^2.4.0",
"@fastify/rate-limit": "^9.0.0",
"@fastify/request-context": "^5.1.0",
"@fastify/session": "^10.7.0",
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",

View File

@@ -0,0 +1,7 @@
import "@fastify/request-context";
declare module "@fastify/request-context" {
interface RequestContextData {
requestId: string;
}
}

View File

@@ -1,6 +1,6 @@
import { FastifyInstance, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify";
import { Logger } from "pino";
import { CustomLogger } from "@app/lib/logger/logger";
import { ZodTypeProvider } from "@app/server/plugins/fastify-zod";
declare global {
@@ -8,7 +8,7 @@ declare global {
RawServerDefault,
RawRequestDefaultExpression<RawServerDefault>,
RawReplyDefaultExpression<RawServerDefault>,
Readonly<Logger>,
Readonly<CustomLogger>,
ZodTypeProvider
>;

View File

@@ -0,0 +1,23 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasProjectDescription = await knex.schema.hasColumn(TableName.Project, "description");
if (!hasProjectDescription) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.string("description");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasProjectDescription = await knex.schema.hasColumn(TableName.Project, "description");
if (hasProjectDescription) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.dropColumn("description");
});
}
}

View File

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

View File

@@ -12,7 +12,7 @@ import { TImmutableDBKeys } from "./models";
export const KmsRootConfigSchema = z.object({
id: z.string().uuid(),
encryptedRootKey: zodBuffer,
encryptionStrategy: z.string(),
encryptionStrategy: z.string().default("SOFTWARE").nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});

View File

@@ -23,7 +23,8 @@ export const ProjectsSchema = z.object({
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
auditLogsRetentionDays: z.number().nullable().optional(),
kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional()
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(),
description: z.string().nullable().optional()
});
export type TProjects = z.infer<typeof ProjectsSchema>;

View File

@@ -212,7 +212,7 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
// No renewal necessary
return { entityId };
};

View File

@@ -179,9 +179,8 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
};
const renew = async (_inputs: unknown, entityId: string) => {
// do nothing
const username = entityId;
return { entityId: username };
// No renewal necessary
return { entityId };
};
return {

View File

@@ -55,11 +55,6 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
return data.success;
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
return { entityId };
};
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
@@ -127,6 +122,11 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
return users;
};
const renew = async (inputs: unknown, entityId: string) => {
// No renewal necessary
return { entityId };
};
return {
validateProviderInputs,
validateConnection,

View File

@@ -99,20 +99,24 @@ export const CassandraProvider = (): TDynamicProviderFns => {
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
if (!providerInputs.renewStatement) return { entityId };
const client = await getClient(providerInputs);
const username = entityId;
const expiration = new Date(expireAt).toISOString();
const { keyspace } = providerInputs;
const renewStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace, expiration });
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
username: entityId,
keyspace,
expiration
});
const queries = renewStatement.toString().split(";").filter(Boolean);
for (const query of queries) {
// eslint-disable-next-line
for await (const query of queries) {
await client.execute(query);
}
await client.shutdown();
return { entityId: username };
return { entityId };
};
return {

View File

@@ -96,7 +96,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
// No renewal necessary
return { entityId };
};

View File

@@ -13,6 +13,7 @@ import { RabbitMqProvider } from "./rabbit-mq";
import { RedisDatabaseProvider } from "./redis";
import { SapHanaProvider } from "./sap-hana";
import { SqlDatabaseProvider } from "./sql-database";
import { TotpProvider } from "./totp";
export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
@@ -27,5 +28,6 @@ export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
[DynamicSecretProviders.Ldap]: LdapProvider(),
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
[DynamicSecretProviders.Snowflake]: SnowflakeProvider()
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
[DynamicSecretProviders.Totp]: TotpProvider()
});

View File

@@ -268,7 +268,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
// No renewal necessary
return { entityId };
};

View File

@@ -17,6 +17,17 @@ export enum LdapCredentialType {
Static = "static"
}
export enum TotpConfigType {
URL = "url",
MANUAL = "manual"
}
export enum TotpAlgorithm {
SHA1 = "sha1",
SHA256 = "sha256",
SHA512 = "sha512"
}
export const DynamicSecretRedisDBSchema = z.object({
host: z.string().trim().toLowerCase(),
port: z.number(),
@@ -221,6 +232,34 @@ export const LdapSchema = z.union([
})
]);
export const DynamicSecretTotpSchema = z.discriminatedUnion("configType", [
z.object({
configType: z.literal(TotpConfigType.URL),
url: z
.string()
.url()
.trim()
.min(1)
.refine((val) => {
const urlObj = new URL(val);
const secret = urlObj.searchParams.get("secret");
return Boolean(secret);
}, "OTP URL must contain secret field")
}),
z.object({
configType: z.literal(TotpConfigType.MANUAL),
secret: z
.string()
.trim()
.min(1)
.transform((val) => val.replace(/\s+/g, "")),
period: z.number().optional(),
algorithm: z.nativeEnum(TotpAlgorithm).optional(),
digits: z.number().optional()
})
]);
export enum DynamicSecretProviders {
SqlDatabase = "sql-database",
Cassandra = "cassandra",
@@ -234,7 +273,8 @@ export enum DynamicSecretProviders {
AzureEntraID = "azure-entra-id",
Ldap = "ldap",
SapHana = "sap-hana",
Snowflake = "snowflake"
Snowflake = "snowflake",
Totp = "totp"
}
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
@@ -250,7 +290,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema })
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema })
]);
export type TDynamicProviderFns = {

View File

@@ -88,6 +88,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
};
const renew = async (_inputs: unknown, entityId: string) => {
// No renewal necessary
return { entityId };
};

View File

@@ -142,7 +142,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
// No renewal necessary
return { entityId };
};

View File

@@ -141,6 +141,8 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
if (!providerInputs.renewStatement) return { entityId };
const connection = await getClient(providerInputs);
const username = entityId;

View File

@@ -135,13 +135,15 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
return { entityId: username };
};
const renew = async (inputs: unknown, username: string, expireAt: number) => {
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
if (!providerInputs.renewStatement) return { entityId };
const client = await getClient(providerInputs);
try {
const expiration = new Date(expireAt).toISOString();
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration });
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username: entityId, expiration });
const queries = renewStatement.toString().split(";").filter(Boolean);
for await (const query of queries) {
await new Promise((resolve, reject) => {
@@ -161,7 +163,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
client.disconnect();
}
return { entityId: username };
return { entityId };
};
return {

View File

@@ -131,17 +131,16 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
return { entityId: username };
};
const renew = async (inputs: unknown, username: string, expireAt: number) => {
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
if (!providerInputs.renewStatement) return { entityId: username };
if (!providerInputs.renewStatement) return { entityId };
const client = await getClient(providerInputs);
try {
const expiration = getDaysToExpiry(new Date(expireAt));
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
username,
username: entityId,
expiration
});
@@ -161,7 +160,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
client.destroy(noop);
}
return { entityId: username };
return { entityId };
};
return {

View File

@@ -110,13 +110,19 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
if (!providerInputs.renewStatement) return { entityId };
const db = await getClient(providerInputs);
const username = entityId;
const expiration = new Date(expireAt).toISOString();
const { database } = providerInputs;
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration, database });
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
username: entityId,
expiration,
database
});
if (renewStatement) {
const queries = renewStatement.toString().split(";").filter(Boolean);
await db.transaction(async (tx) => {
@@ -128,7 +134,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
}
await db.destroy();
return { entityId: username };
return { entityId };
};
return {

View File

@@ -0,0 +1,90 @@
import { authenticator } from "otplib";
import { HashAlgorithms } from "otplib/core";
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) => {
// No renewal necessary
return { entityId };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@@ -27,7 +27,7 @@ export const initializeHsmModule = () => {
logger.info("PKCS#11 module initialized");
} catch (err) {
logger.error("Failed to initialize PKCS#11 module:", err);
logger.error(err, "Failed to initialize PKCS#11 module");
throw err;
}
};
@@ -39,7 +39,7 @@ export const initializeHsmModule = () => {
isInitialized = false;
logger.info("PKCS#11 module finalized");
} catch (err) {
logger.error("Failed to finalize PKCS#11 module:", err);
logger.error(err, "Failed to finalize PKCS#11 module");
throw err;
}
}

View File

@@ -36,8 +36,7 @@ export const testLDAPConfig = async (ldapConfig: TLDAPConfig): Promise<boolean>
});
ldapClient.on("error", (err) => {
logger.error("LDAP client error:", err);
logger.error(err);
logger.error(err, "LDAP client error");
resolve(false);
});

View File

@@ -161,8 +161,8 @@ export const licenseServiceFactory = ({
}
} catch (error) {
logger.error(
`getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]`,
error
error,
`getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]`
);
await keyStore.setItemWithExpiry(
FEATURE_CACHE_KEY(orgId),

View File

@@ -127,14 +127,15 @@ export const permissionDALFactory = (db: TDbClient) => {
const getProjectPermission = async (userId: string, projectId: string) => {
try {
const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId");
const docs = await db
.replicaNode()(TableName.Users)
.where(`${TableName.Users}.id`, userId)
.leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.GroupProjectMembership, (queryBuilder) => {
void queryBuilder
.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId]))
.andOn(`${TableName.GroupProjectMembership}.groupId`, `${TableName.UserGroupMembership}.groupId`);
// @ts-expect-error akhilmhdh: this is valid knexjs query. Its just ts type argument is missing it
.andOnIn(`${TableName.GroupProjectMembership}.groupId`, subQueryUserGroups);
})
.leftJoin(
TableName.GroupProjectMembershipRole,

View File

@@ -1,14 +1,7 @@
import picomatch from "picomatch";
import { z } from "zod";
export enum PermissionConditionOperators {
$IN = "$in",
$ALL = "$all",
$REGEX = "$regex",
$EQ = "$eq",
$NEQ = "$ne",
$GLOB = "$glob"
}
import { PermissionConditionOperators } from "@app/lib/casl";
export const PermissionConditionSchema = {
[PermissionConditionOperators.$IN]: z.string().trim().min(1).array(),

View File

@@ -1,10 +1,10 @@
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
import { z } from "zod";
import { conditionsMatcher } from "@app/lib/casl";
import { conditionsMatcher, PermissionConditionOperators } from "@app/lib/casl";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types";
import { PermissionConditionSchema } from "./permission-types";
export enum ProjectPermissionActions {
Read = "read",

View File

@@ -46,7 +46,7 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
}
return rateLimit;
} catch (err) {
logger.error("Error fetching rate limits %o", err);
logger.error(err, "Error fetching rate limits");
return undefined;
}
};
@@ -69,12 +69,12 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
mfaRateLimit: rateLimit.mfaRateLimit
};
logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration);
logger.info(newRateLimitMaxConfiguration, "syncRateLimitConfiguration: rate limit configuration");
Object.freeze(newRateLimitMaxConfiguration);
rateLimitMaxConfiguration = newRateLimitMaxConfiguration;
}
} catch (error) {
logger.error(`Error syncing rate limit configurations: %o`, error);
logger.error(error, "Error syncing rate limit configurations");
}
};

View File

@@ -238,11 +238,11 @@ export const secretScanningQueueFactory = ({
});
queueService.listen(QueueName.SecretPushEventScan, "failed", (job, err) => {
logger.error("Failed to secret scan on push", job?.data, err);
logger.error(err, "Failed to secret scan on push", job?.data);
});
queueService.listen(QueueName.SecretFullRepoScan, "failed", (job, err) => {
logger.error("Failed to do full repo secret scan", job?.data, err);
logger.error(err, "Failed to do full repo secret scan", job?.data);
});
return { startFullRepoScan, startPushEventScan };

View File

@@ -391,6 +391,7 @@ export const PROJECTS = {
CREATE: {
organizationSlug: "The slug of the organization to create the project in.",
projectName: "The name of the project to create.",
projectDescription: "An optional description label for the project.",
slug: "An optional slug for the project.",
template: "The name of the project template, if specified, to apply to this project."
},
@@ -403,6 +404,7 @@ export const PROJECTS = {
UPDATE: {
workspaceId: "The ID of the project to update.",
name: "The new name of the project.",
projectDescription: "An optional description label for the project.",
autoCapitalization: "Disable or enable auto-capitalization for the project."
},
GET_KEY: {

View File

@@ -54,3 +54,12 @@ export const isAtLeastAsPrivileged = (permissions1: MongoAbility, permissions2:
return set1.size >= set2.size;
};
export enum PermissionConditionOperators {
$IN = "$in",
$ALL = "$all",
$REGEX = "$regex",
$EQ = "$eq",
$NEQ = "$ne",
$GLOB = "$glob"
}

View File

@@ -1,7 +1,7 @@
import { Logger } from "pino";
import { z } from "zod";
import { removeTrailingSlash } from "../fn";
import { CustomLogger } from "../logger/logger";
import { zpStr } from "../zod";
export const GITLAB_URL = "https://gitlab.com";
@@ -212,7 +212,7 @@ let envCfg: Readonly<z.infer<typeof envSchema>>;
export const getConfig = () => envCfg;
// cannot import singleton logger directly as it needs config to load various transport
export const initEnvConfig = (logger?: Logger) => {
export const initEnvConfig = (logger?: CustomLogger) => {
const parsedEnv = envSchema.safeParse(process.env);
if (!parsedEnv.success) {
(logger ?? console).error("Invalid environment variables. Check the error below");

View File

@@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
// logger follows a singleton pattern
// easier to use it that's all.
import { requestContext } from "@fastify/request-context";
import pino, { Logger } from "pino";
import { z } from "zod";
@@ -13,14 +15,37 @@ const logLevelToSeverityLookup: Record<string, string> = {
"60": "CRITICAL"
};
// eslint-disable-next-line import/no-mutable-exports
export let logger: Readonly<Logger>;
// akhilmhdh:
// The logger is not placed in the main app config to avoid a circular dependency.
// The config requires the logger to display errors when an invalid environment is supplied.
// On the other hand, the logger needs the config to obtain credentials for AWS or other transports.
// By keeping the logger separate, it becomes an independent package.
// We define our own custom logger interface to enforce structure to the logging methods.
export interface CustomLogger extends Omit<Logger, "info" | "error" | "warn" | "debug"> {
info: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(obj: unknown, msg?: string, ...args: any[]): void;
};
error: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(obj: unknown, msg?: string, ...args: any[]): void;
};
warn: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(obj: unknown, msg?: string, ...args: any[]): void;
};
debug: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(obj: unknown, msg?: string, ...args: any[]): void;
};
}
// eslint-disable-next-line import/no-mutable-exports
export let logger: Readonly<CustomLogger>;
const loggerConfig = z.object({
AWS_CLOUDWATCH_LOG_GROUP_NAME: z.string().default("infisical-log-stream"),
AWS_CLOUDWATCH_LOG_REGION: z.string().default("us-east-1"),
@@ -62,6 +87,17 @@ const redactedKeys = [
"config"
];
const UNKNOWN_REQUEST_ID = "UNKNOWN_REQUEST_ID";
const extractRequestId = () => {
try {
return requestContext.get("requestId") || UNKNOWN_REQUEST_ID;
} catch (err) {
console.log("failed to get request context", err);
return UNKNOWN_REQUEST_ID;
}
};
export const initLogger = async () => {
const cfg = loggerConfig.parse(process.env);
const targets: pino.TransportMultiOptions["targets"][number][] = [
@@ -94,6 +130,30 @@ export const initLogger = async () => {
targets
});
const wrapLogger = (originalLogger: Logger): CustomLogger => {
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.info = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ requestId: extractRequestId() }).info(obj, msg, ...args);
};
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.error = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ requestId: extractRequestId() }).error(obj, msg, ...args);
};
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.warn = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ requestId: extractRequestId() }).warn(obj, msg, ...args);
};
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.debug = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ requestId: extractRequestId() }).debug(obj, msg, ...args);
};
return originalLogger;
};
logger = pino(
{
mixin(_context, level) {
@@ -113,5 +173,6 @@ export const initLogger = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
transport
);
return logger;
return wrapLogger(logger);
};

View File

@@ -10,13 +10,15 @@ import fastifyFormBody from "@fastify/formbody";
import helmet from "@fastify/helmet";
import type { FastifyRateLimitOptions } from "@fastify/rate-limit";
import ratelimiter from "@fastify/rate-limit";
import { fastifyRequestContext } from "@fastify/request-context";
import fastify from "fastify";
import { Knex } from "knex";
import { Logger } from "pino";
import { HsmModule } from "@app/ee/services/hsm/hsm-types";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig, IS_PACKAGED } from "@app/lib/config/env";
import { CustomLogger } from "@app/lib/logger/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TQueueServiceFactory } from "@app/queue";
import { TSmtpService } from "@app/services/smtp/smtp-service";
@@ -35,7 +37,7 @@ type TMain = {
auditLogDb?: Knex;
db: Knex;
smtp: TSmtpService;
logger?: Logger;
logger?: CustomLogger;
queue: TQueueServiceFactory;
keyStore: TKeyStoreFactory;
hsmModule: HsmModule;
@@ -47,7 +49,9 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
const server = fastify({
logger: appCfg.NODE_ENV === "test" ? false : logger,
genReqId: () => `req-${alphaNumericNanoId(14)}`,
trustProxy: true,
connectionTimeout: appCfg.isHsmConfigured ? 90_000 : 30_000,
ignoreTrailingSlash: true,
pluginTimeout: 40_000
@@ -104,6 +108,13 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
await server.register(maintenanceMode);
await server.register(fastifyRequestContext, {
defaultStoreValues: (request) => ({
requestId: request.id,
log: request.log.child({ requestId: request.id })
})
});
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule });
if (appCfg.isProductionMode) {

View File

@@ -1,4 +1,4 @@
import { ForbiddenError } from "@casl/ability";
import { ForbiddenError, PureAbility } from "@casl/ability";
import fastifyPlugin from "fastify-plugin";
import jwt from "jsonwebtoken";
import { ZodError } from "zod";
@@ -39,77 +39,102 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
if (error instanceof BadRequestError) {
void res
.status(HttpStatusCodes.BadRequest)
.send({ statusCode: HttpStatusCodes.BadRequest, message: error.message, error: error.name });
.send({ requestId: req.id, statusCode: HttpStatusCodes.BadRequest, message: error.message, error: error.name });
} else if (error instanceof NotFoundError) {
void res
.status(HttpStatusCodes.NotFound)
.send({ statusCode: HttpStatusCodes.NotFound, message: error.message, error: error.name });
.send({ requestId: req.id, statusCode: HttpStatusCodes.NotFound, message: error.message, error: error.name });
} else if (error instanceof UnauthorizedError) {
void res
.status(HttpStatusCodes.Unauthorized)
.send({ statusCode: HttpStatusCodes.Unauthorized, message: error.message, error: error.name });
void res.status(HttpStatusCodes.Unauthorized).send({
requestId: req.id,
statusCode: HttpStatusCodes.Unauthorized,
message: error.message,
error: error.name
});
} else if (error instanceof DatabaseError || error instanceof InternalServerError) {
void res
.status(HttpStatusCodes.InternalServerError)
.send({ statusCode: HttpStatusCodes.InternalServerError, message: "Something went wrong", error: error.name });
void res.status(HttpStatusCodes.InternalServerError).send({
requestId: req.id,
statusCode: HttpStatusCodes.InternalServerError,
message: "Something went wrong",
error: error.name
});
} else if (error instanceof GatewayTimeoutError) {
void res
.status(HttpStatusCodes.GatewayTimeout)
.send({ statusCode: HttpStatusCodes.GatewayTimeout, message: error.message, error: error.name });
void res.status(HttpStatusCodes.GatewayTimeout).send({
requestId: req.id,
statusCode: HttpStatusCodes.GatewayTimeout,
message: error.message,
error: error.name
});
} else if (error instanceof ZodError) {
void res
.status(HttpStatusCodes.Unauthorized)
.send({ statusCode: HttpStatusCodes.Unauthorized, error: "ValidationFailure", message: error.issues });
void res.status(HttpStatusCodes.Unauthorized).send({
requestId: req.id,
statusCode: HttpStatusCodes.Unauthorized,
error: "ValidationFailure",
message: error.issues
});
} else if (error instanceof ForbiddenError) {
void res.status(HttpStatusCodes.Forbidden).send({
requestId: req.id,
statusCode: HttpStatusCodes.Forbidden,
error: "PermissionDenied",
message: `You are not allowed to ${error.action} on ${error.subjectType} - ${JSON.stringify(error.subject)}`
message: `You are not allowed to ${error.action} on ${error.subjectType}`,
details: (error.ability as PureAbility).rulesFor(error.action as string, error.subjectType).map((el) => ({
action: el.action,
inverted: el.inverted,
subject: el.subject,
conditions: el.conditions
}))
});
} else if (error instanceof ForbiddenRequestError) {
void res.status(HttpStatusCodes.Forbidden).send({
requestId: req.id,
statusCode: HttpStatusCodes.Forbidden,
message: error.message,
error: error.name
});
} else if (error instanceof RateLimitError) {
void res.status(HttpStatusCodes.TooManyRequests).send({
requestId: req.id,
statusCode: HttpStatusCodes.TooManyRequests,
message: error.message,
error: error.name
});
} else if (error instanceof ScimRequestError) {
void res.status(error.status).send({
requestId: req.id,
schemas: error.schemas,
status: error.status,
detail: error.detail
});
} else if (error instanceof OidcAuthError) {
void res
.status(HttpStatusCodes.InternalServerError)
.send({ statusCode: HttpStatusCodes.InternalServerError, message: error.message, error: error.name });
void res.status(HttpStatusCodes.InternalServerError).send({
requestId: req.id,
statusCode: HttpStatusCodes.InternalServerError,
message: error.message,
error: error.name
});
} else if (error instanceof jwt.JsonWebTokenError) {
const message = (() => {
if (error.message === JWTErrors.JwtExpired) {
return "Your token has expired. Please re-authenticate.";
}
if (error.message === JWTErrors.JwtMalformed) {
return "The provided access token is malformed. Please use a valid token or generate a new one and try again.";
}
if (error.message === JWTErrors.InvalidAlgorithm) {
return "The access token is signed with an invalid algorithm. Please provide a valid token and try again.";
}
let errorMessage = error.message;
return error.message;
})();
if (error.message === JWTErrors.JwtExpired) {
errorMessage = "Your token has expired. Please re-authenticate.";
} else if (error.message === JWTErrors.JwtMalformed) {
errorMessage =
"The provided access token is malformed. Please use a valid token or generate a new one and try again.";
} else if (error.message === JWTErrors.InvalidAlgorithm) {
errorMessage =
"The access token is signed with an invalid algorithm. Please provide a valid token and try again.";
}
void res.status(HttpStatusCodes.Forbidden).send({
requestId: req.id,
statusCode: HttpStatusCodes.Forbidden,
error: "TokenError",
message
message: errorMessage
});
} else {
void res.status(HttpStatusCodes.InternalServerError).send({
requestId: req.id,
statusCode: HttpStatusCodes.InternalServerError,
error: "InternalServerError",
message: "Something went wrong"

View File

@@ -19,7 +19,7 @@ export const registerSecretScannerGhApp = async (server: FastifyZodProvider) =>
app.on("installation", async (context) => {
const { payload } = context;
logger.info("Installed secret scanner to:", { repositories: payload.repositories });
logger.info({ repositories: payload.repositories }, "Installed secret scanner to");
});
app.on("push", async (context) => {

View File

@@ -30,26 +30,32 @@ export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({
export const DefaultResponseErrorsSchema = {
400: z.object({
requestId: z.string(),
statusCode: z.literal(400),
message: z.string(),
error: z.string()
}),
404: z.object({
requestId: z.string(),
statusCode: z.literal(404),
message: z.string(),
error: z.string()
}),
401: z.object({
requestId: z.string(),
statusCode: z.literal(401),
message: z.any(),
error: z.string()
}),
403: z.object({
requestId: z.string(),
statusCode: z.literal(403),
message: z.string(),
details: z.any().optional(),
error: z.string()
}),
500: z.object({
requestId: z.string(),
statusCode: z.literal(500),
message: z.string(),
error: z.string()
@@ -206,6 +212,7 @@ export const SanitizedAuditLogStreamSchema = z.object({
export const SanitizedProjectSchema = ProjectsSchema.pick({
id: true,
name: true,
description: true,
slug: true,
autoCapitalization: true,
orgId: true,

View File

@@ -9,6 +9,7 @@ import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
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 {} from "../sanitizedSchemas";
@@ -206,6 +207,33 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
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 };
}
});

View File

@@ -296,6 +296,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
.max(64, { message: "Name must be 64 or fewer characters" })
.optional()
.describe(PROJECTS.UPDATE.name),
description: z
.string()
.trim()
.max(256, { message: "Description must be 256 or fewer characters" })
.optional()
.describe(PROJECTS.UPDATE.projectDescription),
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization)
}),
response: {
@@ -313,6 +319,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
update: {
name: req.body.name,
description: req.body.description,
autoCapitalization: req.body.autoCapitalization
},
actorAuthMethod: req.permission.authMethod,

View File

@@ -161,6 +161,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
],
body: z.object({
projectName: z.string().trim().describe(PROJECTS.CREATE.projectName),
projectDescription: z.string().trim().optional().describe(PROJECTS.CREATE.projectDescription),
slug: z
.string()
.min(5)
@@ -194,6 +195,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
workspaceName: req.body.projectName,
workspaceDescription: req.body.projectDescription,
slug: req.body.slug,
kmsKeyId: req.body.kmsKeyId,
template: req.body.template
@@ -312,8 +314,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
slug: slugSchema.describe("The slug of the project to update.")
}),
body: z.object({
name: z.string().trim().optional().describe("The new name of the project."),
autoCapitalization: z.boolean().optional().describe("The new auto-capitalization setting.")
name: z.string().trim().optional().describe(PROJECTS.UPDATE.name),
description: z.string().trim().optional().describe(PROJECTS.UPDATE.projectDescription),
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization)
}),
response: {
200: SanitizedProjectSchema
@@ -330,6 +333,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
update: {
name: req.body.name,
description: req.body.description,
autoCapitalization: req.body.autoCapitalization
},
actorId: req.permission.id,

View File

@@ -182,7 +182,12 @@ export const identityProjectServiceFactory = ({
// validate custom roles input
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 customRoles = hasCustomRole

View File

@@ -385,8 +385,8 @@ export const identityTokenAuthServiceFactory = ({
actorOrgId
}: TUpdateTokenAuthTokenDTO) => {
const foundToken = await identityAccessTokenDAL.findOne({
id: tokenId,
authMethod: IdentityAuthMethod.TOKEN_AUTH
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
[`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH
});
if (!foundToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` });
@@ -444,8 +444,8 @@ export const identityTokenAuthServiceFactory = ({
}: TRevokeTokenAuthTokenDTO) => {
const identityAccessToken = await identityAccessTokenDAL.findOne({
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
isAccessTokenRevoked: false,
authMethod: IdentityAuthMethod.TOKEN_AUTH
[`${TableName.IdentityAccessToken}.isAccessTokenRevoked` as "isAccessTokenRevoked"]: false,
[`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH
});
if (!identityAccessToken)
throw new NotFoundError({

View File

@@ -3075,7 +3075,7 @@ const syncSecretsTerraformCloud = async ({
}) => {
// get secrets from Terraform Cloud
const terraformSecrets = (
await request.get<{ data: { attributes: { key: string; value: string }; id: string }[] }>(
await request.get<{ data: { attributes: { key: string; value: string; sensitive: boolean }; id: string }[] }>(
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars`,
{
headers: {
@@ -3089,7 +3089,7 @@ const syncSecretsTerraformCloud = async ({
...obj,
[secret.attributes.key]: secret
}),
{} as Record<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 } = {};
@@ -3170,7 +3170,8 @@ const syncSecretsTerraformCloud = async ({
attributes: {
key,
value: secrets[key]?.value,
category: integration.targetService
category: integration.targetService,
sensitive: true
}
}
},
@@ -3183,7 +3184,11 @@ const syncSecretsTerraformCloud = async ({
}
);
// case: secret exists in Terraform Cloud
} else if (secrets[key]?.value !== terraformSecrets[key].attributes.value) {
} else if (
// we now set secrets to sensitive in Terraform Cloud, this checks if existing secrets are not sensitive and updates them accordingly
!terraformSecrets[key].attributes.sensitive ||
secrets[key]?.value !== terraformSecrets[key].attributes.value
) {
// -> update secret
await request.patch(
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${terraformSecrets[key].id}`,
@@ -3193,7 +3198,8 @@ const syncSecretsTerraformCloud = async ({
id: terraformSecrets[key].id,
attributes: {
...terraformSecrets[key],
value: secrets[key]?.value
value: secrets[key]?.value,
sensitive: true
}
}
},

View File

@@ -9,6 +9,7 @@ import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
import { deleteIntegrationSecrets } from "../integration-auth/integration-delete-secret";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretQueueFactory } from "../secret/secret-queue";
@@ -237,6 +238,46 @@ export const integrationServiceFactory = ({
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 ({
actorId,
id,
@@ -329,6 +370,7 @@ export const integrationServiceFactory = ({
deleteIntegration,
listIntegrationByProject,
getIntegration,
getIntegrationAWSIamRole,
syncIntegration
};
};

View File

@@ -280,7 +280,12 @@ export const projectMembershipServiceFactory = ({
// validate custom roles input
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);
if (hasCustomRole) {

View File

@@ -191,6 +191,10 @@ export const projectDALFactory = (db: TDbClient) => {
return project;
} catch (error) {
if (error instanceof NotFoundError) {
throw error;
}
throw new DatabaseError({ error, name: "Find all projects" });
}
};
@@ -240,6 +244,10 @@ export const projectDALFactory = (db: TDbClient) => {
return project;
} catch (error) {
if (error instanceof NotFoundError || error instanceof UnauthorizedError) {
throw error;
}
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" });
} catch (error) {
if (error instanceof BadRequestError) {
if (error instanceof BadRequestError || error instanceof NotFoundError || error instanceof UnauthorizedError) {
throw error;
}
throw new DatabaseError({ error, name: `Failed to find project by ${filter.type}` });

View File

@@ -285,11 +285,14 @@ export const projectQueueFactory = ({
if (!orgMembership) {
// This can happen. Since we don't remove project memberships and project keys when a user is removed from an org, this is a valid case.
logger.info("User is not in organization", {
userId: key.receiverId,
orgId: project.orgId,
projectId: project.id
});
logger.info(
{
userId: key.receiverId,
orgId: project.orgId,
projectId: project.id
},
"User is not in organization"
);
// eslint-disable-next-line no-continue
continue;
}
@@ -551,10 +554,10 @@ export const projectQueueFactory = ({
.catch(() => [null]);
if (!project) {
logger.error("Failed to upgrade project, because no project was found", data);
logger.error(data, "Failed to upgrade project, because no project was found");
} else {
await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.Failed);
logger.error("Failed to upgrade project", err, {
logger.error(err, "Failed to upgrade project", {
extra: {
project,
jobData: data

View File

@@ -149,6 +149,7 @@ export const projectServiceFactory = ({
actorOrgId,
actorAuthMethod,
workspaceName,
workspaceDescription,
slug: projectSlug,
kmsKeyId,
tx: trx,
@@ -206,6 +207,7 @@ export const projectServiceFactory = ({
const project = await projectDAL.create(
{
name: workspaceName,
description: workspaceDescription,
orgId: organization.id,
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
kmsSecretManagerKeyId: kmsKeyId,
@@ -496,6 +498,7 @@ export const projectServiceFactory = ({
const updatedProject = await projectDAL.updateById(project.id, {
name: update.name,
description: update.description,
autoCapitalization: update.autoCapitalization
});
return updatedProject;

View File

@@ -29,6 +29,7 @@ export type TCreateProjectDTO = {
actorId: string;
actorOrgId?: string;
workspaceName: string;
workspaceDescription?: string;
slug?: string;
kmsKeyId?: string;
createDefaultEnvs?: boolean;
@@ -69,6 +70,7 @@ export type TUpdateProjectDTO = {
filter: Filter;
update: {
name?: string;
description?: string;
autoCapitalization?: boolean;
};
} & Omit<TProjectPermission, "projectId">;

View File

@@ -142,7 +142,7 @@ export const fnTriggerWebhook = async ({
!isDisabled && picomatch.isMatch(secretPath, hookSecretPath, { strictSlashes: false })
);
if (!toBeTriggeredHooks.length) return;
logger.info("Secret webhook job started", { environment, secretPath, projectId });
logger.info({ environment, secretPath, projectId }, "Secret webhook job started");
const project = await projectDAL.findById(projectId);
const webhooksTriggered = await Promise.allSettled(
toBeTriggeredHooks.map((hook) =>
@@ -195,5 +195,5 @@ export const fnTriggerWebhook = async ({
);
}
});
logger.info("Secret webhook job ended", { environment, secretPath, projectId });
logger.info({ environment, secretPath, projectId }, "Secret webhook job ended");
};

View File

@@ -111,7 +111,7 @@ var exportCmd = &cobra.Command{
accessToken = token.Token
} else {
log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details")
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
util.HandleError(err)
}

View File

@@ -41,7 +41,7 @@ var initCmd = &cobra.Command{
}
}
userCreds, err := util.GetCurrentLoggedInUserDetails()
userCreds, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
util.HandleError(err, "Unable to get your login details")
}

View File

@@ -154,6 +154,8 @@ var loginCmd = &cobra.Command{
DisableFlagsInUseLine: true,
Run: func(cmd *cobra.Command, args []string) {
presetDomain := config.INFISICAL_URL
clearSelfHostedDomains, err := cmd.Flags().GetBool("clear-domains")
if err != nil {
util.HandleError(err)
@@ -198,7 +200,7 @@ var loginCmd = &cobra.Command{
// standalone user auth
if loginMethod == "user" {
currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
// if the key can't be found or there is an error getting current credentials from key ring, allow them to override
if err != nil && (strings.Contains(err.Error(), "we couldn't find your logged in details")) {
log.Debug().Err(err)
@@ -216,11 +218,19 @@ var loginCmd = &cobra.Command{
return
}
}
usePresetDomain, err := usePresetDomain(presetDomain)
if err != nil {
util.HandleError(err)
}
//override domain
domainQuery := true
if config.INFISICAL_URL_MANUAL_OVERRIDE != "" &&
config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_EU_URL) &&
config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL) {
config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL) &&
!usePresetDomain {
overrideDomain, err := DomainOverridePrompt()
if err != nil {
util.HandleError(err)
@@ -228,7 +238,7 @@ var loginCmd = &cobra.Command{
//if not override set INFISICAL_URL to exported var
//set domainQuery to false
if !overrideDomain {
if !overrideDomain && !usePresetDomain {
domainQuery = false
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL_MANUAL_OVERRIDE)
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", strings.TrimSuffix(config.INFISICAL_URL, "/api"))
@@ -237,7 +247,7 @@ var loginCmd = &cobra.Command{
}
//prompt user to select domain between Infisical cloud and self-hosting
if domainQuery {
if domainQuery && !usePresetDomain {
err = askForDomain()
if err != nil {
util.HandleError(err, "Unable to parse domain url")
@@ -526,6 +536,45 @@ func DomainOverridePrompt() (bool, error) {
return selectedOption == OVERRIDE, err
}
func usePresetDomain(presetDomain string) (bool, error) {
infisicalConfig, err := util.GetConfigFile()
if err != nil {
return false, fmt.Errorf("askForDomain: unable to get config file because [err=%s]", err)
}
preconfiguredUrl := strings.TrimSuffix(presetDomain, "/api")
if preconfiguredUrl != "" && preconfiguredUrl != util.INFISICAL_DEFAULT_US_URL && preconfiguredUrl != util.INFISICAL_DEFAULT_EU_URL {
parsedDomain := strings.TrimSuffix(strings.Trim(preconfiguredUrl, "/"), "/api")
_, err := url.ParseRequestURI(parsedDomain)
if err != nil {
return false, errors.New(fmt.Sprintf("Invalid domain URL: '%s'", parsedDomain))
}
config.INFISICAL_URL = fmt.Sprintf("%s/api", parsedDomain)
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", parsedDomain)
if !slices.Contains(infisicalConfig.Domains, parsedDomain) {
infisicalConfig.Domains = append(infisicalConfig.Domains, parsedDomain)
err = util.WriteConfigFile(&infisicalConfig)
if err != nil {
return false, fmt.Errorf("askForDomain: unable to write domains to config file because [err=%s]", err)
}
}
whilte := color.New(color.FgGreen)
boldWhite := whilte.Add(color.Bold)
time.Sleep(time.Second * 1)
boldWhite.Printf("[INFO] Using domain '%s' from domain flag or INFISICAL_API_URL environment variable\n", parsedDomain)
return true, nil
}
return false, nil
}
func askForDomain() error {
// query user to choose between Infisical cloud or self-hosting

View File

@@ -54,7 +54,7 @@ func init() {
util.CheckForUpdate()
}
loggedInDetails, err := util.GetCurrentLoggedInUserDetails()
loggedInDetails, err := util.GetCurrentLoggedInUserDetails(false)
if !silent && err == nil && loggedInDetails.IsUserLoggedIn && !loggedInDetails.LoginExpired {
token, err := util.GetInfisicalToken(cmd)

View File

@@ -194,7 +194,7 @@ var secretsSetCmd = &cobra.Command{
projectId = workspaceFile.WorkspaceId
}
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
util.HandleError(err, "unable to authenticate [err=%v]")
}
@@ -278,7 +278,7 @@ var secretsDeleteCmd = &cobra.Command{
util.RequireLogin()
util.RequireLocalWorkspaceFile()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
util.HandleError(err, "Unable to authenticate")
}

View File

@@ -41,7 +41,7 @@ var tokensCreateCmd = &cobra.Command{
},
Run: func(cmd *cobra.Command, args []string) {
// get plain text workspace key
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
util.HandleError(err, "Unable to retrieve your logged in your details. Please login in then try again")

View File

@@ -55,7 +55,7 @@ func GetUserCredsFromKeyRing(userEmail string) (credentials models.UserCredentia
return userCredentials, err
}
func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) {
func GetCurrentLoggedInUserDetails(setConfigVariables bool) (LoggedInUserDetails, error) {
if ConfigFileExists() {
configFile, err := GetConfigFile()
if err != nil {
@@ -75,18 +75,20 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) {
}
}
if setConfigVariables {
config.INFISICAL_URL_MANUAL_OVERRIDE = config.INFISICAL_URL
//configFile.LoggedInUserDomain
//if not empty set as infisical url
if configFile.LoggedInUserDomain != "" {
config.INFISICAL_URL = AppendAPIEndpoint(configFile.LoggedInUserDomain)
}
}
// check to to see if the JWT is still valid
httpClient := resty.New().
SetAuthToken(userCreds.JTWToken).
SetHeader("Accept", "application/json")
config.INFISICAL_URL_MANUAL_OVERRIDE = config.INFISICAL_URL
//configFile.LoggedInUserDomain
//if not empty set as infisical url
if configFile.LoggedInUserDomain != "" {
config.INFISICAL_URL = AppendAPIEndpoint(configFile.LoggedInUserDomain)
}
isAuthenticated := api.CallIsAuthenticated(httpClient)
// TODO: add refresh token
// if !isAuthenticated {

View File

@@ -20,7 +20,7 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder
log.Debug().Msg("GetAllFolders: Trying to fetch folders using logged in details")
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true)
if err != nil {
return nil, err
}
@@ -177,7 +177,7 @@ func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, er
if params.InfisicalToken == "" {
RequireLogin()
RequireLocalWorkspaceFile()
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true)
if err != nil {
return models.SingleFolder{}, err
@@ -224,7 +224,7 @@ func DeleteFolder(params models.DeleteFolderParameters) ([]models.SingleFolder,
RequireLogin()
RequireLocalWorkspaceFile()
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true)
if err != nil {
return nil, err

View File

@@ -246,7 +246,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details")
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true)
isConnected := ValidateInfisicalAPIConnection()
if isConnected {

View File

@@ -3,7 +3,7 @@ title: 'Install'
description: "Infisical's CLI is one of the best way to manage environments and secrets. Install it here"
---
The Infisical CLI is powerful command line tool that can be used to retrieve, modify, export and inject secrets into any process or application as environment variables.
The Infisical CLI is a powerful command line tool that can be used to retrieve, modify, export and inject secrets into any process or application as environment variables.
You can use it across various environments, whether it's local development, CI/CD, staging, or production.
## Installation

View File

@@ -69,7 +69,7 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<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
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@@ -66,7 +66,7 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<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
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the lease details and delete the lease ahead of its expiration time.
This will allow you to see the lease details and delete the lease ahead of its expiration time.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@@ -98,7 +98,7 @@ Click on Add assignments. Search for the application name you created and select
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<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
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@@ -39,7 +39,7 @@ The above configuration allows user creation and granting permissions.
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@@ -116,12 +116,12 @@ The above configuration allows user creation and granting permissions.
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the lease details and delete the lease ahead of its expiration time.
This will allow you to see the lease details and delete the lease ahead of its expiration time.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@@ -34,7 +34,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<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
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@@ -31,7 +31,7 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<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 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 path="Max TTL" type="string" required>

View File

@@ -30,7 +30,7 @@ Create a project scopped API Key with the required permission in your Mongo Atla
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<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
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@@ -31,7 +31,7 @@ Create a user with the required permission in your MongoDB instance. This user w
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<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
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@@ -28,7 +28,7 @@ Create a user with the required permission in your SQL instance. This user will
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<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
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete the lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete the lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@@ -27,7 +27,7 @@ Create a user with the required permission in your SQL instance. This user will
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<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
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@@ -27,7 +27,7 @@ Create a user with the required permission in your SQL instance. This user will
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<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
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@@ -28,7 +28,7 @@ Create a user with the required permission in your SQL instance. This user will
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<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
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete the lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete the lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@@ -28,7 +28,7 @@ The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credential
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<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
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@@ -27,7 +27,7 @@ Create a user with the required permission in your Redis instance. This user wil
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<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
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@@ -30,7 +30,7 @@ The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database c
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<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
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the lease details and delete the lease ahead of its expiration time.
This will allow you to see the lease details and delete the lease ahead of its expiration time.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@@ -109,7 +109,7 @@ Infisical's Snowflake dynamic secrets allow you to generate Snowflake user crede
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the lease details and delete the lease ahead of its expiration time.
This will allow you to see the lease details and delete the lease ahead of its expiration time.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)

View 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">
![Add Dynamic Secret Button](/images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select TOTP">
![Dynamic Secret Modal](/images/platform/dynamic-secrets/dynamic-secret-modal-totp.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<ParamField path="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>
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-url.png)
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-manual.png)
</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.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png)
Once you click the `Generate` button, a new secret lease will be generated and the TOTP will be shown to you.
![Provision Lease](/images/platform/dynamic-secrets/totp-lease-value.png)
</Step>
</Steps>

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

View File

@@ -1,8 +1,9 @@
---
title: "Terraform"
title: "Terraform Provider"
description: "Learn how to fetch Secrets From Infisical With Terraform."
url: "https://registry.terraform.io/providers/Infisical/infisical/latest/docs"
---
{/*
This guide provides step-by-step guidance on how to fetch secrets from Infisical using Terraform.
## Prerequisites
@@ -98,4 +99,4 @@ Terraform will now fetch your secrets from Infisical and display them as output
## Conclusion
You have now successfully set up and used the Infisical provider with Terraform to fetch secrets. For more information, visit the [Infisical documentation](https://registry.terraform.io/providers/Infisical/infisical/latest/docs).
You have now successfully set up and used the Infisical provider with Terraform to fetch secrets. For more information, visit the [Infisical documentation](https://registry.terraform.io/providers/Infisical/infisical/latest/docs). */}

View File

@@ -94,7 +94,7 @@ spec:
projectSlug: new-ob-em
envSlug: dev # "dev", "staging", "prod", etc..
secretsPath: "/" # Root is "/"
recursive: true # Wether or not to use recursive mode (Fetches all secrets in an environment from a given secret path, and all folders inside the path) / defaults to false
recursive: true # Whether or not to use recursive mode (Fetches all secrets in an environment from a given secret path, and all folders inside the path) / defaults to false
credentialsRef:
secretName: universal-auth-credentials
secretNamespace: default

View File

@@ -189,7 +189,8 @@
"documentation/platform/dynamic-secrets/azure-entra-id",
"documentation/platform/dynamic-secrets/ldap",
"documentation/platform/dynamic-secrets/sap-hana",
"documentation/platform/dynamic-secrets/snowflake"
"documentation/platform/dynamic-secrets/snowflake",
"documentation/platform/dynamic-secrets/totp"
]
},
"documentation/platform/project-templates",

View File

@@ -4,13 +4,15 @@ import { Id, toast, ToastContainer, ToastOptions, TypeOptions } from "react-toas
export type TNotification = {
title?: string;
text: ReactNode;
children?: ReactNode;
};
export const NotificationContent = ({ title, text }: TNotification) => {
export const NotificationContent = ({ title, text, children }: TNotification) => {
return (
<div className="msg-container">
{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>
);
};
@@ -23,7 +25,13 @@ export const createNotification = (
position: "bottom-right",
...toastProps,
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" }}
/>
);

View File

@@ -0,0 +1,328 @@
import { FC, useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { useRouter } from "next/router";
import { faInfoCircle } 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 { OrgPermissionCan } from "@app/components/permissions";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button,
Checkbox,
FormControl,
Input,
Modal,
ModalClose,
ModalContent,
Select,
SelectItem,
TextArea
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useOrgPermission,
useSubscription,
useUser
} from "@app/context";
import {
fetchOrgUsers,
useAddUserToWsNonE2EE,
useCreateWorkspace,
useGetExternalKmsList
} from "@app/hooks/api";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
const formSchema = z.object({
name: z.string().trim().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
description: z
.string()
.trim()
.max(256, "Description too long, max length is 256 characters")
.optional(),
addMembers: z.boolean(),
kmsKeyId: z.string(),
template: z.string()
});
type TAddProjectFormData = z.infer<typeof formSchema>;
interface NewProjectModalProps {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
}
type NewProjectFormProps = Pick<NewProjectModalProps, "onOpenChange">;
const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
const router = useRouter();
const { currentOrg } = useOrganization();
const { permission } = useOrgPermission();
const { user } = useUser();
const createWs = useCreateWorkspace();
const addUsersToProject = useAddUserToWsNonE2EE();
const { subscription } = useSubscription();
const canReadProjectTemplates = permission.can(
OrgPermissionActions.Read,
OrgPermissionSubjects.ProjectTemplates
);
const { data: projectTemplates = [] } = useListProjectTemplates({
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
});
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!, {
enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms)
});
const {
control,
handleSubmit,
reset,
formState: { isSubmitting, errors }
} = useForm<TAddProjectFormData>({
resolver: zodResolver(formSchema),
defaultValues: {
kmsKeyId: INTERNAL_KMS_KEY_ID,
template: InfisicalProjectTemplate.Default
}
});
useEffect(() => {
if (Object.keys(errors).length > 0) {
console.log("Current form errors:", errors);
}
}, [errors]);
const onCreateProject = async ({
name,
description,
addMembers,
kmsKeyId,
template
}: TAddProjectFormData) => {
// type check
if (!currentOrg) return;
if (!user) return;
try {
const {
data: {
project: { id: newProjectId }
}
} = await createWs.mutateAsync({
projectName: name,
projectDescription: description,
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
template
});
if (addMembers) {
const orgUsers = await fetchOrgUsers(currentOrg.id);
await addUsersToProject.mutateAsync({
usernames: orgUsers
.filter(
(member) => member.user.username !== user.username && member.status === "accepted"
)
.map((member) => member.user.username),
projectId: newProjectId,
orgId: currentOrg.id
});
}
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
await new Promise((resolve) => setTimeout(resolve, 2_000));
createNotification({ text: "Project created", type: "success" });
reset();
onOpenChange(false);
router.push(`/project/${newProjectId}/secrets/overview`);
} catch (err) {
console.error(err);
createNotification({ text: "Failed to create project", type: "error" });
}
};
const onSubmit = handleSubmit((data) => {
return onCreateProject(data);
});
return (
<form onSubmit={onSubmit}>
<div className="flex flex-col gap-2">
<Controller
control={control}
name="name"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project Name"
isError={Boolean(error)}
errorText={error?.message}
className="flex-1"
>
<Input {...field} placeholder="Type your project name" />
</FormControl>
)}
/>
<Controller
control={control}
name="description"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project Description"
isError={Boolean(error)}
isOptional
errorText={error?.message}
className="flex-1"
>
<TextArea
placeholder="Project description"
{...field}
rows={3}
className="thin-scrollbar w-full !resize-none bg-mineshaft-900"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="template"
render={({ field: { value, onChange } }) => (
<OrgPermissionCan
I={OrgPermissionActions.Read}
a={OrgPermissionSubjects.ProjectTemplates}
>
{(isAllowed) => (
<FormControl
label="Project Template"
icon={<FontAwesomeIcon icon={faInfoCircle} size="sm" />}
tooltipText={
<>
<p>
Create this project from a template to provision it with custom environments
and roles.
</p>
{subscription && !subscription.projectTemplates && (
<p className="pt-2">Project templates are a paid feature.</p>
)}
</>
}
>
<Select
defaultValue={InfisicalProjectTemplate.Default}
placeholder={InfisicalProjectTemplate.Default}
isDisabled={!isAllowed || !subscription?.projectTemplates}
value={value}
onValueChange={onChange}
className="w-full"
>
{projectTemplates.length
? projectTemplates.map((template) => (
<SelectItem key={template.id} value={template.name}>
{template.name}
</SelectItem>
))
: Object.values(InfisicalProjectTemplate).map((template) => (
<SelectItem key={template} value={template}>
{template}
</SelectItem>
))}
</Select>
</FormControl>
)}
</OrgPermissionCan>
)}
/>
</div>
<div className="mt-4 pl-1">
<Controller
control={control}
name="addMembers"
defaultValue={false}
render={({ field: { onBlur, value, onChange } }) => (
<OrgPermissionCan I={OrgPermissionActions.Read} a={OrgPermissionSubjects.Member}>
{(isAllowed) => (
<div>
<Checkbox
id="add-project-layout"
isChecked={value}
onCheckedChange={onChange}
isDisabled={!isAllowed}
onBlur={onBlur}
>
Add all members of my organization to this project
</Checkbox>
</div>
)}
</OrgPermissionCan>
)}
/>
</div>
<div className="mt-14 flex">
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="advance-settings" className="data-[state=open]:border-none">
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
<div className="order-1 ml-3">Advanced Settings</div>
</AccordionTrigger>
<AccordionContent>
<Controller
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error)} label="KMS">
<Select
{...field}
onValueChange={(e) => {
onChange(e);
}}
className="mb-12 w-full bg-mineshaft-600"
>
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
Default Infisical KMS
</SelectItem>
{externalKmsList?.map((kms) => (
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
{kms.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
control={control}
name="kmsKeyId"
/>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="absolute right-0 bottom-0 mr-6 mb-6 flex items-start justify-end">
<ModalClose>
<Button colorSchema="secondary" variant="plain" className="py-2">
Cancel
</Button>
</ModalClose>
<Button isDisabled={isSubmitting} isLoading={isSubmitting} className="ml-4" type="submit">
Create Project
</Button>
</div>
</div>
</form>
);
};
export const NewProjectModal: FC<NewProjectModalProps> = ({ isOpen, onOpenChange }) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title="Create a new project"
subTitle="This project will contain your secrets and configurations."
>
<NewProjectForm onOpenChange={onOpenChange} />
</ModalContent>
</Modal>
);
};

View File

@@ -0,0 +1 @@
export { NewProjectModal } from "./NewProjectModal";

View File

@@ -33,6 +33,15 @@ export enum PermissionConditionOperators {
$GLOB = "$glob"
}
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
[PermissionConditionOperators.$EQ]: "equal to",
[PermissionConditionOperators.$IN]: "contains",
[PermissionConditionOperators.$ALL]: "contains all",
[PermissionConditionOperators.$NEQ]: "not equal to",
[PermissionConditionOperators.$GLOB]: "matches glob pattern",
[PermissionConditionOperators.$REGEX]: "matches regex pattern"
};
export type TPermissionConditionOperators = {
[PermissionConditionOperators.$IN]: string[];
[PermissionConditionOperators.$ALL]: string[];

View File

@@ -9,7 +9,7 @@ export type TGetAuditLogsFilter = {
eventMetadata?: Record<string, string>;
actorType?: ActorType;
projectId?: string;
actorId?: string; // user ID format
actor?: string; // user ID format
startDate?: Date;
endDate?: Date;
limit: number;

View File

@@ -28,7 +28,8 @@ export enum DynamicSecretProviders {
AzureEntraId = "azure-entra-id",
Ldap = "ldap",
SapHana = "sap-hana",
Snowflake = "snowflake"
Snowflake = "snowflake",
Totp = "totp"
}
export enum SqlProviders {
@@ -230,6 +231,21 @@ export type TDynamicSecretProvider =
revocationStatement: string;
renewStatement?: string;
};
}
| {
type: DynamicSecretProviders.Totp;
inputs:
| {
configType: "url";
url: string;
}
| {
configType: "manual";
secret: string;
period?: number;
algorithm?: string;
digits?: number;
};
};
export type TCreateDynamicSecretDTO = {
projectSlug: string;

View File

@@ -57,6 +57,9 @@ export type TIntegration = {
shouldMaskSecrets?: boolean;
shouldProtectSecrets?: boolean;
shouldEnableDelete?: boolean;
awsIamRole?: string;
region?: string;
};
};

View File

@@ -1,3 +1,4 @@
import { PureAbility } from "@casl/ability";
import { ZodIssue } from "zod";
export type { TAccessApprovalPolicy } from "./accessApproval/types";
@@ -33,9 +34,9 @@ export type {
CreateWorkspaceDTO,
DeleteEnvironmentDTO,
DeleteWorkspaceDTO,
RenameWorkspaceDTO,
ToggleAutoCapitalizationDTO,
UpdateEnvironmentDTO,
UpdateProjectDTO,
Workspace,
WorkspaceEnv,
WorkspaceTag
@@ -50,12 +51,26 @@ export enum ApiErrorTypes {
export type TApiErrors =
| {
requestId: string;
error: ApiErrorTypes.ValidationError;
message: ZodIssue[];
statusCode: 401;
}
| {
requestId: string;
error: ApiErrorTypes.UnauthorizedError;
message: string;
statusCode: 401;
}
| {
requestId: string;
error: ApiErrorTypes.ForbiddenError;
message: string;
details: PureAbility["rules"];
statusCode: 403;
}
| { error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 401 }
| {
requestId: string;
statusCode: 400;
message: string;
error: ApiErrorTypes.BadRequestError;

View File

@@ -33,10 +33,11 @@ export {
useListWorkspacePkiAlerts,
useListWorkspacePkiCollections,
useNameWorkspaceSecrets,
useRenameWorkspace,
useToggleAutoCapitalization,
useUpdateIdentityWorkspaceRole,
useUpdateProject,
useUpdateUserWorkspaceRole,
useUpdateWsEnvironment,
useUpgradeProject} from "./queries";
useUpgradeProject
} from "./queries";
export { workspaceKeys } from "./query-keys";

View File

@@ -26,7 +26,6 @@ import {
DeleteWorkspaceDTO,
NameWorkspaceSecretsDTO,
ProjectIdentityOrderBy,
RenameWorkspaceDTO,
TGetUpgradeProjectStatusDTO,
TListProjectIdentitiesDTO,
ToggleAutoCapitalizationDTO,
@@ -35,6 +34,7 @@ import {
UpdateAuditLogsRetentionDTO,
UpdateEnvironmentDTO,
UpdatePitVersionLimitDTO,
UpdateProjectDTO,
Workspace
} from "./types";
@@ -208,19 +208,26 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
export const createWorkspace = ({
projectName,
projectDescription,
kmsKeyId,
template
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
return apiRequest.post("/api/v2/workspace", { projectName, kmsKeyId, template });
return apiRequest.post("/api/v2/workspace", {
projectName,
projectDescription,
kmsKeyId,
template
});
};
export const useCreateWorkspace = () => {
const queryClient = useQueryClient();
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
mutationFn: async ({ projectName, kmsKeyId, template }) =>
mutationFn: async ({ projectName, projectDescription, kmsKeyId, template }) =>
createWorkspace({
projectName,
projectDescription,
kmsKeyId,
template
}),
@@ -230,12 +237,15 @@ export const useCreateWorkspace = () => {
});
};
export const useRenameWorkspace = () => {
export const useUpdateProject = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, RenameWorkspaceDTO>({
mutationFn: ({ workspaceID, newWorkspaceName }) => {
return apiRequest.post(`/api/v1/workspace/${workspaceID}/name`, { name: newWorkspaceName });
return useMutation<{}, {}, UpdateProjectDTO>({
mutationFn: ({ projectID, newProjectName, newProjectDescription }) => {
return apiRequest.patch(`/api/v1/workspace/${projectID}`, {
name: newProjectName,
description: newProjectDescription
});
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);

View File

@@ -16,6 +16,7 @@ export type Workspace = {
__v: number;
id: string;
name: string;
description?: string;
orgId: string;
version: ProjectVersion;
upgradeStatus: string | null;
@@ -26,7 +27,6 @@ export type Workspace = {
auditLogsRetentionDays: number;
slug: string;
createdAt: string;
roles?: TProjectRole[];
};
@@ -56,11 +56,17 @@ export type TGetUpgradeProjectStatusDTO = {
// mutation dto
export type CreateWorkspaceDTO = {
projectName: string;
projectDescription?: string;
kmsKeyId?: string;
template?: string;
};
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
export type UpdateProjectDTO = {
projectID: string;
newProjectName: string;
newProjectDescription?: string;
};
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
export type UpdateAuditLogsRetentionDTO = { projectSlug: string; auditLogsRetentionDays: number };
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };

View File

@@ -3,9 +3,16 @@ import { useState } from "react";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useDebounce } from "@app/hooks/useDebounce";
export const usePagination = <T extends string>(initialOrderBy: T) => {
export const usePagination = <T extends string>(
initialOrderBy: T,
{
initPerPage = 100
}: {
initPerPage?: number;
} = {}
) => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(100);
const [perPage, setPerPage] = useState(initPerPage);
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
const [orderBy, setOrderBy] = useState<T>(initialOrderBy);
const [search, setSearch] = useState("");
@@ -26,6 +33,10 @@ export const usePagination = <T extends string>(initialOrderBy: T) => {
search,
setSearch,
orderBy,
setOrderBy
setOrderBy,
toggleOrderDirection: () =>
setOrderDirection((prev) =>
prev === OrderByDirection.DESC ? OrderByDirection.ASC : OrderByDirection.DESC
)
};
};

View File

@@ -6,7 +6,6 @@
/* eslint-disable func-names */
import { useEffect, useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -21,66 +20,48 @@ import {
faEnvelope,
faInfinity,
faInfo,
faInfoCircle,
faMobile,
faPlus,
faQuestion,
faStar as faSolidStar
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
import { twMerge } from "tailwind-merge";
import * as yup from "yup";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button,
Checkbox,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
FormControl,
Input,
Menu,
MenuItem,
Modal,
ModalContent,
Select,
SelectItem,
UpgradePlanModal
} from "@app/components/v2";
import { NewProjectModal } from "@app/components/v2/projects/NewProjectModal";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useOrgPermission,
useSubscription,
useUser,
useWorkspace
} from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import {
fetchOrgUsers,
useAddUserToWsNonE2EE,
useCreateWorkspace,
useGetAccessRequestsCount,
useGetExternalKmsList,
useGetOrgTrialUrl,
useGetSecretApprovalRequestCount,
useLogoutUser,
useSelectOrganization
} from "@app/hooks/api";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
import { Workspace } from "@app/hooks/api/types";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
@@ -119,20 +100,6 @@ const supportOptions = [
]
];
const formSchema = yup.object({
name: yup
.string()
.required()
.label("Project Name")
.trim()
.max(64, "Too long, maximum length is 64 characters"),
addMembers: yup.bool().required().label("Add Members"),
kmsKeyId: yup.string().label("KMS Key ID"),
template: yup.string().label("Project Template Name")
});
type TAddProjectFormData = yup.InferType<typeof formSchema>;
export const AppLayout = ({ children }: LayoutProps) => {
const router = useRouter();
@@ -165,10 +132,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
const { permission } = useOrgPermission();
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!, {
enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms)
});
const pendingRequestsCount = useMemo(() => {
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
@@ -178,27 +141,13 @@ export const AppLayout = ({ children }: LayoutProps) => {
? subscription.workspacesUsed < subscription.workspaceLimit
: true;
const createWs = useCreateWorkspace();
const addUsersToProject = useAddUserToWsNonE2EE();
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"addNewWs",
"upgradePlan",
"createOrg"
] as const);
const {
control,
formState: { isSubmitting },
reset,
handleSubmit
} = useForm<TAddProjectFormData>({
resolver: yupResolver(formSchema),
defaultValues: {
kmsKeyId: INTERNAL_KMS_KEY_ID
}
});
const { t } = useTranslation();
@@ -281,58 +230,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
putUserInOrg();
}, [router.query.id]);
const canReadProjectTemplates = permission.can(
OrgPermissionActions.Read,
OrgPermissionSubjects.ProjectTemplates
);
const { data: projectTemplates = [] } = useListProjectTemplates({
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
});
const onCreateProject = async ({ name, addMembers, kmsKeyId, template }: TAddProjectFormData) => {
// type check
if (!currentOrg) return;
if (!user) return;
try {
const {
data: {
project: { id: newProjectId }
}
} = await createWs.mutateAsync({
projectName: name,
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
template
});
if (addMembers) {
const orgUsers = await fetchOrgUsers(currentOrg.id);
await addUsersToProject.mutateAsync({
usernames: orgUsers
.filter(
(member) => member.user.username !== user.username && member.status === "accepted"
)
.map((member) => member.user.username),
projectId: newProjectId,
orgId: currentOrg.id
});
}
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
await new Promise((resolve) => setTimeout(resolve, 2_000));
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
await new Promise((resolve) => setTimeout(resolve, 2_000));
createNotification({ text: "Project created", type: "success" });
handlePopUpClose("addNewWs");
router.push(`/project/${newProjectId}/secrets/overview`);
} catch (err) {
console.error(err);
createNotification({ text: "Failed to create project", type: "error" });
}
};
const addProjectToFavorites = async (projectId: string) => {
try {
if (currentOrg?.id) {
@@ -916,176 +813,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
</div>
</nav>
</aside>
<Modal
<NewProjectModal
isOpen={popUp.addNewWs.isOpen}
onOpenChange={(isModalOpen) => {
handlePopUpToggle("addNewWs", isModalOpen);
reset();
}}
>
<ModalContent
title="Create a new project"
subTitle="This project will contain your secrets and configurations."
>
<form onSubmit={handleSubmit(onCreateProject)}>
<div className="flex gap-2">
<Controller
control={control}
name="name"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project Name"
isError={Boolean(error)}
errorText={error?.message}
className="flex-1"
>
<Input {...field} placeholder="Type your project name" />
</FormControl>
)}
/>
<Controller
control={control}
name="template"
render={({ field: { value, onChange } }) => (
<OrgPermissionCan
I={OrgPermissionActions.Read}
a={OrgPermissionSubjects.ProjectTemplates}
>
{(isAllowed) => (
<FormControl
label="Project Template"
icon={<FontAwesomeIcon icon={faInfoCircle} size="sm" />}
tooltipText={
<>
<p>
Create this project from a template to provision it with custom
environments and roles.
</p>
{subscription && !subscription.projectTemplates && (
<p className="pt-2">Project templates are a paid feature.</p>
)}
</>
}
>
<Select
defaultValue={InfisicalProjectTemplate.Default}
placeholder={InfisicalProjectTemplate.Default}
isDisabled={!isAllowed || !subscription?.projectTemplates}
value={value}
onValueChange={onChange}
className="w-44"
>
{projectTemplates.length
? projectTemplates.map((template) => (
<SelectItem key={template.id} value={template.name}>
{template.name}
</SelectItem>
))
: Object.values(InfisicalProjectTemplate).map((template) => (
<SelectItem key={template} value={template}>
{template}
</SelectItem>
))}
</Select>
</FormControl>
)}
</OrgPermissionCan>
)}
/>
</div>
<div className="mt-4 pl-1">
<Controller
control={control}
name="addMembers"
defaultValue={false}
render={({ field: { onBlur, value, onChange } }) => (
<OrgPermissionCan
I={OrgPermissionActions.Read}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<div>
<Checkbox
id="add-project-layout"
isChecked={value}
onCheckedChange={onChange}
isDisabled={!isAllowed}
onBlur={onBlur}
>
Add all members of my organization to this project
</Checkbox>
</div>
)}
</OrgPermissionCan>
)}
/>
</div>
<div className="mt-14 flex">
<Accordion type="single" collapsible className="w-full">
<AccordionItem
value="advance-settings"
className="data-[state=open]:border-none"
>
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
<div className="order-1 ml-3">Advanced Settings</div>
</AccordionTrigger>
<AccordionContent>
<Controller
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
label="KMS"
>
<Select
{...field}
onValueChange={(e) => {
onChange(e);
}}
className="mb-12 w-full bg-mineshaft-600"
>
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
Default Infisical KMS
</SelectItem>
{externalKmsList?.map((kms) => (
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
{kms.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
control={control}
name="kmsKeyId"
/>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="absolute right-0 bottom-0 mr-6 mb-6 flex items-start justify-end">
<Button
key="layout-cancel-create-project"
onClick={() => handlePopUpClose("addNewWs")}
colorSchema="secondary"
variant="plain"
className="py-2"
>
Cancel
</Button>
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className="ml-4"
type="submit"
>
Create Project
</Button>
</div>
</div>
</form>
</ModalContent>
</Modal>
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}

Some files were not shown because too many files have changed in this diff Show More