Merge pull request #5071 from Infisical/feat/rotation-pass-config
feat: sql rotation query option and password policy enforcement
@@ -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<
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Available"
|
||||
openapi: "GET /api/v1/app-connections/mongodb/available"
|
||||
---
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/mongodb/{connectionId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/mongodb/{connectionId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/app-connections/mongodb/connection-name/{connectionName}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections/mongodb"
|
||||
---
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v2/secret-rotations/mongodb-credentials/{rotationId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v2/secret-rotations/mongodb-credentials/{rotationId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v2/secret-rotations/mongodb-credentials/rotation-name/{rotationName}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get Credentials by ID"
|
||||
openapi: "GET /api/v2/secret-rotations/mongodb-credentials/{rotationId}/generated-credentials"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v2/secret-rotations/mongodb-credentials"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Rotate Secrets"
|
||||
openapi: "POST /api/v2/secret-rotations/mongodb-credentials/{rotationId}/rotate-secrets"
|
||||
---
|
||||
@@ -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>
|
||||
@@ -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 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**.
|
||||

|
||||
|
||||
|
||||
@@ -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 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**.
|
||||

|
||||
|
||||
|
||||
@@ -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 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**.
|
||||

|
||||
|
||||
|
||||
@@ -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 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**.
|
||||

|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 436 KiB |
|
Before Width: | Height: | Size: 837 KiB After Width: | Height: | Size: 563 KiB |
|
After Width: | Height: | Size: 430 KiB |
|
Before Width: | Height: | Size: 780 KiB After Width: | Height: | Size: 544 KiB |
|
After Width: | Height: | Size: 428 KiB |
|
Before Width: | Height: | Size: 712 KiB After Width: | Height: | Size: 531 KiB |
|
After Width: | Height: | Size: 483 KiB |
|
Before Width: | Height: | Size: 822 KiB After Width: | Height: | Size: 603 KiB |
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -27,6 +27,7 @@ export type TSqlCredentialsRotationOption = {
|
||||
template: {
|
||||
secretsMapping: TSqlCredentialsRotationProperties["secretsMapping"];
|
||||
createUserStatement: string;
|
||||
rotationStatement: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
40
sink/docker-command-mssql.sh
Normal 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 "------------------------------------"
|
||||
38
sink/docker-command-oracle.sh
Normal 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 "------------------------------------"
|
||||