mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 15:38:03 -05:00
feat: implemented rotation query and password requirement
This commit is contained in:
@@ -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<
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>,
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user