mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #3216 from Infisical/password-reqs
feat: Add password requirements to dyanmic secret
This commit is contained in:
@@ -1,5 +1,16 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export type PasswordRequirements = {
|
||||||
|
length: number;
|
||||||
|
required: {
|
||||||
|
lowercase: number;
|
||||||
|
uppercase: number;
|
||||||
|
digits: number;
|
||||||
|
symbols: number;
|
||||||
|
};
|
||||||
|
allowedSymbols?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export enum SqlProviders {
|
export enum SqlProviders {
|
||||||
Postgres = "postgres",
|
Postgres = "postgres",
|
||||||
MySQL = "mysql2",
|
MySQL = "mysql2",
|
||||||
@@ -100,6 +111,28 @@ export const DynamicSecretSqlDBSchema = z.object({
|
|||||||
database: z.string().trim(),
|
database: z.string().trim(),
|
||||||
username: z.string().trim(),
|
username: z.string().trim(),
|
||||||
password: z.string().trim(),
|
password: z.string().trim(),
|
||||||
|
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"),
|
||||||
|
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(),
|
creationStatement: z.string().trim(),
|
||||||
revocationStatement: z.string().trim(),
|
revocationStatement: z.string().trim(),
|
||||||
renewStatement: z.string().trim().optional(),
|
renewStatement: z.string().trim().optional(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { randomInt } from "crypto";
|
||||||
import handlebars from "handlebars";
|
import handlebars from "handlebars";
|
||||||
import knex from "knex";
|
import knex from "knex";
|
||||||
import { customAlphabet } from "nanoid";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { withGatewayProxy } from "@app/lib/gateway";
|
import { withGatewayProxy } from "@app/lib/gateway";
|
||||||
@@ -8,16 +8,99 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
|||||||
|
|
||||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
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 EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||||
|
|
||||||
const generatePassword = (provider: SqlProviders) => {
|
const DEFAULT_PASSWORD_REQUIREMENTS = {
|
||||||
// oracle has limit of 48 password length
|
length: 48,
|
||||||
const size = provider === SqlProviders.Oracle ? 30 : 48;
|
required: {
|
||||||
|
lowercase: 1,
|
||||||
|
uppercase: 1,
|
||||||
|
digits: 1,
|
||||||
|
symbols: 0
|
||||||
|
},
|
||||||
|
allowedSymbols: "-_.~!*"
|
||||||
|
};
|
||||||
|
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
const ORACLE_PASSWORD_REQUIREMENTS = {
|
||||||
return customAlphabet(charset, 48)(size);
|
...DEFAULT_PASSWORD_REQUIREMENTS,
|
||||||
|
length: 30
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePassword = (provider: SqlProviders, requirements?: PasswordRequirements) => {
|
||||||
|
const defaultReqs = provider === SqlProviders.Oracle ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS;
|
||||||
|
const finalReqs = requirements || defaultReqs;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { length, required, allowedSymbols } = finalReqs;
|
||||||
|
|
||||||
|
const chars = {
|
||||||
|
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)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required.uppercase > 0) {
|
||||||
|
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)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required.symbols > 0) {
|
||||||
|
parts.push(
|
||||||
|
...Array(required.symbols)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => chars.symbols[randomInt(chars.symbols.length)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 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("");
|
||||||
|
|
||||||
|
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 -= 1) {
|
||||||
|
const j = randomInt(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) => {
|
const generateUsername = (provider: SqlProviders) => {
|
||||||
@@ -115,7 +198,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
|||||||
const create = async (inputs: unknown, expireAt: number) => {
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const username = generateUsername(providerInputs.client);
|
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 gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||||
const db = await $getClient({ ...providerInputs, port, host });
|
const db = await $getClient({ ...providerInputs, port, host });
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
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 { 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 { ResetPasswordV2Type } from "@app/services/auth/auth-password-type";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
|||||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
@@ -25,7 +26,6 @@ import {
|
|||||||
TSetupPasswordViaBackupKeyDTO
|
TSetupPasswordViaBackupKeyDTO
|
||||||
} from "./auth-password-type";
|
} from "./auth-password-type";
|
||||||
import { ActorType, AuthMethod, AuthTokenType } from "./auth-type";
|
import { ActorType, AuthMethod, AuthTokenType } from "./auth-type";
|
||||||
import { logger } from "@app/lib/logger";
|
|
||||||
|
|
||||||
type TAuthPasswordServiceFactoryDep = {
|
type TAuthPasswordServiceFactoryDep = {
|
||||||
authDAL: TAuthDALFactory;
|
authDAL: TAuthDALFactory;
|
||||||
|
|||||||
@@ -23,6 +23,23 @@ import { useWorkspace } from "@app/context";
|
|||||||
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
|
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
|
||||||
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
|
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||||
|
|
||||||
|
const passwordRequirementsSchema = 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"),
|
||||||
|
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({
|
const formSchema = z.object({
|
||||||
provider: z.object({
|
provider: z.object({
|
||||||
client: z.nativeEnum(SqlProviders),
|
client: z.nativeEnum(SqlProviders),
|
||||||
@@ -31,6 +48,7 @@ const formSchema = z.object({
|
|||||||
database: z.string().min(1),
|
database: z.string().min(1),
|
||||||
username: z.string().min(1),
|
username: z.string().min(1),
|
||||||
password: z.string().min(1),
|
password: z.string().min(1),
|
||||||
|
passwordRequirements: passwordRequirementsSchema.optional(),
|
||||||
creationStatement: z.string().min(1),
|
creationStatement: z.string().min(1),
|
||||||
revocationStatement: z.string().min(1),
|
revocationStatement: z.string().min(1),
|
||||||
renewStatement: z.string().optional(),
|
renewStatement: z.string().optional(),
|
||||||
@@ -133,11 +151,24 @@ export const SqlDatabaseInputForm = ({
|
|||||||
control,
|
control,
|
||||||
setValue,
|
setValue,
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
handleSubmit
|
handleSubmit,
|
||||||
|
watch
|
||||||
} = useForm<TForm>({
|
} = useForm<TForm>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
provider: getSqlStatements(SqlProviders.Postgres)
|
provider: {
|
||||||
|
...getSqlStatements(SqlProviders.Postgres),
|
||||||
|
passwordRequirements: {
|
||||||
|
length: 48,
|
||||||
|
required: {
|
||||||
|
lowercase: 1,
|
||||||
|
uppercase: 1,
|
||||||
|
digits: 1,
|
||||||
|
symbols: 0
|
||||||
|
},
|
||||||
|
allowedSymbols: '-_.~!*'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -174,6 +205,10 @@ export const SqlDatabaseInputForm = ({
|
|||||||
setValue("provider.renewStatement", sqlStatment.renewStatement);
|
setValue("provider.renewStatement", sqlStatment.renewStatement);
|
||||||
setValue("provider.revocationStatement", sqlStatment.revocationStatement);
|
setValue("provider.revocationStatement", sqlStatment.revocationStatement);
|
||||||
setValue("provider.port", getDefaultPort(type));
|
setValue("provider.port", getDefaultPort(type));
|
||||||
|
|
||||||
|
// Update password requirements based on provider
|
||||||
|
const length = type === SqlProviders.Oracle ? 30 : 48;
|
||||||
|
setValue("provider.passwordRequirements.length", length);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -197,6 +232,7 @@ export const SqlDatabaseInputForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-32">
|
<div className="w-32">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -386,10 +422,13 @@ export const SqlDatabaseInputForm = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Accordion type="single" collapsible className="mb-2 w-full bg-mineshaft-700">
|
<Accordion type="multiple" className="mb-2 w-full bg-mineshaft-700">
|
||||||
<AccordionItem value="advance-statements">
|
<AccordionItem value="advanced">
|
||||||
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
<AccordionTrigger>Creation, Revocation & Renew Statements (optional)</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
|
<div className="mb-4 text-sm text-mineshaft-300">
|
||||||
|
Customize SQL statements for managing database user lifecycle
|
||||||
|
</div>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="provider.creationStatement"
|
name="provider.creationStatement"
|
||||||
@@ -450,6 +489,157 @@ export const SqlDatabaseInputForm = ({
|
|||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
<Accordion type="multiple" className="mb-2 mt-4 w-full bg-mineshaft-700">
|
||||||
|
<AccordionItem value="password-config">
|
||||||
|
<AccordionTrigger>Password Configuration (optional)</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<div className="mb-4 text-sm text-mineshaft-300">
|
||||||
|
Set constraints on the generated database password
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.passwordRequirements.length"
|
||||||
|
defaultValue={48}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Password Length"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={250}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-sm font-medium">Minimum Required Character Counts</h4>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
{(() => {
|
||||||
|
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 (
|
||||||
|
<span className={isError ? "text-red-500" : ""}>
|
||||||
|
Total required characters: {total} {isError ? `(exceeds length of ${length})` : ""}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.passwordRequirements.required.lowercase"
|
||||||
|
defaultValue={1}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Lowercase Count"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="Minimum number of lowercase letters"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.passwordRequirements.required.uppercase"
|
||||||
|
defaultValue={1}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Uppercase Count"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="Minimum number of uppercase letters"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.passwordRequirements.required.digits"
|
||||||
|
defaultValue={1}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Digit Count"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="Minimum number of digits"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.passwordRequirements.required.symbols"
|
||||||
|
defaultValue={0}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Symbol Count"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="Minimum number of symbols"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-sm font-medium">Allowed Symbols</h4>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.passwordRequirements.allowedSymbols"
|
||||||
|
defaultValue="-_.~!*"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Symbols to use in password"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="Default: -_.~!*"
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="-_.~!*" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,23 @@ import { useWorkspace } from "@app/context";
|
|||||||
import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api";
|
import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api";
|
||||||
import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||||
|
|
||||||
|
const passwordRequirementsSchema = 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; // 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({
|
const formSchema = z.object({
|
||||||
inputs: z
|
inputs: z
|
||||||
.object({
|
.object({
|
||||||
@@ -32,6 +49,7 @@ const formSchema = z.object({
|
|||||||
database: z.string().min(1),
|
database: z.string().min(1),
|
||||||
username: z.string().min(1),
|
username: z.string().min(1),
|
||||||
password: z.string().min(1),
|
password: z.string().min(1),
|
||||||
|
passwordRequirements: passwordRequirementsSchema.optional(),
|
||||||
creationStatement: z.string().min(1),
|
creationStatement: z.string().min(1),
|
||||||
revocationStatement: z.string().min(1),
|
revocationStatement: z.string().min(1),
|
||||||
renewStatement: z.string().optional(),
|
renewStatement: z.string().optional(),
|
||||||
@@ -82,6 +100,17 @@ export const EditDynamicSecretSqlProviderForm = ({
|
|||||||
secretPath,
|
secretPath,
|
||||||
projectSlug
|
projectSlug
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const getDefaultPasswordRequirements = (provider: SqlProviders) => ({
|
||||||
|
length: provider === SqlProviders.Oracle ? 30 : 48,
|
||||||
|
required: {
|
||||||
|
lowercase: 1,
|
||||||
|
uppercase: 1,
|
||||||
|
digits: 1,
|
||||||
|
symbols: 0
|
||||||
|
},
|
||||||
|
allowedSymbols: '-_.~!*'
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
@@ -94,10 +123,13 @@ export const EditDynamicSecretSqlProviderForm = ({
|
|||||||
maxTTL: dynamicSecret.maxTTL,
|
maxTTL: dynamicSecret.maxTTL,
|
||||||
newName: dynamicSecret.name,
|
newName: dynamicSecret.name,
|
||||||
inputs: {
|
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 { currentWorkspace } = useWorkspace();
|
||||||
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
|
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
|
||||||
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
|
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
|
||||||
@@ -347,10 +379,13 @@ export const EditDynamicSecretSqlProviderForm = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Accordion type="multiple" className="w-full bg-mineshaft-700">
|
<Accordion type="multiple" className="mb-2 mt-4 w-full bg-mineshaft-700">
|
||||||
<AccordionItem value="modify-sql-statement">
|
<AccordionItem value="advanced">
|
||||||
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
<AccordionTrigger>Creation, Revocation & Renew Statements (optional)</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
|
<div className="mb-4 text-sm text-mineshaft-300">
|
||||||
|
Customize SQL statements for managing database user lifecycle
|
||||||
|
</div>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="inputs.creationStatement"
|
name="inputs.creationStatement"
|
||||||
@@ -418,6 +453,157 @@ export const EditDynamicSecretSqlProviderForm = ({
|
|||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
<Accordion type="multiple" className="mb-2 mt-4 w-full bg-mineshaft-700">
|
||||||
|
<AccordionItem value="password-config">
|
||||||
|
<AccordionTrigger>Password Configuration (optional)</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<div className="mb-4 text-sm text-mineshaft-300">
|
||||||
|
Set constraints on the generated database password
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.passwordRequirements.length"
|
||||||
|
defaultValue={48}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Password Length"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={250}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-sm font-medium">Minimum Required Character Counts</h4>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
{(() => {
|
||||||
|
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 (
|
||||||
|
<span className={isError ? "text-red-500" : ""}>
|
||||||
|
Total required characters: {total} {isError ? `(exceeds length of ${length})` : ""}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.passwordRequirements.required.lowercase"
|
||||||
|
defaultValue={1}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Lowercase Count"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="Minimum number of lowercase letters"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.passwordRequirements.required.uppercase"
|
||||||
|
defaultValue={1}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Uppercase Count"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="Minimum number of uppercase letters"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.passwordRequirements.required.digits"
|
||||||
|
defaultValue={1}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Digit Count"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="Minimum number of digits"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.passwordRequirements.required.symbols"
|
||||||
|
defaultValue={0}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Symbol Count"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="Minimum number of symbols"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-sm font-medium">Allowed Symbols</h4>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.passwordRequirements.allowedSymbols"
|
||||||
|
defaultValue="-_.~!*"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Symbols to use in password"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="Default: -_.~!*"
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="-_.~!*" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user