add a toggle for resource account rotation

This commit is contained in:
x032205
2025-10-24 05:42:43 -04:00
parent 4cdb8cf4c5
commit 525568683f
3 changed files with 70 additions and 73 deletions

View File

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

View File

@@ -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<typeof formSchema>;

View File

@@ -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 (
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="advance-settings" className="data-[state=open]:border-none">
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
<div className="order-1 ml-3">Rotation Account</div>
</AccordionTrigger>
<AccordionContent childrenClassName="px-0 py-0">
<p className="mb-2 text-xs">
Credentials of the privileged account which will be used for rotating other accounts
under this resource
</p>
<div className="flex flex-col gap-2">
<Switch
id="account-rotation"
onCheckedChange={(value) => {
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"
>
<p className="ml-2">Credential Rotation</p>
</Switch>
{enabled && (
<>
<div className="flex gap-2">
<Controller
name="rotationAccountCredentials.username"
@@ -48,15 +82,6 @@ export const SqlRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
>
<div className="relative">
<Input {...field} autoComplete="off" />
{field.value && (
<button
type="button"
className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3"
onClick={() => field.onChange("")}
>
<FontAwesomeIcon icon={faTimes} className="text-gray-500" />
</button>
)}
</div>
</FormControl>
)}
@@ -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 && (
<button
type="button"
className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3"
onClick={() => field.onChange("")}
>
<FontAwesomeIcon icon={faTimes} className="text-gray-500" />
</button>
)}
</div>
</FormControl>
)}
/>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<p className="mb-2 text-xs text-mineshaft-400">
Credentials of the privileged account which will be used for rotating other accounts
under this resource
</p>
</>
)}
</div>
);
};