From 31e6904ff000dbd1897e72cbec318324491f526b Mon Sep 17 00:00:00 2001 From: = Date: Fri, 19 Dec 2025 00:41:12 +0530 Subject: [PATCH 1/5] feat: implemented rotation query and password requirement --- .../pki-acme/pki-acme-challenge-service.ts | 2 +- .../mongodb-credentials-rotation-schemas.ts | 4 +- .../mssql-credentials-rotation-constants.ts | 1 + .../mysql-credentials-rotation-constants.ts | 1 + ...oracledb-credentials-rotation-constants.ts | 1 + ...postgres-credentials-rotation-constants.ts | 1 + .../sql-credentials-rotation-fns.ts | 25 +- .../sql-credentials-rotation-schemas.ts | 24 +- backend/src/lib/api-docs/constants.ts | 3 +- .../forms/SecretRotationV2Form.tsx | 7 +- ...SqlCredentialsRotationParametersFields.tsx | 299 ++++++++++++++---- .../shared/sql-credentials-rotation-schema.ts | 6 +- .../types/shared/sql-credentials-rotation.ts | 1 + 13 files changed, 299 insertions(+), 76 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index 510d05c044..52fd0efb56 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -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< diff --git a/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-schemas.ts index 9a5335f5f6..9c7fb75ccb 100644 --- a/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-schemas.ts +++ b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-schemas.ts @@ -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), diff --git a/backend/src/ee/services/secret-rotation-v2/mssql-credentials/mssql-credentials-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/mssql-credentials/mssql-credentials-rotation-constants.ts index b256bd77fb..d318d0c85e 100644 --- a/backend/src/ee/services/secret-rotation-v2/mssql-credentials/mssql-credentials-rotation-constants.ts +++ b/backend/src/ee/services/secret-rotation-v2/mssql-credentials/mssql-credentials-rotation-constants.ts @@ -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" diff --git a/backend/src/ee/services/secret-rotation-v2/mysql-credentials/mysql-credentials-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/mysql-credentials/mysql-credentials-rotation-constants.ts index bae7a81660..588e6dc4f6 100644 --- a/backend/src/ee/services/secret-rotation-v2/mysql-credentials/mysql-credentials-rotation-constants.ts +++ b/backend/src/ee/services/secret-rotation-v2/mysql-credentials/mysql-credentials-rotation-constants.ts @@ -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" diff --git a/backend/src/ee/services/secret-rotation-v2/oracledb-credentials/oracledb-credentials-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/oracledb-credentials/oracledb-credentials-rotation-constants.ts index dd685041c6..6a0cda1770 100644 --- a/backend/src/ee/services/secret-rotation-v2/oracledb-credentials/oracledb-credentials-rotation-constants.ts +++ b/backend/src/ee/services/secret-rotation-v2/oracledb-credentials/oracledb-credentials-rotation-constants.ts @@ -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" diff --git a/backend/src/ee/services/secret-rotation-v2/postgres-credentials/postgres-credentials-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/postgres-credentials/postgres-credentials-rotation-constants.ts index 395ed46d1b..7408d9173b 100644 --- a/backend/src/ee/services/secret-rotation-v2/postgres-credentials/postgres-credentials-rotation-constants.ts +++ b/backend/src/ee/services/secret-rotation-v2/postgres-credentials/postgres-credentials-rotation-constants.ts @@ -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" diff --git a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts index 965516c3eb..4121e766db 100644 --- a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts @@ -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 = ( operation: (client: Knex) => Promise, @@ -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)); + } } }); }); diff --git a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-schemas.ts index 7ec47741f1..f9ec88e2c7 100644 --- a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-schemas.ts +++ b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-schemas.ts @@ -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() diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 60e930e691..16d2e68f2e 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -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." diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx index 320793ed19..c11b448dae 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx @@ -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, diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/shared/SqlCredentialsRotationParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/shared/SqlCredentialsRotationParametersFields.tsx index 8d6f3797d2..e5b8628462 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/shared/SqlCredentialsRotationParametersFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/shared/SqlCredentialsRotationParametersFields.tsx @@ -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 ( - <> - ( - - + + General + Advanced + + + ( + + + + )} + control={control} + name="parameters.username1" + /> + ( + + + + )} + control={control} + name="parameters.username2" + /> + +

+ Infisical requires two database users to be created for rotation. +

+

+ These users are intended to be solely managed by Infisical. Altering their login after + rotation may cause unexpected failure. +

+

+ Below is an example statement for creating the required users. You may need to modify it + to suit your needs. +

+

+

+              {rotationOption!.template.createUserStatement}
+            
+

+
+
+ + ( + +