From 4cdb8cf4c57aa82abfb43651bd2b9f968903a24d Mon Sep 17 00:00:00 2001 From: x032205 Date: Wed, 22 Oct 2025 23:40:30 -0400 Subject: [PATCH 1/2] fix: allow rotation account credentials to be removed from resources --- backend/src/lib/config/env.ts | 2 +- .../shared/sql-account-schemas.ts | 28 +++++++++ .../PamResourceForm/PostgresResourceForm.tsx | 4 +- .../shared/SqlRotateAccountFields.tsx | 60 +++++++++++++------ 4 files changed, 73 insertions(+), 21 deletions(-) diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 2da7a245a..9fc4cff92 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -3,13 +3,13 @@ import { z } from "zod"; import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; import { crypto } from "@app/lib/crypto/cryptography"; import { QueueWorkerProfile } from "@app/lib/types"; +import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { BadRequestError } from "../errors"; import { removeTrailingSlash } from "../fn"; import { CustomLogger } from "../logger/logger"; import { zpStr } from "../zod"; -import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; export const GITLAB_URL = "https://gitlab.com"; diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/sql-account-schemas.ts b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/sql-account-schemas.ts index 37efd67d4..751277254 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/sql-account-schemas.ts +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/sql-account-schemas.ts @@ -12,3 +12,31 @@ export const BaseSqlAccountSchema = z.object({ .min(1, "Password required") .max(256, "Password must be 256 characters or less") }); + +export const BaseSqlRotationAccountSchema = z + .object({ + username: z.string().trim().max(63, "Username must be 63 characters or less"), + password: z.string().trim().max(256, "Password must be 256 characters or less") + }) + .superRefine((data, ctx) => { + if (data.username && !data.password) { + ctx.addIssue({ + path: ["password"], + message: "Password is required", + code: z.ZodIssueCode.custom + }); + } + if (data.password && !data.username) { + ctx.addIssue({ + path: ["username"], + message: "Username is required", + code: z.ZodIssueCode.custom + }); + } + }) + .transform((val) => { + if (!val.username && !val.password) { + return null; + } + return val; + }); diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx index 7e96fffda..fbca7bc87 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx @@ -6,7 +6,7 @@ import { z } from "zod"; import { Button, ModalClose } from "@app/components/v2"; import { PamResourceType, TPostgresResource } from "@app/hooks/api/pam"; import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants"; -import { BaseSqlAccountSchema } from "@app/pages/pam/PamAccountsPage/components/PamAccountForm/shared/sql-account-schemas"; +import { BaseSqlRotationAccountSchema } from "@app/pages/pam/PamAccountsPage/components/PamAccountForm/shared/sql-account-schemas"; import { BaseSqlResourceSchema } from "./shared/sql-resource-schemas"; import { SqlResourceFields } from "./shared/SqlResourceFields"; @@ -21,7 +21,7 @@ type Props = { const formSchema = genericResourceFieldsSchema.extend({ resourceType: z.literal(PamResourceType.Postgres), connectionDetails: BaseSqlResourceSchema, - rotationAccountCredentials: BaseSqlAccountSchema.nullable().optional() + rotationAccountCredentials: BaseSqlRotationAccountSchema.nullable().optional() }); type FormData = z.infer; diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlRotateAccountFields.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlRotateAccountFields.tsx index e7e9023dd..c3a98a3f6 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlRotateAccountFields.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlRotateAccountFields.tsx @@ -1,5 +1,7 @@ import { useEffect, useState } from "react"; import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { faTimes } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Accordion, @@ -44,7 +46,18 @@ export const SqlRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => { isError={Boolean(error?.message)} label="Username" > - +
+ + {field.value && ( + + )} +
)} /> @@ -58,23 +71,34 @@ export const SqlRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => { isError={Boolean(error?.message)} label="Password" > - { - if (isUpdate && field.value === UNCHANGED_PASSWORD_SENTINEL) { - field.onChange(""); - } - setShowPassword(true); - }} - onBlur={() => { - if (isUpdate && field.value === "") { - field.onChange(UNCHANGED_PASSWORD_SENTINEL); - } - setShowPassword(false); - }} - /> +
+ { + if (isUpdate && field.value === UNCHANGED_PASSWORD_SENTINEL) { + field.onChange(""); + } + setShowPassword(true); + }} + onBlur={() => { + if (isUpdate && field.value === "") { + field.onChange(UNCHANGED_PASSWORD_SENTINEL); + } + setShowPassword(false); + }} + /> + {field.value && ( + + )} +
)} /> From 525568683ff18d0f391fca38522daa7bb0a75ec7 Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 24 Oct 2025 05:42:43 -0400 Subject: [PATCH 2/2] add a toggle for resource account rotation --- .../shared/sql-account-schemas.ts | 28 ----- .../PamResourceForm/PostgresResourceForm.tsx | 4 +- .../shared/SqlRotateAccountFields.tsx | 111 +++++++++++------- 3 files changed, 70 insertions(+), 73 deletions(-) diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/sql-account-schemas.ts b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/sql-account-schemas.ts index 751277254..37efd67d4 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/sql-account-schemas.ts +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/shared/sql-account-schemas.ts @@ -12,31 +12,3 @@ export const BaseSqlAccountSchema = z.object({ .min(1, "Password required") .max(256, "Password must be 256 characters or less") }); - -export const BaseSqlRotationAccountSchema = z - .object({ - username: z.string().trim().max(63, "Username must be 63 characters or less"), - password: z.string().trim().max(256, "Password must be 256 characters or less") - }) - .superRefine((data, ctx) => { - if (data.username && !data.password) { - ctx.addIssue({ - path: ["password"], - message: "Password is required", - code: z.ZodIssueCode.custom - }); - } - if (data.password && !data.username) { - ctx.addIssue({ - path: ["username"], - message: "Username is required", - code: z.ZodIssueCode.custom - }); - } - }) - .transform((val) => { - if (!val.username && !val.password) { - return null; - } - return val; - }); diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx index fbca7bc87..7e96fffda 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx @@ -6,7 +6,7 @@ import { z } from "zod"; import { Button, ModalClose } from "@app/components/v2"; import { PamResourceType, TPostgresResource } from "@app/hooks/api/pam"; import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants"; -import { BaseSqlRotationAccountSchema } from "@app/pages/pam/PamAccountsPage/components/PamAccountForm/shared/sql-account-schemas"; +import { BaseSqlAccountSchema } from "@app/pages/pam/PamAccountsPage/components/PamAccountForm/shared/sql-account-schemas"; import { BaseSqlResourceSchema } from "./shared/sql-resource-schemas"; import { SqlResourceFields } from "./shared/SqlResourceFields"; @@ -21,7 +21,7 @@ type Props = { const formSchema = genericResourceFieldsSchema.extend({ resourceType: z.literal(PamResourceType.Postgres), connectionDetails: BaseSqlResourceSchema, - rotationAccountCredentials: BaseSqlRotationAccountSchema.nullable().optional() + rotationAccountCredentials: BaseSqlAccountSchema.nullable().optional() }); type FormData = z.infer; diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlRotateAccountFields.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlRotateAccountFields.tsx index c3a98a3f6..d3f11ed60 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlRotateAccountFields.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlRotateAccountFields.tsx @@ -1,40 +1,74 @@ import { useEffect, useState } from "react"; import { Controller, useFormContext, useWatch } from "react-hook-form"; -import { faTimes } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, - FormControl, - Input -} from "@app/components/v2"; +import { FormControl, Input, Switch } from "@app/components/v2"; import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants"; export const SqlRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => { - const { control } = useFormContext(); + const { control, setValue, getValues } = useFormContext(); const [showPassword, setShowPassword] = useState(false); const password = useWatch({ control, name: "credentials.password" }); + const rotationUsername = useWatch({ control, name: "rotationAccountCredentials.username" }); + const rotationPassword = useWatch({ control, name: "rotationAccountCredentials.password" }); + + const [enabled, setEnabled] = useState(false); + const [wasRotationPasswordSentinelInitially, setWasRotationPasswordSentinelInitially] = + useState(false); + + useEffect(() => { + const initialRotationPass = getValues("rotationAccountCredentials.password"); + if (initialRotationPass === UNCHANGED_PASSWORD_SENTINEL) { + setWasRotationPasswordSentinelInitially(true); + } + }, [getValues]); + useEffect(() => { if (password === UNCHANGED_PASSWORD_SENTINEL) { setShowPassword(false); } }, [password]); + useEffect(() => { + const isUsernamePopulated = rotationUsername && rotationUsername !== ""; + const isPasswordPopulated = + rotationPassword && + rotationPassword !== "" && + rotationPassword !== UNCHANGED_PASSWORD_SENTINEL; + + if (isUsernamePopulated || isPasswordPopulated) { + setEnabled(true); + } + }, [rotationUsername, rotationPassword]); + return ( - - - -
Rotation Account
-
- -

- Credentials of the privileged account which will be used for rotating other accounts - under this resource -

+
+ { + setEnabled(value); + if (value) { + setValue("rotationAccountCredentials.username", "", { + shouldDirty: true + }); + setValue("rotationAccountCredentials.password", "", { + shouldDirty: true + }); + } else { + setValue("rotationAccountCredentials", null, { + shouldDirty: true + }); + } + }} + isChecked={enabled} + containerClassName="flex-row-reverse w-fit" + className="ml-0" + > +

Credential Rotation

+
+ + {enabled && ( + <>
{ >
- {field.value && ( - - )}
)} @@ -83,28 +108,28 @@ export const SqlRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => { setShowPassword(true); }} onBlur={() => { - if (isUpdate && field.value === "") { + if ( + isUpdate && + field.value === "" && + wasRotationPasswordSentinelInitially + ) { field.onChange(UNCHANGED_PASSWORD_SENTINEL); } setShowPassword(false); }} /> - {field.value && ( - - )}
)} />
-
-
-
+ +

+ Credentials of the privileged account which will be used for rotating other accounts + under this resource +

+ + )} + ); };