review fixes

This commit is contained in:
x032205
2025-10-20 19:13:14 -04:00
parent 777b67555b
commit 179380f82b
12 changed files with 176 additions and 83 deletions

View File

@@ -516,6 +516,7 @@ export enum EventType {
PAM_ACCOUNT_UPDATE = "pam-account-update",
PAM_ACCOUNT_DELETE = "pam-account-delete",
PAM_ACCOUNT_CREDENTIAL_ROTATION = "pam-account-credential-rotation",
PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED = "pam-account-credential-rotation-failed",
PAM_RESOURCE_LIST = "pam-resource-list",
PAM_RESOURCE_GET = "pam-resource-get",
PAM_RESOURCE_CREATE = "pam-resource-create",
@@ -3834,6 +3835,16 @@ interface PamAccountCredentialRotationEvent {
};
}
interface PamAccountCredentialRotationFailedEvent {
type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED;
metadata: {
accountName: string;
accountId: string;
resourceId: string;
errorMessage: string;
};
}
interface PamResourceListEvent {
type: EventType.PAM_RESOURCE_LIST;
metadata: {
@@ -4225,6 +4236,7 @@ export type Event =
| PamAccountUpdateEvent
| PamAccountDeleteEvent
| PamAccountCredentialRotationEvent
| PamAccountCredentialRotationFailedEvent
| PamResourceListEvent
| PamResourceGetEvent
| PamResourceCreateEvent

View File

@@ -18,7 +18,8 @@ export const pamAccountDALFactory = (db: TDbClient) => {
.select(
// resource
db.ref("name").withSchema(TableName.PamResource).as("resourceName"),
db.ref("resourceType").withSchema(TableName.PamResource)
db.ref("resourceType").withSchema(TableName.PamResource),
db.ref("encryptedRotationAccountCredentials").withSchema(TableName.PamResource)
);
if (filter) {
@@ -28,15 +29,18 @@ export const pamAccountDALFactory = (db: TDbClient) => {
const accounts = await query;
return accounts.map(({ resourceId, resourceName, resourceType, ...account }) => ({
...account,
resourceId,
resource: {
id: resourceId,
name: resourceName,
resourceType
}
}));
return accounts.map(
({ resourceId, resourceName, resourceType, encryptedRotationAccountCredentials, ...account }) => ({
...account,
resourceId,
resource: {
id: resourceId,
name: resourceName,
resourceType,
encryptedRotationAccountCredentials
}
})
);
};
const findAccountsDueForRotation = async (tx?: Knex) => {

View File

@@ -330,7 +330,7 @@ export const pamAccountServiceFactory = ({
const decryptedAndPermittedAccounts: Array<
TPamAccounts & {
resource: Pick<TPamResources, "id" | "name" | "resourceType">;
resource: Pick<TPamResources, "id" | "name" | "resourceType"> & { rotationCredentialsConfigured: boolean };
credentials: TPamAccountCredentials;
}
> = [];
@@ -360,7 +360,8 @@ export const pamAccountServiceFactory = ({
resource: {
id: account.resource.id,
name: account.resource.name,
resourceType: account.resource.resourceType
resourceType: account.resource.resourceType,
rotationCredentialsConfigured: !!account.resource.encryptedRotationAccountCredentials
}
});
}
@@ -608,6 +609,25 @@ export const pamAccountServiceFactory = ({
}
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
await auditLogService.createAuditLog({
projectId: account.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
event: {
type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED,
metadata: {
accountId: account.id,
accountName: account.name,
resourceId: account.resourceId,
errorMessage
}
}
});
logger.error(error, `Failed to rotate credentials for account ${account.id}`);
}
});

View File

@@ -204,13 +204,26 @@ export const pamResourceServiceFactory = ({
finalCredentials.password = decryptedCredentials.password;
}
const validatedRotationAccountCredentials = await factory.validateAccountCredentials(finalCredentials);
try {
const validatedRotationAccountCredentials = await factory.validateAccountCredentials(finalCredentials);
updateDoc.encryptedRotationAccountCredentials = await encryptAccountCredentials({
credentials: validatedRotationAccountCredentials,
projectId: resource.projectId,
kmsService
});
updateDoc.encryptedRotationAccountCredentials = await encryptAccountCredentials({
credentials: validatedRotationAccountCredentials,
projectId: resource.projectId,
kmsService
});
} catch (err) {
if (
err instanceof BadRequestError &&
err.message === "Account credentials invalid: Username or password incorrect"
) {
throw new BadRequestError({
message: "Rotation Account credentials invalid: Username or password incorrect"
});
}
throw err;
}
}
}

View File

@@ -43,11 +43,3 @@ Here’s how it works:
![Rotate Credentials Account](/images/pam/overview/rotate-credentials-account.png)
Infisical will then use the rotation account on the resource to automatically update the credentials of the target account at the specified interval, eliminating credential staleness.
## FAQ
<AccordionGroup>
<Accordion title="What resources does Infisical PAM currently support?">
Infisical PAM currently supports PostgreSQL, with support for more databases, RDP, Kubernetes, social media accounts, and more coming soon.
</Accordion>
</AccordionGroup>

View File

@@ -12,6 +12,7 @@ export const pamKeys = {
session: () => [...pamKeys.all, "session"] as const,
listResourceOptions: () => [...pamKeys.resource(), "options"] as const,
listResources: (projectId: string) => [...pamKeys.resource(), "list", projectId],
getResource: (resourceId?: string) => [...pamKeys.resource(), "get", resourceId],
listAccounts: (projectId: string) => [...pamKeys.account(), "list", projectId],
getSession: (sessionId: string) => [...pamKeys.session(), "get", sessionId],
listSessions: (projectId: string) => [...pamKeys.session(), "list", projectId]
@@ -68,6 +69,27 @@ export const useListPamResources = (
});
};
export const useGetPamResourceById = (
resourceId?: string,
options?: Omit<
UseQueryOptions<TPamResource, unknown, TPamResource, ReturnType<typeof pamKeys.getResource>>,
"queryKey" | "queryFn" | "enabled"
>
) => {
return useQuery({
queryKey: pamKeys.getResource(resourceId),
queryFn: async () => {
const { data } = await apiRequest.get<{ resource: TPamResource }>(
`/api/v1/pam/resources/${resourceId}`
);
return data.resource;
},
enabled: !!resourceId,
...options
});
};
// Accounts
export const useListPamAccounts = (
projectId: string,

View File

@@ -9,6 +9,7 @@ export interface TBasePamAccount {
id: string;
name: string;
resourceType: PamResourceType;
rotationCredentialsConfigured: boolean;
};
name: string;
description?: string | null;

View File

@@ -65,7 +65,7 @@ const CreateForm = ({
switch (resourceType) {
case PamResourceType.Postgres:
return <PostgresAccountForm onSubmit={onSubmit} />;
return <PostgresAccountForm onSubmit={onSubmit} resourceId={resourceId} />;
default:
throw new Error(`Unhandled resource: ${resourceType}`);
}

View File

@@ -1,9 +1,10 @@
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 { TPostgresAccount, useGetPamResourceById } from "@app/hooks/api/pam";
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
import { SqlAccountFields } from "./shared/SqlAccountFields";
@@ -12,6 +13,7 @@ import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountF
type Props = {
account?: TPostgresAccount;
resourceId?: string;
onSubmit: (formData: FormData) => Promise<void>;
};
@@ -21,7 +23,7 @@ const formSchema = genericAccountFieldsSchema.extend(rotateAccountFieldsSchema.s
type FormData = z.infer<typeof formSchema>;
export const PostgresAccountForm = ({ account, onSubmit }: Props) => {
export const PostgresAccountForm = ({ account, resourceId, onSubmit }: Props) => {
const isUpdate = Boolean(account);
const form = useForm<FormData>({
@@ -42,6 +44,18 @@ export const PostgresAccountForm = ({ account, onSubmit }: Props) => {
formState: { isSubmitting, isDirty }
} = form;
const [rotationCredentialsConfigured, setRotationCredentialsConfigured] = useState(false);
const { data: resource } = useGetPamResourceById(resourceId);
useEffect(() => {
if (account) {
setRotationCredentialsConfigured(account.resource.rotationCredentialsConfigured);
} else {
setRotationCredentialsConfigured(!!resource?.rotationAccountCredentials);
}
}, [account]);
return (
<FormProvider {...form}>
<form
@@ -51,7 +65,7 @@ export const PostgresAccountForm = ({ account, onSubmit }: Props) => {
>
<GenericAccountFields />
<SqlAccountFields isUpdate={isUpdate} />
<RotateAccountFields />
<RotateAccountFields rotationCredentialsConfigured={rotationCredentialsConfigured} />
<div className="mt-6 flex items-center">
<Button
className="mr-4"

View File

@@ -1,14 +1,19 @@
import { Controller, useFormContext } from "react-hook-form";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
import { FormControl, Select, SelectItem, Switch } from "@app/components/v2";
import { FormControl, Select, SelectItem, Switch, Tooltip } from "@app/components/v2";
export const rotateAccountFieldsSchema = z.object({
rotationEnabled: z.boolean(),
rotationIntervalSeconds: z.number()
});
export const RotateAccountFields = () => {
export const RotateAccountFields = ({
rotationCredentialsConfigured
}: {
rotationCredentialsConfigured: boolean;
}) => {
const { control, watch } = useFormContext<{
rotationEnabled: boolean;
rotationIntervalSeconds: number;
@@ -17,55 +22,63 @@ export const RotateAccountFields = () => {
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="rotation-enabled"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
/>
</FormControl>
<Tooltip 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"
)}
/>
<span className="text-sm">Rotate Credentials Every</span>
>
<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.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: "130px"
}}
<Controller
name="rotationIntervalSeconds"
control={control}
defaultValue={2592000}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
className="mb-0"
>
<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>
<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 || !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>
);
};

View File

@@ -17,7 +17,7 @@ export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
isError={Boolean(error?.message)}
label="Username"
>
<Input {...field} />
<Input {...field} autoComplete="off" />
</FormControl>
)}
/>
@@ -34,6 +34,7 @@ export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
<Input
{...field}
type="password"
autoComplete="new-password"
onFocus={(e) => {
if (isUpdate && field.value === "******") {
field.onChange("");

View File

@@ -16,11 +16,11 @@ export const SqlRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
<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>
<div className="order-1 ml-3">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
Credentials of the privileged account which will be used for rotating other accounts
under this resource
</p>
<div className="flex gap-2">
@@ -34,7 +34,7 @@ export const SqlRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
isError={Boolean(error?.message)}
label="Username"
>
<Input {...field} />
<Input {...field} autoComplete="off" />
</FormControl>
)}
/>
@@ -51,6 +51,7 @@ export const SqlRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
<Input
{...field}
type="password"
autoComplete="new-password"
onFocus={(e) => {
if (isUpdate && field.value === "******") {
field.onChange("");