feat: implemented rotation query and password requirement

This commit is contained in:
=
2025-12-19 00:41:12 +05:30
parent 3bfd106f23
commit 31e6904ff0
13 changed files with 299 additions and 76 deletions

View File

@@ -6,6 +6,7 @@ import { TPkiAcmeChallenges } from "@app/db/schemas/pki-acme-challenges";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { isValidIp } from "@app/lib/ip";
import { isPrivateIp } from "@app/lib/ip/ipRange";
import { logger } from "@app/lib/logger";
import { ActorType } from "@app/services/auth/auth-type";
@@ -20,7 +21,6 @@ import {
} from "./pki-acme-errors";
import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas";
import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types";
import { isValidIp } from "@app/lib/ip";
type TPkiAcmeChallengeServiceFactoryDep = {
acmeChallengeDAL: Pick<

View File

@@ -17,7 +17,9 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
export const MongoDBCredentialsRotationGeneratedCredentialsSchema = SqlCredentialsRotationGeneratedCredentialsSchema;
export const MongoDBCredentialsRotationParametersSchema = SqlCredentialsRotationParametersSchema;
export const MongoDBCredentialsRotationTemplateSchema = SqlCredentialsRotationTemplateSchema;
export const MongoDBCredentialsRotationTemplateSchema = SqlCredentialsRotationTemplateSchema.omit({
rotationStatement: true
});
const MongoDBCredentialsRotationSecretsMappingSchema = z.object({
username: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.MONGODB_CREDENTIALS.username),

View File

@@ -21,6 +21,7 @@ CREATE USER [infisical_user] FOR LOGIN [infisical_user];
-- Grant permissions to the user on the schema in this database
GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [infisical_user];`,
rotationStatement: `ALTER LOGIN [{{username}}] WITH PASSWORD = '{{password}}'`,
secretsMapping: {
username: "MSSQL_DB_USERNAME",
password: "MSSQL_DB_PASSWORD"

View File

@@ -15,6 +15,7 @@ GRANT ALL PRIVILEGES ON my_database.* TO 'infisical_user'@'%';
-- apply the privilege changes
FLUSH PRIVILEGES;`,
rotationStatement: `ALTER USER '{{username}}'@'%' IDENTIFIED BY '{{password}}'`,
secretsMapping: {
username: "MYSQL_USERNAME",
password: "MYSQL_PASSWORD"

View File

@@ -12,6 +12,7 @@ CREATE USER INFISICAL_USER IDENTIFIED BY "temporary_password";
-- grant all privileges
GRANT ALL PRIVILEGES TO INFISICAL_USER;`,
rotationStatement: `ALTER USER "{{username}}" IDENTIFIED BY "{{password}}"`,
secretsMapping: {
username: "ORACLEDB_USERNAME",
password: "ORACLEDB_PASSWORD"

View File

@@ -15,6 +15,7 @@ GRANT CONNECT ON DATABASE my_database TO infisical_user;
-- grant relevant table permissions
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO infisical_user;`,
rotationStatement: `ALTER USER "{{username}}" WITH PASSWORD '{{password}}'`,
secretsMapping: {
username: "POSTGRES_DB_USERNAME",
password: "POSTGRES_DB_PASSWORD"

View File

@@ -1,3 +1,4 @@
import handlebars from "handlebars";
import { Knex } from "knex";
import {
@@ -44,13 +45,19 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
> = (secretRotation, _appConnectionDAL, _kmsService, gatewayService, gatewayV2Service) => {
const {
connection,
parameters: { username1, username2 },
parameters: {
username1,
username2,
rotationStatement: userProvidedRotationStatement,
passwordRequirements: userProvidedPasswordRequirements
},
activeIndex,
secretsMapping
} = secretRotation;
const passwordRequirement =
const defaultPasswordRequirement =
connection.app === AppConnection.OracleDB ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS;
const passwordRequirement = userProvidedPasswordRequirements || defaultPasswordRequirement;
const executeOperation = <T>(
operation: (client: Knex) => Promise<T>,
@@ -96,7 +103,19 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
await executeOperation(async (client) => {
await client.transaction(async (tx) => {
for await (const credentials of credentialsSet) {
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
if (userProvidedRotationStatement) {
const revokeStatement = handlebars.compile(userProvidedRotationStatement)({
username: credentials.username,
password: credentials.password,
database: connection.credentials.database
});
const queries = revokeStatement.toString().split(";").filter(Boolean);
for await (const query of queries) {
await tx.raw(query);
}
} else {
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
}
}
});
});

View File

@@ -1,8 +1,11 @@
import { z } from "zod";
import { SecretRotations } from "@app/lib/api-docs";
import { isValidHandleBarTemplate } from "@app/lib/template/validate-handlebars";
import { SecretNameSchema } from "@app/server/lib/schemas";
import { PasswordRequirementsSchema } from "../general";
export const SqlCredentialsRotationGeneratedCredentialsSchema = z
.object({
username: z.string(),
@@ -22,7 +25,25 @@ export const SqlCredentialsRotationParametersSchema = z.object({
.string()
.trim()
.min(1, "Username2 Required")
.describe(SecretRotations.PARAMETERS.SQL_CREDENTIALS.username2)
.describe(SecretRotations.PARAMETERS.SQL_CREDENTIALS.username2),
rotationStatement: z
.string()
.trim()
.min(1, "Rotation Statement Required")
.describe(SecretRotations.PARAMETERS.SQL_CREDENTIALS.rotationStatement)
.refine(
(el) =>
isValidHandleBarTemplate(el, {
allowedExpressions: (val) => ["username", "password", "database"].includes(val)
}),
"Invalid expression detected in rotation statement"
)
.refine(
(el) => el.includes("{{username}}") && el.includes("{{password}}"),
"Rotation statement must have username and password template expression"
)
.optional(),
passwordRequirements: PasswordRequirementsSchema.optional()
});
export const SqlCredentialsRotationSecretsMappingSchema = z.object({
@@ -32,6 +53,7 @@ export const SqlCredentialsRotationSecretsMappingSchema = z.object({
export const SqlCredentialsRotationTemplateSchema = z.object({
createUserStatement: z.string(),
rotationStatement: z.string(),
secretsMapping: z.object({
username: z.string(),
password: z.string()

View File

@@ -2876,7 +2876,8 @@ export const SecretRotations = {
username1:
"The username of the first login to rotate passwords for. This user must already exists in your database.",
username2:
"The username of the second login to rotate passwords for. This user must already exists in your database."
"The username of the second login to rotate passwords for. This user must already exists in your database.",
rotationStatement: "The SQL template query used for rotation."
},
AUTH0_CLIENT_SECRET: {
clientId: "The client ID of the Auth0 Application to rotate the client secret for."

View File

@@ -101,12 +101,7 @@ export const SecretRotationV2Form = ({
reValidateMode: "onChange"
});
const onSubmit = async ({
environment,
connection,
...formData
}: TSecretRotationV2Form) => {
const onSubmit = async ({ environment, connection, ...formData }: TSecretRotationV2Form) => {
const mutation = secretRotation
? updateSecretRotation.mutateAsync({
rotationId: secretRotation.id,

View File

@@ -1,84 +1,259 @@
import { Controller, useFormContext } from "react-hook-form";
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
import { FormControl, Input } from "@app/components/v2";
import { FormControl, Input, Tab, TabList, TabPanel, Tabs, TextArea } from "@app/components/v2";
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretRotation, useSecretRotationV2Option } from "@app/hooks/api/secretRotationsV2";
import { DEFAULT_PASSWORD_REQUIREMENTS } from "../../schemas/shared";
enum ParameterTab {
Statement = "statement",
Advanced = "advance"
}
export const SqlCredentialsRotationParametersFields = () => {
const { control, watch } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.PostgresCredentials | SecretRotation.MsSqlCredentials;
type:
| SecretRotation.PostgresCredentials
| SecretRotation.MsSqlCredentials
| SecretRotation.OracleDBCredentials;
}
>();
const type = watch("type");
const { rotationOption } = useSecretRotationV2Option(type);
return (
<>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Database Username 1"
>
<Input
value={value}
onChange={onChange}
placeholder={
rotationOption.connection === AppConnection.OracleDB
? "INFISICAL_USER_1"
: "infisical_user_1"
<Tabs defaultValue={ParameterTab.Statement}>
<TabList className="border-b border-mineshaft-500">
<Tab value={ParameterTab.Statement}>General</Tab>
<Tab value={ParameterTab.Advanced}>Advanced</Tab>
</TabList>
<TabPanel value={ParameterTab.Statement}>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Database Username 1"
>
<Input
value={value}
onChange={onChange}
placeholder={
rotationOption.connection === AppConnection.OracleDB
? "INFISICAL_USER_1"
: "infisical_user_1"
}
/>
</FormControl>
)}
control={control}
name="parameters.username1"
/>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Database Username 2"
>
<Input
value={value}
onChange={onChange}
placeholder={
rotationOption.connection === AppConnection.OracleDB
? "INFISICAL_USER_2"
: "infisical_user_2"
}
/>
</FormControl>
)}
control={control}
name="parameters.username2"
/>
<NoticeBannerV2 title="Example Create User Statement">
<p className="mb-3 text-sm text-mineshaft-300">
Infisical requires two database users to be created for rotation.
</p>
<p className="mb-3 text-sm text-mineshaft-300">
These users are intended to be solely managed by Infisical. Altering their login after
rotation may cause unexpected failure.
</p>
<p className="mb-3 text-sm text-mineshaft-300">
Below is an example statement for creating the required users. You may need to modify it
to suit your needs.
</p>
<p className="mb-3 text-sm">
<pre className="max-h-40 overflow-y-auto rounded-sm border border-mineshaft-700 bg-mineshaft-800 p-2 whitespace-pre-wrap text-mineshaft-300">
{rotationOption!.template.createUserStatement}
</pre>
</p>
</NoticeBannerV2>
</TabPanel>
<TabPanel value={ParameterTab.Advanced}>
<Controller
control={control}
name="parameters.rotationStatement"
defaultValue={rotationOption?.template?.rotationStatement}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Rotation Statement"
isError={Boolean(error?.message)}
errorText={error?.message}
helperText="username, password and database are dynamically provisioned"
>
<TextArea
{...field}
reSize="none"
rows={3}
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
/>
</FormControl>
)}
/>
<div className="flex flex-col gap-3">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-300">Password Requirements</span>
</div>
<div className="grid grid-cols-2 gap-x-3 gap-y-1 rounded-sm border border-mineshaft-600 bg-mineshaft-700 px-3 pt-3">
<Controller
control={control}
name="parameters.passwordRequirements.length"
defaultValue={
// for oracle 48 would throw error
type === SecretRotation.OracleDBCredentials
? 30
: DEFAULT_PASSWORD_REQUIREMENTS.length
}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Password Length"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="The length of the password to generate"
>
<Input
type="number"
min={1}
max={250}
size="sm"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
</FormControl>
)}
control={control}
name="parameters.username1"
/>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Database Username 2"
>
<Input
value={value}
onChange={onChange}
placeholder={
rotationOption.connection === AppConnection.OracleDB
? "INFISICAL_USER_2"
: "infisical_user_2"
}
<Controller
control={control}
name="parameters.passwordRequirements.required.digits"
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.required.digits}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Digit Count"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Minimum number of digits"
>
<Input
type="number"
min={0}
size="sm"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
</FormControl>
)}
control={control}
name="parameters.username2"
/>
<NoticeBannerV2 title="Example Create User Statement">
<p className="mb-3 text-sm text-mineshaft-300">
Infisical requires two database users to be created for rotation.
</p>
<p className="mb-3 text-sm text-mineshaft-300">
These users are intended to be solely managed by Infisical. Altering their login after
rotation may cause unexpected failure.
</p>
<p className="mb-3 text-sm text-mineshaft-300">
Below is an example statement for creating the required users. You may need to modify it
to suit your needs.
</p>
<p className="mb-3 text-sm">
<pre className="max-h-40 overflow-y-auto rounded-sm border border-mineshaft-700 bg-mineshaft-800 p-2 whitespace-pre-wrap text-mineshaft-300">
{rotationOption!.template.createUserStatement}
</pre>
</p>
</NoticeBannerV2>
</>
<Controller
control={control}
name="parameters.passwordRequirements.required.lowercase"
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.required.lowercase}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Lowercase Character Count"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Minimum number of lowercase characters"
>
<Input
type="number"
min={0}
size="sm"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="parameters.passwordRequirements.required.uppercase"
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.required.uppercase}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Uppercase Character Count"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Minimum number of uppercase characters"
>
<Input
type="number"
min={0}
size="sm"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="parameters.passwordRequirements.required.symbols"
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.required.symbols}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Symbol Count"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Minimum number of symbols"
>
<Input
type="number"
min={0}
size="sm"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="parameters.passwordRequirements.allowedSymbols"
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.allowedSymbols}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Symbols"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Symbols to use in generated password"
>
<Input
placeholder="-_.~!*"
size="sm"
{...field}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
</div>
</div>
</TabPanel>
</Tabs>
);
};

View File

@@ -2,10 +2,14 @@ import { z } from "zod";
import { SecretNameSchema } from "@app/lib/schemas";
import { PasswordRequirementsSchema } from "./password-requirements-schema";
export const SqlCredentialsRotationSchema = z.object({
parameters: z.object({
username1: z.string().trim().min(1, "Database Username 1 Required"),
username2: z.string().trim().min(1, "Database Username 2 Required")
username2: z.string().trim().min(1, "Database Username 2 Required"),
rotationStatement: z.string().trim().min(1).optional(),
passwordRequirements: PasswordRequirementsSchema.optional()
}),
secretsMapping: z.object({
username: SecretNameSchema,

View File

@@ -27,6 +27,7 @@ export type TSqlCredentialsRotationOption = {
template: {
secretsMapping: TSqlCredentialsRotationProperties["secretsMapping"];
createUserStatement: string;
rotationStatement: string;
};
};