From e2b58a0a5b5f30ee38459c881d3d81844291888a Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Sat, 3 Jan 2026 10:13:35 -0800 Subject: [PATCH] fix(iam): support both AWS standard and legacy IAM role ARN formats Fix issue #7946 where SeaweedFS only recognized legacy IAM role ARN format (arn:aws:iam::role/RoleName) but not the standard AWS format with account ID (arn:aws:iam::ACCOUNT:role/RoleName). This was breaking EKS pod identity integration which expects the standard format. Changes: - Update ExtractRoleNameFromArn() to handle both formats by searching for 'role/' marker instead of matching a fixed prefix - Update ExtractRoleNameFromPrincipal() to clearly document both STS and IAM formats it supports - Simplify role ARN validation in validateRoleAssumptionForWebIdentity() and validateRoleAssumptionForCredentials() to use the extraction function The fix maintains backward compatibility with legacy format while adding support for standard AWS format with account ID. Fixes: https://github.com/seaweedfs/seaweedfs/issues/7946 --- weed/iam/sts/sts_service.go | 18 ++++---------- weed/iam/utils/arn_utils.go | 48 +++++++++++++++++++------------------ 2 files changed, 29 insertions(+), 37 deletions(-) diff --git a/weed/iam/sts/sts_service.go b/weed/iam/sts/sts_service.go index fc7285a37..7164b08a8 100644 --- a/weed/iam/sts/sts_service.go +++ b/weed/iam/sts/sts_service.go @@ -699,13 +699,8 @@ func (s *STSService) validateRoleAssumptionForWebIdentity(ctx context.Context, r return fmt.Errorf("web identity token cannot be empty") } - // Basic role ARN format validation - expectedPrefix := "arn:aws:iam::role/" - if len(roleArn) < len(expectedPrefix) || roleArn[:len(expectedPrefix)] != expectedPrefix { - return fmt.Errorf("invalid role ARN format: got %s, expected format: %s*", roleArn, expectedPrefix) - } - - // Extract role name and validate ARN format + // Validate role ARN and extract role name + // Accepts both arn:aws:iam::role/X and arn:aws:iam::ACCOUNT:role/X roleName := utils.ExtractRoleNameFromArn(roleArn) if roleName == "" { return fmt.Errorf("invalid role ARN format: %s", roleArn) @@ -736,13 +731,8 @@ func (s *STSService) validateRoleAssumptionForCredentials(ctx context.Context, r return fmt.Errorf("identity cannot be nil") } - // Basic role ARN format validation - expectedPrefix := "arn:aws:iam::role/" - if len(roleArn) < len(expectedPrefix) || roleArn[:len(expectedPrefix)] != expectedPrefix { - return fmt.Errorf("invalid role ARN format: got %s, expected format: %s*", roleArn, expectedPrefix) - } - - // Extract role name and validate ARN format + // Validate role ARN and extract role name + // Accepts both arn:aws:iam::role/X and arn:aws:iam::ACCOUNT:role/X roleName := utils.ExtractRoleNameFromArn(roleArn) if roleName == "" { return fmt.Errorf("invalid role ARN format: %s", roleArn) diff --git a/weed/iam/utils/arn_utils.go b/weed/iam/utils/arn_utils.go index 3f8cf0b8f..e34d6ee86 100644 --- a/weed/iam/utils/arn_utils.go +++ b/weed/iam/utils/arn_utils.go @@ -3,37 +3,39 @@ package utils import "strings" // ExtractRoleNameFromPrincipal extracts role name from principal ARN -// Handles both STS assumed role and IAM role formats +// Handles both STS assumed role and IAM role formats with or without account ID: +// - arn:aws:sts::assumed-role/Role/Session (legacy) +// - arn:aws:sts::ACCOUNT:assumed-role/Role/Session (standard) +// - arn:aws:iam::role/Role (legacy) +// - arn:aws:iam::ACCOUNT:role/Role (standard) func ExtractRoleNameFromPrincipal(principal string) string { - // Handle STS assumed role format: arn:aws:sts::assumed-role/RoleName/SessionName - stsPrefix := "arn:aws:sts::assumed-role/" - if strings.HasPrefix(principal, stsPrefix) { - remainder := principal[len(stsPrefix):] - // Split on first '/' to get role name - if slashIndex := strings.Index(remainder, "/"); slashIndex != -1 { - return remainder[:slashIndex] + // Handle STS assumed role format + if strings.HasPrefix(principal, "arn:aws:sts::") { + remainder := principal[len("arn:aws:sts::"):] + if idx := strings.Index(remainder, "assumed-role/"); idx != -1 { + afterMarker := remainder[idx+len("assumed-role/"):] + if slash := strings.Index(afterMarker, "/"); slash != -1 { + return afterMarker[:slash] + } + return afterMarker } - // If no slash found, return the remainder (edge case) - return remainder } - // Handle IAM role format: arn:aws:iam::role/RoleName - iamPrefix := "arn:aws:iam::role/" - if strings.HasPrefix(principal, iamPrefix) { - return principal[len(iamPrefix):] - } - - // Return empty string to signal invalid ARN format - // This allows callers to handle the error explicitly instead of masking it - return "" + // Handle IAM role format + return ExtractRoleNameFromArn(principal) } // ExtractRoleNameFromArn extracts role name from an IAM role ARN -// Specifically handles: arn:aws:iam::role/RoleName +// Handles both formats: +// - arn:aws:iam::role/RoleName (legacy, without account ID) +// - arn:aws:iam::ACCOUNT:role/RoleName (standard AWS format) func ExtractRoleNameFromArn(roleArn string) string { - prefix := "arn:aws:iam::role/" - if strings.HasPrefix(roleArn, prefix) && len(roleArn) > len(prefix) { - return roleArn[len(prefix):] + if !strings.HasPrefix(roleArn, "arn:aws:iam::") { + return "" + } + remainder := roleArn[len("arn:aws:iam::"):] + if idx := strings.Index(remainder, "role/"); idx != -1 { + return remainder[idx+len("role/"):] } return "" }