mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #5001 from Infisical/chore/aws-iam-pam-docs
improvement(pam): add AWS IAM resource documentation and form enhancements
This commit is contained in:
@@ -2,8 +2,10 @@ import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@@ -25,11 +27,23 @@ import {
|
||||
|
||||
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
|
||||
|
||||
const AWS_STS_MIN_SESSION_DURATION = 900; // 15 minutes
|
||||
const AWS_STS_MAX_SESSION_DURATION_ROLE_CHAINING = 3600; // 1 hour
|
||||
|
||||
type SubmitData = {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
credentials: {
|
||||
targetRoleArn: string;
|
||||
defaultSessionDuration: number;
|
||||
};
|
||||
};
|
||||
|
||||
type Props = {
|
||||
account?: TAwsIamAccount;
|
||||
resourceId?: string;
|
||||
resourceType?: PamResourceType;
|
||||
onSubmit: (formData: FormData) => Promise<void>;
|
||||
onSubmit: (formData: SubmitData) => Promise<void>;
|
||||
};
|
||||
|
||||
const arnRoleRegex = /^arn:aws:iam::\d{12}:role\/[\w+=,.@/-]+$/;
|
||||
@@ -42,12 +56,29 @@ const AwsIamCredentialsSchema = z.object({
|
||||
.refine((val) => arnRoleRegex.test(val), {
|
||||
message: "ARN must be in the format 'arn:aws:iam::123456789012:role/RoleName'"
|
||||
}),
|
||||
// Max 1 hour (3600s) due to AWS role chaining limitation, min 15 min (900s)
|
||||
defaultSessionDuration: z.coerce
|
||||
.number()
|
||||
.min(900, "Minimum session duration is 900 seconds (15 minutes)")
|
||||
.max(3600, "Maximum session duration is 3600 seconds (1 hour)")
|
||||
.default(3600)
|
||||
defaultSessionDuration: z.string().superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
if (typeof valMs !== "number" || valMs <= 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Invalid duration format. Use formats like 15m, 30m, 1h"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const valSeconds = valMs / 1000;
|
||||
if (valSeconds < AWS_STS_MIN_SESSION_DURATION) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Minimum session duration is 15 minutes (15m)"
|
||||
});
|
||||
}
|
||||
if (valSeconds > AWS_STS_MAX_SESSION_DURATION_ROLE_CHAINING) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Maximum session duration is 1 hour (1h) due to AWS role chaining"
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const formSchema = genericAccountFieldsSchema.extend({
|
||||
@@ -66,17 +97,17 @@ export const AwsIamAccountForm = ({ account, resourceId, resourceType, onSubmit
|
||||
enabled: !!resourceIdToFetch && !!resourceTypeToFetch
|
||||
});
|
||||
|
||||
const pamRoleArn =
|
||||
const resourceRoleArn =
|
||||
(resource?.resourceType === PamResourceType.AwsIam &&
|
||||
(resource as TAwsIamResource).connectionDetails?.roleArn) ||
|
||||
"arn:aws:iam::<YOUR_ACCOUNT_ID>:role/<YOUR_PAM_ROLE_NAME>";
|
||||
"arn:aws:iam::<YOUR_ACCOUNT_ID>:role/<YOUR_RESOURCE_ROLE_NAME>";
|
||||
|
||||
const targetRoleTrustPolicy = `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": "${pamRoleArn}"
|
||||
"AWS": "${resourceRoleArn}"
|
||||
},
|
||||
"Action": "sts:AssumeRole",
|
||||
"Condition": {
|
||||
@@ -87,16 +118,36 @@ export const AwsIamAccountForm = ({ account, resourceId, resourceType, onSubmit
|
||||
}]
|
||||
}`;
|
||||
|
||||
// Convert seconds to human-readable format for existing accounts
|
||||
const getDefaultSessionDuration = () => {
|
||||
if (account?.credentials?.defaultSessionDuration) {
|
||||
const seconds = account.credentials.defaultSessionDuration;
|
||||
if (seconds >= 3600 && seconds % 3600 === 0) {
|
||||
return `${seconds / 3600}h`;
|
||||
}
|
||||
return `${seconds / 60}m`;
|
||||
}
|
||||
return "1h";
|
||||
};
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: account ?? {
|
||||
name: "",
|
||||
description: "",
|
||||
credentials: {
|
||||
targetRoleArn: "",
|
||||
defaultSessionDuration: 3600
|
||||
}
|
||||
}
|
||||
defaultValues: account
|
||||
? {
|
||||
...account,
|
||||
credentials: {
|
||||
...account.credentials,
|
||||
defaultSessionDuration: getDefaultSessionDuration()
|
||||
}
|
||||
}
|
||||
: {
|
||||
name: "",
|
||||
description: "",
|
||||
credentials: {
|
||||
targetRoleArn: "",
|
||||
defaultSessionDuration: "1h"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -105,9 +156,23 @@ export const AwsIamAccountForm = ({ account, resourceId, resourceType, onSubmit
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
const handleFormSubmit = async (formData: FormData) => {
|
||||
const durationMs = ms(formData.credentials.defaultSessionDuration);
|
||||
const durationSeconds = Math.floor(durationMs / 1000);
|
||||
|
||||
await onSubmit({
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
credentials: {
|
||||
targetRoleArn: formData.credentials.targetRoleArn,
|
||||
defaultSessionDuration: durationSeconds
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<GenericAccountFields />
|
||||
|
||||
<div className="mb-4 rounded-sm border border-mineshaft-600 bg-mineshaft-700/70 p-3">
|
||||
@@ -139,12 +204,12 @@ export const AwsIamAccountForm = ({ account, resourceId, resourceType, onSubmit
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-0"
|
||||
helperText="In seconds. Min 900 (15m), max 3600 (1h) due to AWS role chaining limit."
|
||||
helperText="Min 15m, max 1h due to AWS role chaining limit."
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Default Session Duration (seconds)"
|
||||
label={<TtlFormLabel label="Default Session Duration" />}
|
||||
>
|
||||
<Input {...field} type="number" placeholder="3600" />
|
||||
<Input {...field} placeholder="1h" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -164,10 +229,9 @@ export const AwsIamAccountForm = ({ account, resourceId, resourceType, onSubmit
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-2.5">
|
||||
<p className="mb-3 text-sm text-mineshaft-300">
|
||||
The target role must have a trust policy that allows the PAM role (created in the
|
||||
"Resources" tab) to assume it. If your target role name follows the
|
||||
wildcard pattern you defined in the PAM role's permissions policy, no
|
||||
additional changes are needed.
|
||||
The target role must have a trust policy that allows the Resource Role (created in
|
||||
the "Resources" tab) to assume it. Ensure the target role's trust
|
||||
policy includes the Resource Role as a trusted principal.
|
||||
</p>
|
||||
|
||||
<p className="mb-2 text-sm font-medium text-mineshaft-200">
|
||||
@@ -182,12 +246,11 @@ export const AwsIamAccountForm = ({ account, resourceId, resourceType, onSubmit
|
||||
</pre>
|
||||
</div>
|
||||
<p className="text-xs text-mineshaft-400">
|
||||
<strong>Note:</strong> The Principal role ARN shown above is from the PAM Resource
|
||||
<strong>Note:</strong> The Principal role ARN shown above is from the Resource
|
||||
selected for this account. The External ID{" "}
|
||||
<code className="rounded bg-mineshaft-700 px-1 font-bold">{projectId}</code> is your
|
||||
current project ID. If your target role name doesn't match the wildcard pattern
|
||||
in your PAM Resource's role's permissions policy, you'll need to
|
||||
update that policy to include this role's ARN.
|
||||
current project ID. If you configured granular permissions in your Resource
|
||||
Role's policy, ensure this target role's ARN is included.
|
||||
</p>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { DocumentationLinkBadge } from "@app/components/v3";
|
||||
import { PAM_RESOURCE_TYPE_MAP, PamResourceType } from "@app/hooks/api/pam";
|
||||
|
||||
const PAM_ACCOUNT_DOCS_MAP: Partial<Record<PamResourceType, string>> = {
|
||||
[PamResourceType.AwsIam]: "aws-iam#create-pam-accounts"
|
||||
};
|
||||
|
||||
type Props = {
|
||||
resourceName: string;
|
||||
resourceType: PamResourceType;
|
||||
@@ -8,6 +13,7 @@ type Props = {
|
||||
|
||||
export const PamAccountHeader = ({ resourceName, resourceType, onBack }: Props) => {
|
||||
const details = PAM_RESOURCE_TYPE_MAP[resourceType];
|
||||
const docsPath = PAM_ACCOUNT_DOCS_MAP[resourceType];
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex w-full items-start gap-2 border-b border-mineshaft-500 pb-4">
|
||||
@@ -17,7 +23,14 @@ export const PamAccountHeader = ({ resourceName, resourceType, onBack }: Props)
|
||||
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center text-mineshaft-300">{resourceName}</div>
|
||||
<div className="flex items-center gap-x-2 text-mineshaft-300">
|
||||
{resourceName}
|
||||
{docsPath && (
|
||||
<DocumentationLinkBadge
|
||||
href={`https://infisical.com/docs/documentation/platform/pam/resources/${docsPath}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm leading-4 text-mineshaft-400">{details.name} resource</p>
|
||||
</div>
|
||||
{onBack && (
|
||||
|
||||
@@ -30,7 +30,7 @@ const AwsIamConnectionDetailsSchema = z.object({
|
||||
roleArn: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "PAM Role ARN is required")
|
||||
.min(1, "Resource Role ARN is required")
|
||||
.refine((val) => arnRoleRegex.test(val), {
|
||||
message: "ARN must be in the format 'arn:aws:iam::123456789012:role/RoleName'"
|
||||
})
|
||||
@@ -57,7 +57,7 @@ export const AwsIamResourceForm = ({ resource, onSubmit }: Props) => {
|
||||
"Statement": [{
|
||||
"Effect": "Allow",
|
||||
"Action": "sts:AssumeRole",
|
||||
"Resource": "arn:aws:iam::<YOUR_ACCOUNT_ID>:role/<YOUR_PREFIX>-*"
|
||||
"Resource": "arn:aws:iam::<YOUR_ACCOUNT_ID>:role/*"
|
||||
}]
|
||||
}`;
|
||||
|
||||
@@ -116,12 +116,15 @@ export const AwsIamResourceForm = ({ resource, onSubmit }: Props) => {
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
helperText="The ARN of the Infisical PAM role that can assume target roles"
|
||||
helperText="The ARN of the Infisical Resource Role that can assume target roles"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="PAM Role ARN"
|
||||
label="Resource Role ARN"
|
||||
>
|
||||
<Input placeholder="arn:aws:iam::123456789012:role/InfisicalPAMRole" {...field} />
|
||||
<Input
|
||||
placeholder="arn:aws:iam::123456789012:role/InfisicalResourceRole"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -148,12 +151,12 @@ export const AwsIamResourceForm = ({ resource, onSubmit }: Props) => {
|
||||
Step 1: Create a permissions policy for assuming target roles
|
||||
</p>
|
||||
<p className="mb-3 text-sm text-mineshaft-300">
|
||||
This policy allows the PAM role to assume target roles. We recommend using a
|
||||
wildcard pattern (e.g.,{" "}
|
||||
<code className="rounded bg-mineshaft-700 px-1 text-xs">pam-*</code> or{" "}
|
||||
<code className="rounded bg-mineshaft-700 px-1 text-xs">privileged-*</code>) so you
|
||||
can add new accounts without updating this policy. Choose a prefix that fits your
|
||||
naming conventions.
|
||||
This policy allows the Resource Role to assume target roles. For simplicity, use a
|
||||
wildcard to allow assuming any role in your account. For more granular control,
|
||||
replace <code className="rounded bg-mineshaft-700 px-1 text-xs">*</code> with a
|
||||
specific pattern like{" "}
|
||||
<code className="rounded bg-mineshaft-700 px-1 text-xs">/pam-*</code> or{" "}
|
||||
<code className="rounded bg-mineshaft-700 px-1 text-xs">/infisical-*</code>.
|
||||
</p>
|
||||
<div className="relative mb-4">
|
||||
<div className="absolute top-1 right-1">
|
||||
@@ -165,12 +168,12 @@ export const AwsIamResourceForm = ({ resource, onSubmit }: Props) => {
|
||||
</div>
|
||||
|
||||
<p className="mb-2 text-sm font-medium text-mineshaft-200">
|
||||
Step 2: Create the PAM role with a trust policy
|
||||
Step 2: Create the Resource Role with a trust policy
|
||||
</p>
|
||||
<p className="mb-3 text-sm text-mineshaft-300">
|
||||
Create an IAM role (e.g.,{" "}
|
||||
<code className="rounded bg-mineshaft-700 px-1 text-xs">InfisicalPAMRole</code>)
|
||||
with the permissions policy above and the following trust policy:
|
||||
<code className="rounded bg-mineshaft-700 px-1 text-xs">InfisicalResourceRole</code>
|
||||
) with the permissions policy above and the following trust policy:
|
||||
</p>
|
||||
<div className="relative mb-4">
|
||||
<div className="absolute top-1 right-3">
|
||||
@@ -189,11 +192,8 @@ export const AwsIamResourceForm = ({ resource, onSubmit }: Props) => {
|
||||
<code className="rounded bg-mineshaft-700 px-1 font-bold">
|
||||
{INFISICAL_AWS_ACCOUNT_EU}
|
||||
</code>{" "}
|
||||
for EU region. Replace{" "}
|
||||
<code className="rounded bg-mineshaft-700 px-1 font-bold">
|
||||
<INFISICAL_AWS_ACCOUNT_ID>
|
||||
</code>{" "}
|
||||
with the appropriate Infisical AWS account ID for your region. The External ID{" "}
|
||||
for EU region. For dedicated instances, contact Infisical support. For self-hosted
|
||||
instances, use your Infisical deployment's AWS account ID. The External ID{" "}
|
||||
<code className="rounded bg-mineshaft-700 px-1 font-bold">{projectId}</code> is your
|
||||
current project ID.
|
||||
</p>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { DocumentationLinkBadge } from "@app/components/v3";
|
||||
import { PAM_RESOURCE_TYPE_MAP, PamResourceType } from "@app/hooks/api/pam";
|
||||
|
||||
const PAM_RESOURCE_DOCS_MAP: Partial<Record<PamResourceType, string>> = {
|
||||
[PamResourceType.AwsIam]: "aws-iam#create-the-pam-resource"
|
||||
};
|
||||
|
||||
type Props = {
|
||||
resourceType: PamResourceType;
|
||||
onBack?: () => void;
|
||||
@@ -7,6 +12,7 @@ type Props = {
|
||||
|
||||
export const PamResourceHeader = ({ resourceType, onBack }: Props) => {
|
||||
const details = PAM_RESOURCE_TYPE_MAP[resourceType];
|
||||
const docsPath = PAM_RESOURCE_DOCS_MAP[resourceType];
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex w-full items-start gap-2 border-b border-mineshaft-500 pb-4">
|
||||
@@ -16,7 +22,14 @@ export const PamResourceHeader = ({ resourceType, onBack }: Props) => {
|
||||
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center text-mineshaft-300">{details.name}</div>
|
||||
<div className="flex items-center gap-x-2 text-mineshaft-300">
|
||||
{details.name}
|
||||
{docsPath && (
|
||||
<DocumentationLinkBadge
|
||||
href={`https://infisical.com/docs/documentation/platform/pam/resources/${docsPath}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm leading-4 text-mineshaft-400">Resource</p>
|
||||
</div>
|
||||
{onBack && (
|
||||
|
||||
Reference in New Issue
Block a user