Merge pull request #5071 from Infisical/feat/rotation-pass-config

feat: sql rotation query option and password policy enforcement
This commit is contained in:
Scott Wilson
2025-12-19 10:58:12 -08:00
committed by GitHub
44 changed files with 564 additions and 84 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

@@ -16,8 +16,13 @@ import { SecretNameSchema } from "@app/server/lib/schemas";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const MongoDBCredentialsRotationGeneratedCredentialsSchema = SqlCredentialsRotationGeneratedCredentialsSchema;
export const MongoDBCredentialsRotationParametersSchema = SqlCredentialsRotationParametersSchema;
export const MongoDBCredentialsRotationTemplateSchema = SqlCredentialsRotationTemplateSchema;
export const MongoDBCredentialsRotationParametersSchema = SqlCredentialsRotationParametersSchema.omit({
rotationStatement: true,
passwordRequirements: true
});
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>,
@@ -82,21 +89,38 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
}
};
const $executeQuery = async (tx: Knex, username: string, password: string) => {
if (userProvidedRotationStatement) {
const revokeStatement = handlebars.compile(userProvidedRotationStatement)({
username,
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]({ username, password }));
}
};
const issueCredentials: TRotationFactoryIssueCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
callback
) => {
// For SQL, since we get existing users, we change both their passwords
// on issue to invalidate their existing passwords
const credentialsSet = [
{ username: username1, password: generatePassword(passwordRequirement) },
{ username: username2, password: generatePassword(passwordRequirement) }
];
const credentialsSet = [{ username: username1, password: generatePassword(passwordRequirement) }];
// if both are same username like for mysql dual password rotation - we don't want to reissue twice loosing first cred access
if (username1 !== username2) {
credentialsSet.push({ username: username2, password: generatePassword(passwordRequirement) });
}
try {
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));
await $executeQuery(tx, credentials.username, credentials.password);
}
});
});
@@ -125,7 +149,7 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
await client.transaction(async (tx) => {
for await (const credentials of revokedCredentials) {
// invalidate previous passwords
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
await $executeQuery(tx, credentials.username, credentials.password);
}
});
});
@@ -148,7 +172,7 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
try {
await executeOperation(async (client) => {
await client.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
await $executeQuery(client, credentials.username, credentials.password);
});
} catch (error) {
throw new Error(redactPasswords(error, [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

@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/mongodb/available"
---

View File

@@ -0,0 +1,10 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/mongodb"
---
<Note>
Check out the configuration docs for [MongoDB
Connections](/integrations/app-connections/mongodb) to learn how to obtain the
required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/mongodb/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/mongodb/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/mongodb/connection-name/{connectionName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/mongodb"
---

View File

@@ -0,0 +1,10 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/mongodb/{connectionId}"
---
<Note>
Check out the configuration docs for [MongoDB
Connections](/integrations/app-connections/mongodb) to learn how to obtain the
required credentials.
</Note>

View File

@@ -0,0 +1,10 @@
---
title: "Create"
openapi: "POST /api/v2/secret-rotations/mongodb-credentials"
---
<Note>
Check out the configuration docs for [MongoDB
Credentials Rotations](/documentation/platform/secret-rotation/mongodb-credentials) to learn how to obtain the
required parameters.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v2/secret-rotations/mongodb-credentials/{rotationId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v2/secret-rotations/mongodb-credentials/{rotationId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v2/secret-rotations/mongodb-credentials/rotation-name/{rotationName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get Credentials by ID"
openapi: "GET /api/v2/secret-rotations/mongodb-credentials/{rotationId}/generated-credentials"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/secret-rotations/mongodb-credentials"
---

View File

@@ -0,0 +1,4 @@
---
title: "Rotate Secrets"
openapi: "POST /api/v2/secret-rotations/mongodb-credentials/{rotationId}/rotate-secrets"
---

View File

@@ -0,0 +1,10 @@
---
title: "Update"
openapi: "PATCH /api/v2/secret-rotations/mongodb-credentials/{rotationId}"
---
<Note>
Check out the configuration docs for [MongoDB
Credentials Rotations](/documentation/platform/secret-rotation/mongodb-credentials) to learn how to obtain the
required parameters.
</Note>

View File

@@ -56,6 +56,11 @@ An example creation statement might look like:
- **Database Username 1** - the username of the first user that will be used for rotation.
- **Database Username 2** - the username of the second user that will be used for rotation.
![Rotation Advance Parameters](/images/secret-rotations-v2/mssql-credentials/mssql-credentials-advance-parameters.png)
- **Rotation Statement** - the template string query to generate password for the rotated user.
- **Password Requirements** - the requirements for the password of the MySQL users that will be created for the rotation.
5. Specify the secret names that the active credentials should be mapped to. Then click **Next**.
![Rotation Secrets Mapping](/images/secret-rotations-v2/mssql-credentials/mssql-credentials-secrets-mapping.png)

View File

@@ -51,6 +51,11 @@ description: "Learn how to automatically rotate MySQL credentials."
- **Database Username 1** - the username of the first user that will be used for rotation.
- **Database Username 2** - the username of the second user that will be used for rotation.
![Rotation Advance Parameters](/images/secret-rotations-v2/mysql-credentials/mysql-credentials-advance-parameters.png)
- **Rotation Statement** - the template string query to generate password for the rotated user.
- **Password Requirements** - the requirements for the password of the MySQL users that will be created for the rotation.
5. Specify the secret names that the active credentials should be mapped to. Then click **Next**.
![Rotation Secrets Mapping](/images/secret-rotations-v2/mysql-credentials/mysql-credentials-secrets-mapping.png)

View File

@@ -61,6 +61,11 @@ description: "Learn how to automatically rotate Oracle Database credentials."
If your Oracle usernames were created without "quotes", Oracle sees them as UPPERCASE. Please use UPPERCASE for those names in the fields above.
</Note>
![Rotation Advance Parameters](/images/secret-rotations-v2/oracledb-credentials/oracledb-credentials-advance-parameters.png)
- **Rotation Statement** - the template string query to generate password for the rotated user.
- **Password Requirements** - the requirements for the password of the MySQL users that will be created for the rotation.
5. Specify the secret names that the active credentials should be mapped to. Then click **Next**.
![Rotation Secrets Mapping](/images/secret-rotations-v2/oracledb-credentials/oracledb-credentials-secrets-mapping.png)

View File

@@ -53,6 +53,11 @@ description: "Learn how to automatically rotate PostgreSQL credentials."
- **Database Username 1** - the username of the first user that will be used for rotation.
- **Database Username 2** - the username of the second user that will be used for rotation.
![Rotation Advance Parameters](/images/secret-rotations-v2/postgres-credentials/postgres-credentials-advance-parameters.png)
- **Rotation Statement** - the template string query to generate password for the rotated user.
- **Password Requirements** - the requirements for the password of the MySQL users that will be created for the rotation.
5. Specify the secret names that the active credentials should be mapped to. Then click **Next**.
![Rotation Secrets Mapping](/images/secret-rotations-v2/postgres-credentials/postgres-credentials-secrets-mapping.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 837 KiB

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 780 KiB

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 712 KiB

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 822 KiB

After

Width:  |  Height:  |  Size: 603 KiB

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

@@ -0,0 +1,66 @@
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 { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
import { SecretRotation, useSecretRotationV2Option } from "@app/hooks/api/secretRotationsV2";
export const MongoRotationParametersFields = () => {
const { control, watch } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.MongoDBCredentials;
}
>();
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="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="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>
</>
);
};

View File

@@ -7,6 +7,7 @@ import { Auth0ClientSecretRotationParametersFields } from "./Auth0ClientSecretRo
import { AwsIamUserSecretRotationParametersFields } from "./AwsIamUserSecretRotationParametersFields";
import { AzureClientSecretRotationParametersFields } from "./AzureClientSecretRotationParametersFields";
import { LdapPasswordRotationParametersFields } from "./LdapPasswordRotationParametersFields";
import { MongoRotationParametersFields } from "./MongoRotationParametersFields";
import { OktaClientSecretRotationParametersFields } from "./OktaClientSecretRotationParametersFields";
import { RedisCredentialsRotationParametersFields } from "./RedisCredentialsRotationParametersFields";
import { SqlCredentialsRotationParametersFields } from "./shared";
@@ -22,7 +23,7 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields,
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationParametersFields,
[SecretRotation.RedisCredentials]: RedisCredentialsRotationParametersFields,
[SecretRotation.MongoDBCredentials]: SqlCredentialsRotationParametersFields
[SecretRotation.MongoDBCredentials]: MongoRotationParametersFields
};
export const SecretRotationV2ParametersFields = () => {

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;
};
};

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -e
CONTAINER_NAME="mssql"
SA_PASSWORD="StrongP@ssw0rd!"
IMAGE="mcr.microsoft.com/mssql/server:2022-latest"
echo "🚀 Starting SQL Server 2022 container..."
docker run -d \
--platform=linux/amd64 \
--name "${CONTAINER_NAME}" \
-p 1433:1433 \
-e "ACCEPT_EULA=Y" \
-e "SA_PASSWORD=${SA_PASSWORD}" \
"${IMAGE}"
echo "⏳ Waiting for SQL Server to be ready..."
# Check logs for ready message
until docker logs "${CONTAINER_NAME}" 2>&1 | grep -q "SQL Server is now ready for client connections."; do
sleep 5
done
echo ""
echo "✅ SQL Server is ready!"
echo ""
echo "🔐 Connection details:"
echo "------------------------------------"
echo "Host: localhost"
echo "Port: 1433"
echo "Username: sa"
echo "Password: ${SA_PASSWORD}"
echo "Database: master"
echo ""
echo "📎 JDBC URL:"
echo "jdbc:sqlserver://localhost:1433;databaseName=master;user=sa;password=${SA_PASSWORD}"
echo ""
echo "🧪 Connect using sqlcmd:"
echo "sqlcmd -S localhost,1433 -U sa -P '${SA_PASSWORD}'"
echo "------------------------------------"

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -e
CONTAINER_NAME="oracle-free"
ORACLE_PASSWORD="oracle"
IMAGE="gvenzl/oracle-free:23-slim"
echo "🚀 Starting Oracle Free 23c container..."
docker run -d \
--name "${CONTAINER_NAME}" \
-p 1521:1521 \
-p 5500:5500 \
-e ORACLE_PASSWORD="${ORACLE_PASSWORD}" \
"${IMAGE}"
echo "⏳ Waiting for Oracle database to be ready..."
until docker logs "${CONTAINER_NAME}" 2>&1 | grep -q "DATABASE IS READY TO USE"; do
sleep 5
done
echo ""
echo "✅ Oracle Database is ready!"
echo ""
echo "🔐 Connection details:"
echo "------------------------------------"
echo "Host: localhost"
echo "Port: 1521"
echo "Service: FREEPDB1"
echo "Username: system"
echo "Password: ${ORACLE_PASSWORD}"
echo ""
echo "📎 JDBC URL:"
echo "jdbc:oracle:thin:@localhost:1521/FREEPDB1"
echo ""
echo "🧪 Connect using SQL*Plus:"
echo "sqlplus system/${ORACLE_PASSWORD}@FREEPDB1"
echo "------------------------------------"