From 6db5188b363f707fa3fe4696aa2c0656b32811c9 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Fri, 5 Dec 2025 16:56:55 -0300 Subject: [PATCH] feat: update AWS IAM session duration handling and improve account access functionality - Changed session duration parameter from maxSessionDuration to defaultSessionDuration for consistency. - Refactored AWS STS client creation to use a hardcoded default region, simplifying the configuration. - Enhanced PAM account access modal to include account path and project ID in the access request. - Updated various components and schemas to reflect the new session duration naming and improve type safety. --- .../pam-account/pam-account-service.ts | 2 +- .../aws-iam/aws-iam-federation.ts | 20 +- .../aws-iam/aws-iam-resource-factory.ts | 2 +- .../aws-iam/aws-iam-resource-schemas.ts | 5 +- frontend/src/hooks/api/pam/mutations.tsx | 6 +- .../hooks/api/pam/types/aws-iam-resource.ts | 3 +- .../components/PamAccessAccountModal.tsx | 250 ++++-------------- .../PamAccountForm/AwsIamAccountForm.tsx | 94 +++---- .../components/PamAccountsTable.tsx | 10 +- .../components/useAccessAwsIamAccount.tsx | 6 +- .../PamResourceForm/AwsIamResourceForm.tsx | 117 ++++---- 11 files changed, 207 insertions(+), 308 deletions(-) diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index f4a5f4801..bb00f2eaf 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -591,7 +591,7 @@ export const pamAccountServiceFactory = ({ targetRoleArn: awsCredentials.targetRoleArn, roleSessionName: actorEmail, projectId: account.projectId, // Use project ID as External ID for security - sessionDuration: awsCredentials.maxSessionDuration + sessionDuration: awsCredentials.defaultSessionDuration }); const session = await pamSessionDAL.create({ diff --git a/backend/src/ee/services/pam-resource/aws-iam/aws-iam-federation.ts b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-federation.ts index 2595627a1..f9be88b2f 100644 --- a/backend/src/ee/services/pam-resource/aws-iam/aws-iam-federation.ts +++ b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-federation.ts @@ -8,11 +8,17 @@ import { TAwsIamResourceConnectionDetails } from "./aws-iam-resource-types"; const AWS_STS_MIN_DURATION_SECONDS = 900; -const createStsClient = (region: string): STSClient => { +// We hardcode us-east-1 because: +// 1. IAM is global - roles can be assumed from any STS regional endpoint +// 2. The temporary credentials returned work globally across all AWS regions +// 3. The target account's resources can be in any region - it doesn't affect STS calls +const AWS_STS_DEFAULT_REGION = "us-east-1"; + +const createStsClient = (): STSClient => { const appCfg = getConfig(); const config: STSClientConfig = { - region, + region: AWS_STS_DEFAULT_REGION, useFipsEndpoint: crypto.isFipsModeEnabled(), sha256: CustomAWSHasher, credentials: @@ -31,7 +37,7 @@ export const validatePamRoleConnection = async ( connectionDetails: TAwsIamResourceConnectionDetails, projectId: string ): Promise => { - const stsClient = createStsClient(connectionDetails.region); + const stsClient = createStsClient(); try { await stsClient.send( @@ -58,7 +64,7 @@ export const validateTargetRoleAssumption = async ({ targetRoleArn: string; projectId: string; }): Promise => { - const stsClient = createStsClient(connectionDetails.region); + const stsClient = createStsClient(); try { // First assume the PAM role @@ -77,7 +83,7 @@ export const validateTargetRoleAssumption = async ({ // Then use the PAM role credentials to assume the target role const pamStsClient = new STSClient({ - region: connectionDetails.region, + region: AWS_STS_DEFAULT_REGION, useFipsEndpoint: crypto.isFipsModeEnabled(), sha256: CustomAWSHasher, credentials: { @@ -118,7 +124,7 @@ export const generateConsoleFederationUrl = async ({ projectId: string; sessionDuration: number; }): Promise<{ consoleUrl: string; expiresAt: Date }> => { - const stsClient = createStsClient(connectionDetails.region); + const stsClient = createStsClient(); // First assume the PAM role const pamRoleCredentials = await stsClient.send( @@ -136,7 +142,7 @@ export const generateConsoleFederationUrl = async ({ // Role chaining: use PAM role credentials to assume the target role const pamStsClient = new STSClient({ - region: connectionDetails.region, + region: AWS_STS_DEFAULT_REGION, useFipsEndpoint: crypto.isFipsModeEnabled(), sha256: CustomAWSHasher, credentials: { diff --git a/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-factory.ts b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-factory.ts index 6fe7bfafe..01f593b4f 100644 --- a/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-factory.ts +++ b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-factory.ts @@ -32,7 +32,7 @@ export const awsIamResourceFactory: TPamResourceFactory { export type TAccessPamAccountDTO = { accountId: string; + accountPath: string; + projectId: string; duration: string; }; @@ -141,11 +143,13 @@ export type TAccessPamAccountResponse = { export const useAccessPamAccount = () => { return useMutation({ - mutationFn: async ({ accountId, duration }: TAccessPamAccountDTO) => { + mutationFn: async ({ accountId, accountPath, projectId, duration }: TAccessPamAccountDTO) => { const { data } = await apiRequest.post( "/api/v1/pam/accounts/access", { accountId, + accountPath, + projectId, duration } ); diff --git a/frontend/src/hooks/api/pam/types/aws-iam-resource.ts b/frontend/src/hooks/api/pam/types/aws-iam-resource.ts index ced875f30..8cb51a0ec 100644 --- a/frontend/src/hooks/api/pam/types/aws-iam-resource.ts +++ b/frontend/src/hooks/api/pam/types/aws-iam-resource.ts @@ -3,13 +3,12 @@ import { TBasePamAccount } from "./base-account"; import { TBasePamResource } from "./base-resource"; export type TAwsIamConnectionDetails = { - region: string; roleArn: string; }; export type TAwsIamCredentials = { targetRoleArn: string; - maxSessionDuration: number; + defaultSessionDuration: number; }; export type TAwsIamResource = Omit & { diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccessAccountModal.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccessAccountModal.tsx index 0b78120c6..4d70a0ae4 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccessAccountModal.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccessAccountModal.tsx @@ -1,20 +1,12 @@ import { useMemo, useState } from "react"; import { faCopy } from "@fortawesome/free-regular-svg-icons"; -import { faExternalLink, faUpRightFromSquare, faWarning } from "@fortawesome/free-solid-svg-icons"; +import { faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import ms from "ms"; import { createNotification } from "@app/components/notifications"; -import { - Button, - FormControl, - FormLabel, - IconButton, - Input, - Modal, - ModalContent -} from "@app/components/v2"; -import { PamResourceType, TPamAccount, useAccessPamAccount } from "@app/hooks/api/pam"; +import { FormLabel, IconButton, Input, Modal, ModalContent } from "@app/components/v2"; +import { PamResourceType, TPamAccount } from "@app/hooks/api/pam"; type Props = { account?: TPamAccount; @@ -24,124 +16,25 @@ type Props = { projectId: string; }; -const AwsIamAccessContent = ({ - account, - onOpenChange -}: { - account: TPamAccount; - onOpenChange: (isOpen: boolean) => void; -}) => { - const [durationInput, setDurationInput] = useState("1h"); - const accessPamAccount = useAccessPamAccount(); - - const parsedDuration = useMemo(() => { - try { - const milliseconds = ms(durationInput); - if (!milliseconds) return null; - const seconds = Math.floor(milliseconds / 1000); - // Min 15 minutes (900s), max 1 hour (3600s) due to AWS role chaining limitation - if (seconds < 900 || seconds > 3600) return null; - return seconds; - } catch { - return null; - } - }, [durationInput]); - - const handleAccessConsole = async () => { - if (!parsedDuration) return; - - try { - const response = await accessPamAccount.mutateAsync({ - accountId: account.id, - duration: `${parsedDuration}s` - }); - - if (response.consoleUrl) { - // Open the AWS Console URL in a new tab - window.open(response.consoleUrl, "_blank", "noopener,noreferrer"); - - createNotification({ - text: "AWS Console opened in new tab", - type: "success" - }); - - onOpenChange(false); - } else { - createNotification({ - text: "Failed to generate AWS Console URL", - type: "error" - }); - } - } catch { - createNotification({ - text: "Failed to access AWS Console", - type: "error" - }); - } - }; - - return ( - <> - 0 && !parsedDuration} - errorText="Invalid duration. Use format like 15m, 30m, 1h" - > - setDurationInput(e.target.value)} - placeholder="1h" - /> - - -
-
- -
- Important: AWS Console sessions cannot be terminated early. The session - remains active until the STS token expires. All activity is logged in AWS CloudTrail. -
-
-
- - - - ); -}; - -const CliAccessContent = ({ - account, +export const PamAccessAccountModal = ({ + isOpen, onOpenChange, + account, projectId, accountPath -}: { - account: TPamAccount; - onOpenChange: (isOpen: boolean) => void; - projectId: string; - accountPath?: string; -}) => { - let fullAccountPath = account?.name; - if (accountPath) { - let path = accountPath; - if (path.startsWith("/")) path = path.slice(1); - fullAccountPath = `${path}/${account?.name}`; - } +}: Props) => { + const [duration, setDuration] = useState("4h"); const { protocol, hostname, port } = window.location; const portSuffix = port && port !== "80" && port !== "443" ? `:${port}` : ""; const siteURL = `${protocol}//${hostname}${portSuffix}`; - const [duration, setDuration] = useState("4h"); + let fullAccountPath = account?.name ?? ""; + if (accountPath) { + let path = accountPath; + if (path.startsWith("/")) path = path.slice(1); + fullAccountPath = `${path}/${account?.name}`; + } const isDurationValid = useMemo(() => duration && ms(duration || "1s") > 0, [duration]); @@ -196,87 +89,58 @@ const CliAccessContent = ({ default: return ""; } - }, [account, cliDuration]); + }, [account, fullAccountPath, projectId, cliDuration, siteURL]); - return ( - <> - - setDuration(e.target.value)} - placeholder="permanent" - isError={!isDurationValid} - /> - -
- - { - navigator.clipboard.writeText(command); - - createNotification({ - text: "Command copied to clipboard", - type: "info" - }); - - onOpenChange(false); - }} - className="w-10" - > - - -
- - Install the Infisical CLI - - - - ); -}; - -export const PamAccessAccountModal = ({ - isOpen, - onOpenChange, - account, - projectId, - accountPath -}: Props) => { if (!account) return null; - const isAwsIam = account.resource.resourceType === PamResourceType.AwsIam; - return ( - {isAwsIam ? ( - - ) : ( - - )} + + setDuration(e.target.value)} + placeholder="permanent" + isError={!isDurationValid} + /> + +
+ + { + navigator.clipboard.writeText(command); + + createNotification({ + text: "Command copied to clipboard", + type: "info" + }); + + onOpenChange(false); + }} + className="w-10" + > + + +
+ + Install the Infisical CLI + +
); diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/AwsIamAccountForm.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/AwsIamAccountForm.tsx index 0d8ef5336..9d6a882b9 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/AwsIamAccountForm.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/AwsIamAccountForm.tsx @@ -1,4 +1,6 @@ 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 { z } from "zod"; @@ -12,6 +14,7 @@ import { Input, ModalClose } from "@app/components/v2"; +import { CopyButton } from "@app/components/v2/CopyButton"; import { useProject } from "@app/context"; import { PamResourceType, TAwsIamAccount } from "@app/hooks/api/pam"; @@ -35,7 +38,7 @@ const AwsIamCredentialsSchema = z.object({ 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) - maxSessionDuration: z.coerce + defaultSessionDuration: z.coerce .number() .min(900, "Minimum session duration is 900 seconds (15 minutes)") .max(3600, "Maximum session duration is 3600 seconds (1 hour)") @@ -57,6 +60,22 @@ export const AwsIamAccountForm = ({ account, onSubmit }: Props) => { const isUpdate = Boolean(account); const { projectId } = useProject(); + const targetRoleTrustPolicy = `{ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam:::role/" + }, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "${projectId}" + } + } + }] +}`; + const form = useForm({ resolver: zodResolver(formSchema), defaultValues: account ?? { @@ -64,7 +83,7 @@ export const AwsIamAccountForm = ({ account, onSubmit }: Props) => { description: "", credentials: { targetRoleArn: "", - maxSessionDuration: 3600 + defaultSessionDuration: 3600 } } }); @@ -104,7 +123,7 @@ export const AwsIamAccountForm = ({ account, onSubmit }: Props) => { /> ( { helperText="In seconds. Min 900 (15m), max 3600 (1h) due to AWS role chaining limit." errorText={error?.message} isError={Boolean(error?.message)} - label="Session Duration (seconds)" + label="Default Session Duration (seconds)" > @@ -120,10 +139,19 @@ export const AwsIamAccountForm = ({ account, onSubmit }: Props) => { /> - - - Target Role Setup - + + + +
+ + Target Role Setup +
+
+

The target role must have a trust policy that allows the Infisical PAM role to assume it. If you used the{" "} @@ -134,32 +162,24 @@ export const AwsIamAccountForm = ({ account, onSubmit }: Props) => {

Target role trust policy:

-
-                {`{
-  "Version": "2012-10-17",
-  "Statement": [{
-    "Effect": "Allow",
-    "Principal": {
-      "AWS": "arn:aws:iam:::role/"
-    },
-    "Action": "sts:AssumeRole",
-    "Condition": {
-      "StringEquals": {
-        "sts:ExternalId": "${projectId}"
-      }
-    }
-  }]
-}`}
-              
+
+
+ +
+
+                  {targetRoleTrustPolicy}
+                
+

Note: Replace{" "} <YOUR_ACCOUNT_ID> with your AWS account ID and{" "} <YOUR_PAM_ROLE_NAME>{" "} - with the name of the PAM role you created (e.g.,{" "} - InfisicalPAMRole). The - External ID {projectId} is - your current project ID. If your target role name doesn't follow the{" "} + with the name of the PAM role you created and used in the "Resources" tab + (e.g., InfisicalPAMRole). The + External ID{" "} + {projectId} is your + current project ID. If your target role name doesn't follow the{" "} infisical-pam-* pattern, you must update the PAM role's permissions policy to include the target role ARN.

@@ -167,22 +187,6 @@ export const AwsIamAccountForm = ({ account, onSubmit }: Props) => {
-
-

- Note: While users cannot terminate AWS Console sessions directly, - administrators can revoke active sessions by using the{" "} - - Revoke Sessions - {" "} - feature in the IAM console. All activity is logged in AWS CloudTrail. -

-
-