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

+ ( + + + + )} + />