mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 11:28:43 +00:00
Merge pull request #4685 from Infisical/ENG-3925
feat(pam): account credential rotation
This commit is contained in:
+15
-5
@@ -34,10 +34,11 @@ const CreateForm = ({
|
||||
}: CreateFormProps) => {
|
||||
const createPamAccount = useCreatePamAccount();
|
||||
|
||||
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({
|
||||
@@ -64,7 +65,13 @@ const CreateForm = ({
|
||||
|
||||
switch (resourceType) {
|
||||
case PamResourceType.Postgres:
|
||||
return <PostgresAccountForm onSubmit={onSubmit} />;
|
||||
return (
|
||||
<PostgresAccountForm
|
||||
onSubmit={onSubmit}
|
||||
resourceId={resourceId}
|
||||
resourceType={resourceType}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unhandled resource: ${resourceType}`);
|
||||
}
|
||||
@@ -74,7 +81,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({
|
||||
|
||||
+24
-4
@@ -1,26 +1,31 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, ModalClose } from "@app/components/v2";
|
||||
import { TPostgresAccount } from "@app/hooks/api/pam";
|
||||
import { PamResourceType, TPostgresAccount, useGetPamResourceById } from "@app/hooks/api/pam";
|
||||
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
|
||||
|
||||
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;
|
||||
resourceId?: string;
|
||||
resourceType?: PamResourceType;
|
||||
onSubmit: (formData: FormData) => Promise<void>;
|
||||
};
|
||||
|
||||
const formSchema = genericAccountFieldsSchema.extend({
|
||||
const formSchema = genericAccountFieldsSchema.extend(rotateAccountFieldsSchema.shape).extend({
|
||||
credentials: BaseSqlAccountSchema
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const PostgresAccountForm = ({ account, onSubmit }: Props) => {
|
||||
export const PostgresAccountForm = ({ account, resourceId, resourceType, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(account);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
@@ -30,7 +35,7 @@ export const PostgresAccountForm = ({ account, onSubmit }: Props) => {
|
||||
...account,
|
||||
credentials: {
|
||||
...account.credentials,
|
||||
password: "******"
|
||||
password: UNCHANGED_PASSWORD_SENTINEL
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
@@ -41,6 +46,20 @@ export const PostgresAccountForm = ({ account, onSubmit }: Props) => {
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
const [rotationCredentialsConfigured, setRotationCredentialsConfigured] = useState(false);
|
||||
|
||||
const { data: resource } = useGetPamResourceById(resourceType, resourceId, {
|
||||
enabled: !account && !!resourceId && !!resourceType
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (account) {
|
||||
setRotationCredentialsConfigured(account.resource.rotationCredentialsConfigured);
|
||||
} else {
|
||||
setRotationCredentialsConfigured(!!resource?.rotationAccountCredentials);
|
||||
}
|
||||
}, [account, resource]);
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
@@ -50,6 +69,7 @@ export const PostgresAccountForm = ({ account, onSubmit }: Props) => {
|
||||
>
|
||||
<GenericAccountFields />
|
||||
<SqlAccountFields isUpdate={isUpdate} />
|
||||
<RotateAccountFields rotationCredentialsConfigured={rotationCredentialsConfigured} />
|
||||
<div className="mt-6 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { z } from "zod";
|
||||
|
||||
import { FormControl, Select, SelectItem, Switch, Tooltip } from "@app/components/v2";
|
||||
|
||||
export const rotateAccountFieldsSchema = z.object({
|
||||
rotationEnabled: z.boolean(),
|
||||
rotationIntervalSeconds: z.number().nullable().optional()
|
||||
});
|
||||
|
||||
export const RotateAccountFields = ({
|
||||
rotationCredentialsConfigured
|
||||
}: {
|
||||
rotationCredentialsConfigured: boolean;
|
||||
}) => {
|
||||
const { control, watch } = useFormContext<{
|
||||
rotationEnabled: boolean;
|
||||
rotationIntervalSeconds?: number | null;
|
||||
}>();
|
||||
|
||||
const rotationEnabled = watch("rotationEnabled");
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
isDisabled={rotationCredentialsConfigured}
|
||||
content="The resource which owns this account does not have rotation credentials configured."
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex h-9 w-fit items-center gap-3",
|
||||
!rotationCredentialsConfigured && "opacity-50"
|
||||
)}
|
||||
>
|
||||
<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="rotation-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
isDisabled={!rotationCredentialsConfigured}
|
||||
/>
|
||||
</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 || 2592000).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 || !rotationCredentialsConfigured}
|
||||
dropdownContainerStyle={{
|
||||
width: "130px"
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
+20
-9
@@ -1,9 +1,19 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
|
||||
import { FormControl, Input } from "@app/components/v2";
|
||||
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
|
||||
|
||||
export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
|
||||
const { control } = useFormContext();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const password = useWatch({ control, name: "credentials.password" });
|
||||
|
||||
useEffect(() => {
|
||||
if (password === UNCHANGED_PASSWORD_SENTINEL) {
|
||||
setShowPassword(false);
|
||||
}
|
||||
}, [password]);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
@@ -17,7 +27,7 @@ export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
|
||||
isError={Boolean(error?.message)}
|
||||
label="Username"
|
||||
>
|
||||
<Input {...field} />
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -33,18 +43,19 @@ export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="password"
|
||||
onFocus={(e) => {
|
||||
if (isUpdate && field.value === "******") {
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="new-password"
|
||||
onFocus={() => {
|
||||
if (isUpdate && field.value === UNCHANGED_PASSWORD_SENTINEL) {
|
||||
field.onChange("");
|
||||
}
|
||||
e.target.type = "text";
|
||||
setShowPassword(true);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
onBlur={() => {
|
||||
if (isUpdate && field.value === "") {
|
||||
field.onChange("******");
|
||||
field.onChange(UNCHANGED_PASSWORD_SENTINEL);
|
||||
}
|
||||
e.target.type = "password";
|
||||
setShowPassword(false);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
+6
-2
@@ -5,6 +5,10 @@ export const BaseSqlAccountSchema = z.object({
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Username required")
|
||||
.max(255, "Username must be 255 characters or less"),
|
||||
password: z.string().trim().min(1, "Password required")
|
||||
.max(63, "Username must be 63 characters or less"),
|
||||
password: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Password required")
|
||||
.max(256, "Password must be 256 characters or less")
|
||||
});
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
faEllipsisV,
|
||||
faFolder,
|
||||
faRightToBracket,
|
||||
faRotate,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -100,6 +102,12 @@ export const PamAccountRow = ({
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
{account.lastRotatedAt && (
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 bg-orange/20 whitespace-nowrap text-orange">
|
||||
<FontAwesomeIcon icon={faRotate} />
|
||||
<span>Rotated {formatDistance(new Date(), account.lastRotatedAt)} ago</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
|
||||
Reference in New Issue
Block a user