From 16d3bbb67ad84456117123d48ae5e4db3b11873d Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 10 Mar 2025 23:46:04 -0400 Subject: [PATCH 1/5] Add password requirements to dyanmic secret This will add a new accordion to add custom requirements for the generated password for DB drivers. We can use this pattern for other dynamic secrets too --- .../dynamic-secret/providers/models.ts | 38 +++ .../dynamic-secret/providers/sql-database.ts | 90 ++++++- frontend/src/hooks/api/dynamicSecret/types.ts | 1 + .../SqlDatabaseInputForm.tsx | 247 +++++++++++++++++- .../EditDynamicSecretSqlProviderForm.tsx | 243 ++++++++++++++++- 5 files changed, 604 insertions(+), 15 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 621c3c631..93af4eabc 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -1,5 +1,22 @@ import { z } from "zod"; +export type PasswordRequirements = { + minLength: number; + maxLength: number; + required: { + lowercase: number; + uppercase: number; + digits: number; + symbols: number; + }; + allowedCharacters?: { + lowercase?: string; + uppercase?: string; + digits?: string; + symbols?: string; + }; +}; + export enum SqlProviders { Postgres = "postgres", MySQL = "mysql2", @@ -100,6 +117,27 @@ export const DynamicSecretSqlDBSchema = z.object({ database: z.string().trim(), username: z.string().trim(), password: z.string().trim(), + passwordRequirements: z + .object({ + minLength: z.number().min(1).max(100), + maxLength: z.number().min(1).max(100), + required: z.object({ + lowercase: z.number().min(0), + uppercase: z.number().min(0), + digits: z.number().min(0), + symbols: z.number().min(0) + }), + allowedCharacters: z + .object({ + lowercase: z.string().optional(), + uppercase: z.string().optional(), + digits: z.string().optional(), + symbols: z.string().optional() + }) + .optional() + }) + .optional() + .describe('Password generation requirements'), creationStatement: z.string().trim(), revocationStatement: z.string().trim(), renewStatement: z.string().trim().optional(), diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index 68089ea4c..7d76db697 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -8,16 +8,92 @@ import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TGatewayServiceFactory } from "../../gateway/gateway-service"; import { verifyHostInputValidity } from "../dynamic-secret-fns"; -import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models"; +import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models"; const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; -const generatePassword = (provider: SqlProviders) => { - // oracle has limit of 48 password length - const size = provider === SqlProviders.Oracle ? 30 : 48; +const DEFAULT_PASSWORD_REQUIREMENTS = { + minLength: 48, + maxLength: 48, + required: { + lowercase: 1, + uppercase: 1, + digits: 1, + symbols: 0 + }, + allowedCharacters: { + lowercase: 'abcdefghijklmnopqrstuvwxyz', + uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + digits: '0123456789', + symbols: '-_.~!*' + } +}; - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; - return customAlphabet(charset, 48)(size); +const ORACLE_PASSWORD_REQUIREMENTS = { + ...DEFAULT_PASSWORD_REQUIREMENTS, + minLength: 30, + maxLength: 30 +}; + +const generatePassword = (provider: SqlProviders, requirements?: PasswordRequirements) => { + const defaultReqs = provider === SqlProviders.Oracle ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS; + const finalReqs = requirements || defaultReqs; + + try { + const { minLength, maxLength, required, allowedCharacters = {} } = finalReqs; + + const chars = { + lowercase: allowedCharacters.lowercase || 'abcdefghijklmnopqrstuvwxyz', + uppercase: allowedCharacters.uppercase || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + digits: allowedCharacters.digits || '0123456789', + symbols: allowedCharacters.symbols || '-_.~!*' + }; + + const parts: string[] = []; + + if (required.lowercase > 0) { + parts.push(...Array(required.lowercase).fill(0).map(() => + chars.lowercase[Math.floor(Math.random() * chars.lowercase.length)] + )); + } + + if (required.uppercase > 0) { + parts.push(...Array(required.uppercase).fill(0).map(() => + chars.uppercase[Math.floor(Math.random() * chars.uppercase.length)] + )); + } + + if (required.digits > 0) { + parts.push(...Array(required.digits).fill(0).map(() => + chars.digits[Math.floor(Math.random() * chars.digits.length)] + )); + } + + if (required.symbols > 0) { + parts.push(...Array(required.symbols).fill(0).map(() => + chars.symbols[Math.floor(Math.random() * chars.symbols.length)] + )); + } + + const requiredTotal = Object.values(required).reduce((a, b) => a + b, 0); + const remainingLength = Math.max(minLength - requiredTotal, 0); + + const allChars = Object.values(chars).join(''); + parts.push(...Array(remainingLength).fill(0).map(() => + allChars[Math.floor(Math.random() * allChars.length)] + )); + + // shuffle the array to mix up the characters + for (let i = parts.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [parts[i], parts[j]] = [parts[j], parts[i]]; + } + + return parts.join(''); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Unknown error'; + throw new Error(`Failed to generate password: ${message}`); + } }; const generateUsername = (provider: SqlProviders) => { @@ -115,7 +191,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) const create = async (inputs: unknown, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); const username = generateUsername(providerInputs.client); - const password = generatePassword(providerInputs.client); + const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements); const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => { const db = await $getClient({ ...providerInputs, port, host }); try { diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 7d8c6920a..154449d5b 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -50,6 +50,7 @@ export type TDynamicSecretProvider = database: string; username: string; password: string; + passwordRegex?: string; creationStatement: string; revocationStatement: string; renewStatement?: string; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx index a4962d10c..354f226f8 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx @@ -23,6 +23,25 @@ import { useWorkspace } from "@app/context"; import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types"; +const passwordRequirementsSchema = z.object({ + minLength: z.number().min(1).max(100), + maxLength: z.number().min(1).max(100), + required: z.object({ + lowercase: z.number().min(0), + uppercase: z.number().min(0), + digits: z.number().min(0), + symbols: z.number().min(0) + }), + allowedCharacters: z + .object({ + lowercase: z.string().optional(), + uppercase: z.string().optional(), + digits: z.string().optional(), + symbols: z.string().optional() + }) + .optional() +}); + const formSchema = z.object({ provider: z.object({ client: z.nativeEnum(SqlProviders), @@ -31,6 +50,7 @@ const formSchema = z.object({ database: z.string().min(1), username: z.string().min(1), password: z.string().min(1), + passwordRequirements: passwordRequirementsSchema.optional(), creationStatement: z.string().min(1), revocationStatement: z.string().min(1), renewStatement: z.string().optional(), @@ -137,7 +157,19 @@ export const SqlDatabaseInputForm = ({ } = useForm({ resolver: zodResolver(formSchema), defaultValues: { - provider: getSqlStatements(SqlProviders.Postgres) + provider: { + ...getSqlStatements(SqlProviders.Postgres), + passwordRequirements: { + minLength: 48, + maxLength: 48, + required: { + lowercase: 1, + uppercase: 1, + digits: 1, + symbols: 0 + } + } + } } }); @@ -174,6 +206,11 @@ export const SqlDatabaseInputForm = ({ setValue("provider.renewStatement", sqlStatment.renewStatement); setValue("provider.revocationStatement", sqlStatment.revocationStatement); setValue("provider.port", getDefaultPort(type)); + + // Update password requirements based on provider + const minMaxLength = type === SqlProviders.Oracle ? 30 : 48; + setValue("provider.passwordRequirements.minLength", minMaxLength); + setValue("provider.passwordRequirements.maxLength", minMaxLength); }; return ( @@ -197,6 +234,7 @@ export const SqlDatabaseInputForm = ({ )} /> +
)} /> - - - Modify SQL Statements + + + Creation, Revocation & Renew Statements (optional) +
+ Customize SQL statements for managing database user lifecycle +
+ + + Password Configuration (optional) + +
+ Set constraints on the generated database password +
+
+
+ ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> +
+ +
+

Required Characters

+
+ ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> +
+
+ +
+

Allowed Characters (Optional)

+
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+
+
+
+
+
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx index 39655e431..5cf5c6524 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx @@ -23,6 +23,25 @@ import { useWorkspace } from "@app/context"; import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api"; import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; +const passwordRequirementsSchema = z.object({ + minLength: z.number().min(1).max(100), + maxLength: z.number().min(1).max(100), + required: z.object({ + lowercase: z.number().min(0), + uppercase: z.number().min(0), + digits: z.number().min(0), + symbols: z.number().min(0) + }), + allowedCharacters: z + .object({ + lowercase: z.string().optional(), + uppercase: z.string().optional(), + digits: z.string().optional(), + symbols: z.string().optional() + }) + .optional() +}); + const formSchema = z.object({ inputs: z .object({ @@ -32,6 +51,7 @@ const formSchema = z.object({ database: z.string().min(1), username: z.string().min(1), password: z.string().min(1), + passwordRequirements: passwordRequirementsSchema.optional(), creationStatement: z.string().min(1), revocationStatement: z.string().min(1), renewStatement: z.string().optional(), @@ -82,6 +102,17 @@ export const EditDynamicSecretSqlProviderForm = ({ secretPath, projectSlug }: Props) => { + const getDefaultPasswordRequirements = (provider: SqlProviders) => ({ + minLength: provider === SqlProviders.Oracle ? 30 : 48, + maxLength: provider === SqlProviders.Oracle ? 30 : 48, + required: { + lowercase: 1, + uppercase: 1, + digits: 1, + symbols: 0 + } + }); + const { control, watch, @@ -94,10 +125,13 @@ export const EditDynamicSecretSqlProviderForm = ({ maxTTL: dynamicSecret.maxTTL, newName: dynamicSecret.name, inputs: { - ...(dynamicSecret.inputs as TForm["inputs"]) + ...(dynamicSecret.inputs as TForm["inputs"]), + passwordRequirements: (dynamicSecret.inputs as TForm["inputs"])?.passwordRequirements || + getDefaultPasswordRequirements((dynamicSecret.inputs as TForm["inputs"])?.client || SqlProviders.Postgres) } } }); + const { currentWorkspace } = useWorkspace(); const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery( gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id }) @@ -347,10 +381,13 @@ export const EditDynamicSecretSqlProviderForm = ({ )} /> - - - Modify SQL Statements + + + Creation, Revocation & Renew Statements (optional) +
+ Customize SQL statements for managing database user lifecycle +
+ + + Password Configuration (optional) + +
+ Set constraints on the generated database password +
+
+
+ ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> +
+ +
+

Required Characters

+
+ ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> +
+
+ +
+

Allowed Characters (Optional)

+
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+
+
+
+
+
From 6fa41a609bc985ee2dd2ddadba1f3eedef126682 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 11 Mar 2025 12:28:48 -0400 Subject: [PATCH 2/5] remove char and digit rangs and other requested changes/improvments --- .../dynamic-secret/providers/models.ts | 31 ++- .../dynamic-secret/providers/sql-database.ts | 50 +++-- .../SqlDatabaseInputForm.tsx | 165 ++++++--------- .../EditDynamicSecretSqlProviderForm.tsx | 197 +++++++----------- 4 files changed, 168 insertions(+), 275 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 93af4eabc..1808057e7 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -1,20 +1,14 @@ import { z } from "zod"; export type PasswordRequirements = { - minLength: number; - maxLength: number; + length: number; required: { lowercase: number; uppercase: number; digits: number; symbols: number; }; - allowedCharacters?: { - lowercase?: string; - uppercase?: string; - digits?: string; - symbols?: string; - }; + allowedSymbols?: string; }; export enum SqlProviders { @@ -119,23 +113,22 @@ export const DynamicSecretSqlDBSchema = z.object({ password: z.string().trim(), passwordRequirements: z .object({ - minLength: z.number().min(1).max(100), - maxLength: z.number().min(1).max(100), + length: z.number().min(1).max(250), required: z.object({ lowercase: z.number().min(0), uppercase: z.number().min(0), digits: z.number().min(0), symbols: z.number().min(0) - }), - allowedCharacters: z - .object({ - lowercase: z.string().optional(), - uppercase: z.string().optional(), - digits: z.string().optional(), - symbols: z.string().optional() - }) - .optional() + }).refine((data) => { + const total = Object.values(data).reduce((sum, count) => sum + count, 0); + return total <= 250; + }, "Sum of required characters cannot exceed 250"), + allowedSymbols: z.string().optional() }) + .refine((data) => { + const total = Object.values(data.required).reduce((sum, count) => sum + count, 0); + return total <= data.length; + }, "Sum of required characters cannot exceed the total length") .optional() .describe('Password generation requirements'), creationStatement: z.string().trim(), diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index 7d76db697..bb9240139 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -2,6 +2,7 @@ import handlebars from "handlebars"; import knex from "knex"; import { customAlphabet } from "nanoid"; import { z } from "zod"; +import { randomInt } from 'crypto'; import { withGatewayProxy } from "@app/lib/gateway"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -13,26 +14,19 @@ import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicP const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; const DEFAULT_PASSWORD_REQUIREMENTS = { - minLength: 48, - maxLength: 48, + length: 48, required: { lowercase: 1, uppercase: 1, digits: 1, symbols: 0 }, - allowedCharacters: { - lowercase: 'abcdefghijklmnopqrstuvwxyz', - uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', - digits: '0123456789', - symbols: '-_.~!*' - } + allowedSymbols: '-_.~!*' }; const ORACLE_PASSWORD_REQUIREMENTS = { ...DEFAULT_PASSWORD_REQUIREMENTS, - minLength: 30, - maxLength: 30 + length: 30 }; const generatePassword = (provider: SqlProviders, requirements?: PasswordRequirements) => { @@ -40,52 +34,56 @@ const generatePassword = (provider: SqlProviders, requirements?: PasswordRequire const finalReqs = requirements || defaultReqs; try { - const { minLength, maxLength, required, allowedCharacters = {} } = finalReqs; + const { length, required, allowedSymbols } = finalReqs; const chars = { - lowercase: allowedCharacters.lowercase || 'abcdefghijklmnopqrstuvwxyz', - uppercase: allowedCharacters.uppercase || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', - digits: allowedCharacters.digits || '0123456789', - symbols: allowedCharacters.symbols || '-_.~!*' + lowercase: 'abcdefghijklmnopqrstuvwxyz', + uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + digits: '0123456789', + symbols: allowedSymbols || '-_.~!*' }; const parts: string[] = []; if (required.lowercase > 0) { parts.push(...Array(required.lowercase).fill(0).map(() => - chars.lowercase[Math.floor(Math.random() * chars.lowercase.length)] + chars.lowercase[randomInt(chars.lowercase.length)] )); } - + if (required.uppercase > 0) { parts.push(...Array(required.uppercase).fill(0).map(() => - chars.uppercase[Math.floor(Math.random() * chars.uppercase.length)] + chars.uppercase[randomInt(chars.uppercase.length)] )); } - + if (required.digits > 0) { parts.push(...Array(required.digits).fill(0).map(() => - chars.digits[Math.floor(Math.random() * chars.digits.length)] + chars.digits[randomInt(chars.digits.length)] )); } - + if (required.symbols > 0) { parts.push(...Array(required.symbols).fill(0).map(() => - chars.symbols[Math.floor(Math.random() * chars.symbols.length)] + chars.symbols[randomInt(chars.symbols.length)] )); } const requiredTotal = Object.values(required).reduce((a, b) => a + b, 0); - const remainingLength = Math.max(minLength - requiredTotal, 0); + const remainingLength = Math.max(length - requiredTotal, 0); + + const allowedChars = Object.entries(chars) + .filter(([key]) => required[key as keyof typeof required] > 0) + .map(([, value]) => value) + .join(''); - const allChars = Object.values(chars).join(''); parts.push(...Array(remainingLength).fill(0).map(() => - allChars[Math.floor(Math.random() * allChars.length)] + allowedChars[randomInt(allowedChars.length)] )); // shuffle the array to mix up the characters for (let i = parts.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); + const j = randomInt(i + 1); [parts[i], parts[j]] = [parts[j], parts[i]]; } diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx index 354f226f8..063b2d08b 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx @@ -24,23 +24,21 @@ import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types"; const passwordRequirementsSchema = z.object({ - minLength: z.number().min(1).max(100), - maxLength: z.number().min(1).max(100), + length: z.number().min(1).max(250), required: z.object({ lowercase: z.number().min(0), uppercase: z.number().min(0), digits: z.number().min(0), symbols: z.number().min(0) - }), - allowedCharacters: z - .object({ - lowercase: z.string().optional(), - uppercase: z.string().optional(), - digits: z.string().optional(), - symbols: z.string().optional() - }) - .optional() -}); + }).refine((data) => { + const total = Object.values(data).reduce((sum, count) => sum + count, 0); + return total <= 250; + }, "Sum of required characters cannot exceed 250"), + allowedSymbols: z.string().optional() +}).refine((data) => { + const total = Object.values(data.required).reduce((sum, count) => sum + count, 0); + return total <= data.length; +}, "Sum of required characters cannot exceed the total length"); const formSchema = z.object({ provider: z.object({ @@ -153,21 +151,22 @@ export const SqlDatabaseInputForm = ({ control, setValue, formState: { isSubmitting }, - handleSubmit + handleSubmit, + watch } = useForm({ resolver: zodResolver(formSchema), defaultValues: { provider: { ...getSqlStatements(SqlProviders.Postgres), passwordRequirements: { - minLength: 48, - maxLength: 48, + length: 48, required: { lowercase: 1, uppercase: 1, digits: 1, symbols: 0 - } + }, + allowedSymbols: '-_.~!*' } } } @@ -208,9 +207,8 @@ export const SqlDatabaseInputForm = ({ setValue("provider.port", getDefaultPort(type)); // Update password requirements based on provider - const minMaxLength = type === SqlProviders.Oracle ? 30 : 48; - setValue("provider.passwordRequirements.minLength", minMaxLength); - setValue("provider.passwordRequirements.maxLength", minMaxLength); + const length = type === SqlProviders.Oracle ? 30 : 48; + setValue("provider.passwordRequirements.length", length); }; return ( @@ -499,41 +497,21 @@ export const SqlDatabaseInputForm = ({ Set constraints on the generated database password
-
+
( field.onChange(Number(e.target.value))} - /> - - )} - /> - ( - - field.onChange(Number(e.target.value))} /> @@ -543,7 +521,19 @@ export const SqlDatabaseInputForm = ({
-

Required Characters

+

Minimum Required Character Counts

+
+ {(() => { + const total = Object.values(watch("provider.passwordRequirements.required") || {}).reduce((sum, count) => sum + Number(count || 0), 0); + const length = watch("provider.passwordRequirements.length") || 0; + const isError = total > length; + return ( + + Total required characters: {total} {isError ? `(exceeds length of ${length})` : ""} + + ); + })()} +
( ( ( (
-

Allowed Characters (Optional)

-
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> -
+

Allowed Symbols

+ ( + + + + )} + />
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx index 5cf5c6524..85fb6456e 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx @@ -24,23 +24,21 @@ import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api"; import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; const passwordRequirementsSchema = z.object({ - minLength: z.number().min(1).max(100), - maxLength: z.number().min(1).max(100), + length: z.number().min(1).max(250), required: z.object({ lowercase: z.number().min(0), uppercase: z.number().min(0), digits: z.number().min(0), symbols: z.number().min(0) - }), - allowedCharacters: z - .object({ - lowercase: z.string().optional(), - uppercase: z.string().optional(), - digits: z.string().optional(), - symbols: z.string().optional() - }) - .optional() -}); + }).refine((data) => { + const total = Object.values(data).reduce((sum, count) => sum + count, 0); + return total <= 250; // Sanity check for individual validation + }, "Sum of required characters cannot exceed 250"), + allowedSymbols: z.string().optional() +}).refine((data) => { + const total = Object.values(data.required).reduce((sum, count) => sum + count, 0); + return total <= data.length; +}, "Sum of required characters cannot exceed the total length"); const formSchema = z.object({ inputs: z @@ -103,14 +101,14 @@ export const EditDynamicSecretSqlProviderForm = ({ projectSlug }: Props) => { const getDefaultPasswordRequirements = (provider: SqlProviders) => ({ - minLength: provider === SqlProviders.Oracle ? 30 : 48, - maxLength: provider === SqlProviders.Oracle ? 30 : 48, + length: provider === SqlProviders.Oracle ? 30 : 48, required: { lowercase: 1, uppercase: 1, digits: 1, symbols: 0 - } + }, + allowedSymbols: '-_.~!*' }); const { @@ -463,41 +461,21 @@ export const EditDynamicSecretSqlProviderForm = ({ Set constraints on the generated database password
-
+
( field.onChange(Number(e.target.value))} - /> - - )} - /> - ( - - field.onChange(Number(e.target.value))} /> @@ -507,7 +485,19 @@ export const EditDynamicSecretSqlProviderForm = ({
-

Required Characters

+

Minimum Required Character Counts

+
+ {(() => { + const total = Object.values(watch("inputs.passwordRequirements.required") || {}).reduce((sum, count) => sum + Number(count || 0), 0); + const length = watch("inputs.passwordRequirements.length") || 0; + const isError = total > length; + return ( + + Total required characters: {total} {isError ? `(exceeds length of ${length})` : ""} + + ); + })()} +
( field.onChange(Number(e.target.value))} - /> + type="number" + min={0} + {...field} + onChange={(e) => field.onChange(Number(e.target.value))} + /> )} /> @@ -534,16 +525,17 @@ export const EditDynamicSecretSqlProviderForm = ({ defaultValue={1} render={({ field, fieldState: { error } }) => ( field.onChange(Number(e.target.value))} - /> + type="number" + min={0} + {...field} + onChange={(e) => field.onChange(Number(e.target.value))} + /> )} /> @@ -553,16 +545,17 @@ export const EditDynamicSecretSqlProviderForm = ({ defaultValue={1} render={({ field, fieldState: { error } }) => ( field.onChange(Number(e.target.value))} - /> + type="number" + min={0} + {...field} + onChange={(e) => field.onChange(Number(e.target.value))} + /> )} /> @@ -572,16 +565,17 @@ export const EditDynamicSecretSqlProviderForm = ({ defaultValue={0} render={({ field, fieldState: { error } }) => ( field.onChange(Number(e.target.value))} - /> + type="number" + min={0} + {...field} + onChange={(e) => field.onChange(Number(e.target.value))} + /> )} /> @@ -589,65 +583,22 @@ export const EditDynamicSecretSqlProviderForm = ({
-

Allowed Characters (Optional)

-
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> -
+

Allowed Symbols

+ ( + + + + )} + />
From 75e0a68b685f284096e2c28a9acfe8250187da9a Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 11 Mar 2025 12:46:43 -0400 Subject: [PATCH 3/5] remove password regex --- backend/src/ee/services/license/license-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 21d378802..ba89f965a 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -17,7 +17,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ environmentsUsed: 0, identityLimit: null, identitiesUsed: 0, - dynamicSecret: false, + dynamicSecret: true, secretVersioning: true, pitRecovery: false, ipAllowlisting: false, From f5749e326a023211cbe6f9bd65ccc95d4a1f3a17 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 11 Mar 2025 12:49:55 -0400 Subject: [PATCH 4/5] remove regex and fix lint --- backend/src/ee/services/license/license-fns.ts | 2 +- frontend/src/hooks/api/dynamicSecret/types.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index ba89f965a..21d378802 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -17,7 +17,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ environmentsUsed: 0, identityLimit: null, identitiesUsed: 0, - dynamicSecret: true, + dynamicSecret: false, secretVersioning: true, pitRecovery: false, ipAllowlisting: false, diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 154449d5b..7d8c6920a 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -50,7 +50,6 @@ export type TDynamicSecretProvider = database: string; username: string; password: string; - passwordRegex?: string; creationStatement: string; revocationStatement: string; renewStatement?: string; From edf6a37fe54f791db4fc22f524a10f80dc698002 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 11 Mar 2025 13:08:04 -0400 Subject: [PATCH 5/5] fix lint --- .../dynamic-secret/providers/models.ts | 22 ++++--- .../dynamic-secret/providers/sql-database.ts | 65 +++++++++++-------- .../src/server/routes/v2/password-router.ts | 4 +- .../services/auth/auth-password-service.ts | 2 +- 4 files changed, 52 insertions(+), 41 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 1808057e7..449f6d8f6 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -114,15 +114,17 @@ export const DynamicSecretSqlDBSchema = z.object({ passwordRequirements: z .object({ length: z.number().min(1).max(250), - required: z.object({ - lowercase: z.number().min(0), - uppercase: z.number().min(0), - digits: z.number().min(0), - symbols: z.number().min(0) - }).refine((data) => { - const total = Object.values(data).reduce((sum, count) => sum + count, 0); - return total <= 250; - }, "Sum of required characters cannot exceed 250"), + required: z + .object({ + lowercase: z.number().min(0), + uppercase: z.number().min(0), + digits: z.number().min(0), + symbols: z.number().min(0) + }) + .refine((data) => { + const total = Object.values(data).reduce((sum, count) => sum + count, 0); + return total <= 250; + }, "Sum of required characters cannot exceed 250"), allowedSymbols: z.string().optional() }) .refine((data) => { @@ -130,7 +132,7 @@ export const DynamicSecretSqlDBSchema = z.object({ return total <= data.length; }, "Sum of required characters cannot exceed the total length") .optional() - .describe('Password generation requirements'), + .describe("Password generation requirements"), creationStatement: z.string().trim(), revocationStatement: z.string().trim(), renewStatement: z.string().trim().optional(), diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index bb9240139..eea9fef94 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -1,8 +1,7 @@ +import { randomInt } from "crypto"; import handlebars from "handlebars"; import knex from "knex"; -import { customAlphabet } from "nanoid"; import { z } from "zod"; -import { randomInt } from 'crypto'; import { withGatewayProxy } from "@app/lib/gateway"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -21,7 +20,7 @@ const DEFAULT_PASSWORD_REQUIREMENTS = { digits: 1, symbols: 0 }, - allowedSymbols: '-_.~!*' + allowedSymbols: "-_.~!*" }; const ORACLE_PASSWORD_REQUIREMENTS = { @@ -35,38 +34,46 @@ const generatePassword = (provider: SqlProviders, requirements?: PasswordRequire try { const { length, required, allowedSymbols } = finalReqs; - + const chars = { - lowercase: 'abcdefghijklmnopqrstuvwxyz', - uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', - digits: '0123456789', - symbols: allowedSymbols || '-_.~!*' + lowercase: "abcdefghijklmnopqrstuvwxyz", + uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + digits: "0123456789", + symbols: allowedSymbols || "-_.~!*" }; const parts: string[] = []; - + if (required.lowercase > 0) { - parts.push(...Array(required.lowercase).fill(0).map(() => - chars.lowercase[randomInt(chars.lowercase.length)] - )); + parts.push( + ...Array(required.lowercase) + .fill(0) + .map(() => chars.lowercase[randomInt(chars.lowercase.length)]) + ); } if (required.uppercase > 0) { - parts.push(...Array(required.uppercase).fill(0).map(() => - chars.uppercase[randomInt(chars.uppercase.length)] - )); + parts.push( + ...Array(required.uppercase) + .fill(0) + .map(() => chars.uppercase[randomInt(chars.uppercase.length)]) + ); } if (required.digits > 0) { - parts.push(...Array(required.digits).fill(0).map(() => - chars.digits[randomInt(chars.digits.length)] - )); + parts.push( + ...Array(required.digits) + .fill(0) + .map(() => chars.digits[randomInt(chars.digits.length)]) + ); } if (required.symbols > 0) { - parts.push(...Array(required.symbols).fill(0).map(() => - chars.symbols[randomInt(chars.symbols.length)] - )); + parts.push( + ...Array(required.symbols) + .fill(0) + .map(() => chars.symbols[randomInt(chars.symbols.length)]) + ); } const requiredTotal = Object.values(required).reduce((a, b) => a + b, 0); @@ -75,21 +82,23 @@ const generatePassword = (provider: SqlProviders, requirements?: PasswordRequire const allowedChars = Object.entries(chars) .filter(([key]) => required[key as keyof typeof required] > 0) .map(([, value]) => value) - .join(''); + .join(""); - parts.push(...Array(remainingLength).fill(0).map(() => - allowedChars[randomInt(allowedChars.length)] - )); + parts.push( + ...Array(remainingLength) + .fill(0) + .map(() => allowedChars[randomInt(allowedChars.length)]) + ); // shuffle the array to mix up the characters - for (let i = parts.length - 1; i > 0; i--) { + for (let i = parts.length - 1; i > 0; i -= 1) { const j = randomInt(i + 1); [parts[i], parts[j]] = [parts[j], parts[i]]; } - return parts.join(''); + return parts.join(""); } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; + const message = error instanceof Error ? error.message : "Unknown error"; throw new Error(`Failed to generate password: ${message}`); } }; diff --git a/backend/src/server/routes/v2/password-router.ts b/backend/src/server/routes/v2/password-router.ts index 165130dec..63b6d8aac 100644 --- a/backend/src/server/routes/v2/password-router.ts +++ b/backend/src/server/routes/v2/password-router.ts @@ -1,10 +1,10 @@ import { z } from "zod"; import { authRateLimit } from "@app/server/config/rateLimiter"; -import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns"; -import { AuthMode } from "@app/services/auth/auth-type"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns"; import { ResetPasswordV2Type } from "@app/services/auth/auth-password-type"; +import { AuthMode } from "@app/services/auth/auth-type"; export const registerPasswordRouter = async (server: FastifyZodProvider) => { server.route({ diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index f5f42a236..14fb58258 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -7,6 +7,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { generateUserSrpKeys } from "@app/lib/crypto/srp"; import { BadRequestError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; @@ -25,7 +26,6 @@ import { TSetupPasswordViaBackupKeyDTO } from "./auth-password-type"; import { ActorType, AuthMethod, AuthTokenType } from "./auth-type"; -import { logger } from "@app/lib/logger"; type TAuthPasswordServiceFactoryDep = { authDAL: TAuthDALFactory;