feat(pam): account credential rotation

This commit is contained in:
x032205
2025-10-16 18:12:03 -04:00
parent 4092325a02
commit f0edcd1f0f
25 changed files with 588 additions and 36 deletions
@@ -12,6 +12,9 @@ export interface TBasePamAccount {
};
name: string;
description?: string | null;
rotationEnabled: boolean;
rotationIntervalSeconds: number;
lastRotatedAt?: string | null;
createdAt: string;
updatedAt: string;
}
@@ -6,6 +6,7 @@ import { TBasePamResource } from "./base-resource";
// Resources
export type TPostgresResource = TBasePamResource & { resourceType: PamResourceType.Postgres } & {
connectionDetails: TBaseSqlConnectionDetails;
rotationAccountCredentials?: TBaseSqlCredentials | null;
};
// Accounts
@@ -37,7 +37,10 @@ const CreateForm = ({
console.log({ folderId });
const onSubmit = async (
formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials">
formData: DiscriminativePick<
TPamAccount,
"name" | "description" | "credentials" | "rotationEnabled" | "rotationIntervalSeconds"
>
) => {
try {
const account = await createPamAccount.mutateAsync({
@@ -74,7 +77,10 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => {
const updatePamAccount = useUpdatePamAccount();
const onSubmit = async (
formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials">
formData: DiscriminativePick<
TPamAccount,
"name" | "description" | "credentials" | "rotationEnabled" | "rotationIntervalSeconds"
>
) => {
try {
const updatedAccount = await updatePamAccount.mutateAsync({
@@ -8,13 +8,14 @@ import { TPostgresAccount } from "@app/hooks/api/pam";
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
import { SqlAccountFields } from "./shared/SqlAccountFields";
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountFields";
type Props = {
account?: TPostgresAccount;
onSubmit: (formData: FormData) => Promise<void>;
};
const formSchema = genericAccountFieldsSchema.extend({
const formSchema = genericAccountFieldsSchema.extend(rotateAccountFieldsSchema.shape).extend({
credentials: BaseSqlAccountSchema
});
@@ -50,6 +51,7 @@ export const PostgresAccountForm = ({ account, onSubmit }: Props) => {
>
<GenericAccountFields />
<SqlAccountFields isUpdate={isUpdate} />
<RotateAccountFields />
<div className="mt-6 flex items-center">
<Button
className="mr-4"
@@ -0,0 +1,71 @@
import { Controller, useFormContext } from "react-hook-form";
import { z } from "zod";
import { FormControl, Select, SelectItem, Switch } from "@app/components/v2";
export const rotateAccountFieldsSchema = z.object({
rotationEnabled: z.boolean(),
rotationIntervalSeconds: z.number()
});
export const RotateAccountFields = () => {
const { control, watch } = useFormContext<{
rotationEnabled: boolean;
rotationIntervalSeconds: number;
}>();
const rotationEnabled = watch("rotationEnabled");
return (
<div className="flex h-9 items-center gap-3">
<Controller
control={control}
name="rotationEnabled"
defaultValue={false}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} className="mb-0">
<Switch
className="ml-0 bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
id="can-remove-certificates"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
/>
</FormControl>
)}
/>
<span className="text-sm">Rotate Credentials Every</span>
<Controller
name="rotationIntervalSeconds"
control={control}
defaultValue={2592000}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
className="mb-0"
>
<Select
value={value.toString()}
onValueChange={(val) => onChange(parseInt(val, 10))}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
placeholder="Select an interval..."
dropdownContainerClassName="max-w-none"
isDisabled={!rotationEnabled}
dropdownContainerStyle={{
width: "120px"
}}
>
<SelectItem value="2592000">30 Days</SelectItem>
<SelectItem value="604800">7 Days</SelectItem>
<SelectItem value="259200">3 Days</SelectItem>
<SelectItem value="86400">1 Day</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
);
};
@@ -5,9 +5,11 @@ import { z } from "zod";
import { Button, ModalClose } from "@app/components/v2";
import { PamResourceType, TPostgresResource } from "@app/hooks/api/pam";
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";
import { SqlRotateAccountFields } from "./shared/SqlRotateAccountFields";
import { GenericResourceFields, genericResourceFieldsSchema } from "./GenericResourceFields";
type Props = {
@@ -17,7 +19,8 @@ type Props = {
const formSchema = genericResourceFieldsSchema.extend({
resourceType: z.literal(PamResourceType.Postgres),
connectionDetails: BaseSqlResourceSchema
connectionDetails: BaseSqlResourceSchema,
rotationAccountCredentials: BaseSqlAccountSchema.nullable().optional()
});
type FormData = z.infer<typeof formSchema>;
@@ -59,6 +62,7 @@ export const PostgresResourceForm = ({ resource, onSubmit }: Props) => {
selectedTabIndex={selectedTabIndex}
setSelectedTabIndex={setSelectedTabIndex}
/>
<SqlRotateAccountFields />
<div className="mt-6 flex items-center">
<Button
className="mr-4"
@@ -0,0 +1,64 @@
import { Controller, useFormContext } from "react-hook-form";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
FormControl,
Input,
SecretInput
} from "@app/components/v2";
export const SqlRotateAccountFields = () => {
const { control } = useFormContext();
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">Credential Rotation Account</div>
</AccordionTrigger>
<AccordionContent childrenClassName="px-0 py-0">
<p className="mb-2 text-xs">
Credentials to the high privilege account which will be used for rotating other accounts
under this resource
</p>
<div className="flex gap-2">
<Controller
name="rotationAccountCredentials.username"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-0 flex-1"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Username"
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
name="rotationAccountCredentials.password"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-0 flex-1"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Password"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:border-primary-400/50! border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
{...field}
/>
</FormControl>
)}
/>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
);
};