Files
seaweedFS/weed/iam/utils/arn_utils_test.go
Chris Lu 54de32f207 Support AWS standard IAM role ARN formats (issue #7946) (#7948)
* 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 with or without account ID
- Simplify role ARN validation in validateRoleAssumptionForWebIdentity() and
  validateRoleAssumptionForCredentials() to use the extraction function
- Add comprehensive test coverage with 25 test cases covering both formats

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

* docs: improve docstring coverage for ARN utility functions

- Add comprehensive package-level documentation
- Enhance ExtractRoleNameFromPrincipal docstring with parameter and return descriptions
- Enhance ExtractRoleNameFromArn docstring with detailed format documentation
- Add docstrings to test functions explaining test coverage
- Update all docstrings to 80%+ coverage for code review compliance

* refactor: improve ARN parsing code maintainability and error messages

- Define constants for ARN prefixes and markers (stsPrefix, stsAssumedRoleMarker, iamPrefix, iamRoleMarker)
- Replace hardcoded magic strings with named constants in ExtractRoleNameFromPrincipal and ExtractRoleNameFromArn
- Enhance error messages in sts_service.go to show expected ARN format when validation fails
- Error message now shows: 'arn:aws:iam::[ACCOUNT_ID:]role/ROLE_NAME' format
- Improves code readability and maintainability
- Facilitates future ARN format changes and debugging

* feat: add structured ARN type for better debugging and extensibility

Implements Option 2 (Structured ARN Type) from ARN handling comparison:

New Features:
- ARNInfo struct with Original, RoleName, AccountID, and Format fields
- ARNFormat enum (Legacy, Standard, Invalid) for type-safe format tracking
- ParseRoleARN() function for structured IAM role ARN parsing
- ParsePrincipalARN() function for structured STS/IAM principal parsing

Benefits:
- Better debugging: Can see original ARN, extracted components, and format type
- Extensible: Easy to add more fields (Region, Service, etc.) in future
- Type-safe: Format is an enum, not a string
- Backward compatible: Kept original string-based functions

STS Service Updates:
- Uses ParseRoleARN() for structured validation
- Logs ARN components at V(4) level for debugging (role, account, format)
- Better error context when validation fails

Test Coverage:
- 7 new tests for ParseRoleARN (legacy, standard, invalid formats)
- 7 new tests for ParsePrincipalARN (STS/IAM, legacy/standard)
- All 39 existing tests still pass
- Total: 53 ARN-related tests

Comparison with MinIO:
- More flexible: Supports both AWS formats (MinIO only supports MinIO format)
- Better tested: 53 tests vs MinIO's 8 tests
- Structured like MinIO but more practical for AWS use cases

* security: fix ARN parsing to prevent malicious ARN acceptance

Fix critical security vulnerability where malicious ARNs could bypass validation:
- ARNs like 'arn:aws:iam::123456789012:user/role/malicious' were incorrectly accepted
- The previous implementation used strings.Index to find 'role/' anywhere in the ARN
- This allowed non-role resource types to be accepted if they contained 'role/' in their path

Changes:
1. Updated ExtractRoleNameFromArn() to validate resource type is exactly 'role/'
2. Updated ExtractRoleNameFromPrincipal() to validate resource type is exactly 'assumed-role/'
3. Updated ParseRoleARN() to validate structure before extracting fields
4. Updated ParsePrincipalARN() to validate structure before extracting fields
5. Added 6 security test cases to prevent regression

The fix validates ARN structure by:
- Splitting on ':' to separate account ID from resource type
- Verifying resource type starts with exact marker ('role/' or 'assumed-role/')
- Only then extracting role name, account ID, and format

All 59 tests pass, including new security tests that verify malicious ARNs are rejected.

Fixes: GitHub Copilot review #3624499048

* test: add test cases for empty role names and improve validation

Address review feedback to improve edge case coverage:

1. Added test case for standard format with empty role name
   - TestExtractRoleNameFromArn: arn:aws:iam::123456789012:role/
   - TestParseRoleARN: arn:aws:iam::123456789012:role/

2. Added empty role name validation for STS ARNs in ParsePrincipalARN
   - Now matches ParseRoleARN behavior
   - Prevents ARNs like arn:aws:sts::assumed-role/ from having valid Format

3. Added test cases for empty STS role names
   - TestParsePrincipalARN: arn:aws:sts::assumed-role/
   - TestParsePrincipalARN: arn:aws:sts::123456789012:assumed-role/

All 65 tests pass (15 for ExtractRoleNameFromArn, 10 for ExtractRoleNameFromPrincipal,
8 for ParseRoleARN, 9 for ParsePrincipalARN, 4 security user ARNs, 2 security STS,
plus existing tests).

* refactor: simplify ARNInfo by removing Format enum

Remove ARNFormat enum (ARNFormatLegacy, ARNFormatStandard, ARNFormatInvalid)
as it's not needed for backward compatibility. Simplifications:

1. Removed ARNFormat type and all format constants
2. Removed Format field from ARNInfo struct
3. Validation now checks if RoleName is empty (simpler and clearer)
4. AccountID presence already distinguishes legacy (empty) from standard (non-empty) formats
5. Updated STS service to check RoleName emptiness instead of Format field
6. Improved debug logging to explicitly show "(legacy format)" or "(standard format)"

Benefits:
- Simpler code with fewer concepts
- AccountID field already provides format information
- Validation is clearer: empty RoleName = invalid ARN
- All 65 tests still pass

This change maintains the same functionality while reducing code complexity.
No backward compatibility concerns as the structured ARN parsing is new.

* test: add comprehensive edge case tests for ARN parsing

Add 4 new test functions covering:
- Multiple role markers in paths (e.g., role/role/name)
- Consecutive slashes in role paths (preserved as valid components)
- Special characters valid in AWS role names (+=,.@-_)
- Extremely long role names near AWS limits

These tests verify the parser's resilience to edge cases and ensure
proper handling of various valid role name formats and special characters.
2026-01-03 19:00:04 -08:00

647 lines
19 KiB
Go

package utils
import "testing"
// TestExtractRoleNameFromArn tests the ExtractRoleNameFromArn function with
// comprehensive test cases covering:
// - Legacy IAM role ARN format (arn:aws:iam::role/RoleName)
// - Standard AWS IAM role ARN format (arn:aws:iam::ACCOUNT:role/RoleName)
// - Role names with path components (e.g., role/Path/To/RoleName)
// - Invalid and edge case ARNs (missing prefix, wrong service, empty strings)
//
// The test uses table-driven test pattern with multiple scenarios for each
// format to ensure robust handling of both legacy and modern AWS ARN formats.
func TestExtractRoleNameFromArn(t *testing.T) {
testCases := []struct {
name string
roleArn string
expected string
}{
// Legacy format (without account ID)
{
name: "legacy_format_simple_role_name",
roleArn: "arn:aws:iam::role/default",
expected: "default",
},
{
name: "legacy_format_custom_role_name",
roleArn: "arn:aws:iam::role/MyRole",
expected: "MyRole",
},
{
name: "legacy_format_with_path",
roleArn: "arn:aws:iam::role/Path/MyRole",
expected: "Path/MyRole",
},
{
name: "legacy_format_with_nested_path",
roleArn: "arn:aws:iam::role/Division/Team/Role",
expected: "Division/Team/Role",
},
// Standard AWS format (with account ID)
{
name: "standard_format_simple_role_name",
roleArn: "arn:aws:iam::123456789012:role/default",
expected: "default",
},
{
name: "standard_format_custom_role_name",
roleArn: "arn:aws:iam::999999999999:role/MyRole",
expected: "MyRole",
},
{
name: "standard_format_with_path",
roleArn: "arn:aws:iam::123456789012:role/Path/MyRole",
expected: "Path/MyRole",
},
{
name: "standard_format_with_nested_path",
roleArn: "arn:aws:iam::123456789012:role/Division/Team/Role",
expected: "Division/Team/Role",
},
// Edge cases and invalid formats
{
name: "invalid_arn_missing_prefix",
roleArn: "invalid-arn",
expected: "",
},
{
name: "invalid_arn_incomplete",
roleArn: "arn:aws:iam::",
expected: "",
},
{
name: "invalid_arn_no_role_marker",
roleArn: "arn:aws:iam::123456789012:user/username",
expected: "",
},
{
name: "invalid_arn_wrong_service",
roleArn: "arn:aws:sts::assumed-role/Role/Session",
expected: "",
},
{
name: "empty_string",
roleArn: "",
expected: "",
},
{
name: "role_marker_no_name",
roleArn: "arn:aws:iam::role/",
expected: "",
},
{
name: "standard_format_role_marker_no_name",
roleArn: "arn:aws:iam::123456789012:role/",
expected: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := ExtractRoleNameFromArn(tc.roleArn)
if result != tc.expected {
t.Errorf("ExtractRoleNameFromArn(%q) = %q, want %q", tc.roleArn, result, tc.expected)
}
})
}
}
// TestExtractRoleNameFromPrincipal tests the ExtractRoleNameFromPrincipal function
// with comprehensive test cases covering:
// - STS assumed role ARN format (arn:aws:sts::assumed-role/RoleName/SessionName)
// - Standard AWS STS format (arn:aws:sts::ACCOUNT:assumed-role/RoleName/SessionName)
// - IAM role ARN format delegated to ExtractRoleNameFromArn
// - Both legacy and standard IAM role formats with and without paths
// - Invalid and edge case principals (wrong format, empty strings)
//
// The test ensures that ExtractRoleNameFromPrincipal correctly handles both
// STS temporary credentials and permanent IAM role ARNs used in different
// authentication and authorization workflows.
func TestExtractRoleNameFromPrincipal(t *testing.T) {
testCases := []struct {
name string
principal string
expected string
}{
// STS assumed role format (legacy)
{
name: "sts_assumed_role_legacy",
principal: "arn:aws:sts::assumed-role/RoleName/SessionName",
expected: "RoleName",
},
{
name: "sts_assumed_role_legacy_no_session",
principal: "arn:aws:sts::assumed-role/RoleName",
expected: "RoleName",
},
// STS assumed role format (standard with account ID)
{
name: "sts_assumed_role_standard",
principal: "arn:aws:sts::123456789012:assumed-role/RoleName/SessionName",
expected: "RoleName",
},
{
name: "sts_assumed_role_standard_no_session",
principal: "arn:aws:sts::123456789012:assumed-role/RoleName",
expected: "RoleName",
},
// IAM role format (legacy)
{
name: "iam_role_legacy",
principal: "arn:aws:iam::role/RoleName",
expected: "RoleName",
},
{
name: "iam_role_legacy_with_path",
principal: "arn:aws:iam::role/Path/RoleName",
expected: "Path/RoleName",
},
// IAM role format (standard)
{
name: "iam_role_standard",
principal: "arn:aws:iam::123456789012:role/RoleName",
expected: "RoleName",
},
{
name: "iam_role_standard_with_path",
principal: "arn:aws:iam::123456789012:role/Path/RoleName",
expected: "Path/RoleName",
},
// Invalid formats
{
name: "invalid_principal",
principal: "invalid-arn",
expected: "",
},
{
name: "empty_string",
principal: "",
expected: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := ExtractRoleNameFromPrincipal(tc.principal)
if result != tc.expected {
t.Errorf("ExtractRoleNameFromPrincipal(%q) = %q, want %q", tc.principal, result, tc.expected)
}
})
}
}
// TestParseRoleARN tests the ParseRoleARN function with structured ARNInfo output
func TestParseRoleARN(t *testing.T) {
testCases := []struct {
name string
roleArn string
expected ARNInfo
}{
{
name: "legacy_format_simple_role",
roleArn: "arn:aws:iam::role/MyRole",
expected: ARNInfo{
Original: "arn:aws:iam::role/MyRole",
RoleName: "MyRole",
AccountID: "",
},
},
{
name: "legacy_format_with_path",
roleArn: "arn:aws:iam::role/Division/Team/MyRole",
expected: ARNInfo{
Original: "arn:aws:iam::role/Division/Team/MyRole",
RoleName: "Division/Team/MyRole",
AccountID: "",
},
},
{
name: "standard_format_simple_role",
roleArn: "arn:aws:iam::123456789012:role/MyRole",
expected: ARNInfo{
Original: "arn:aws:iam::123456789012:role/MyRole",
RoleName: "MyRole",
AccountID: "123456789012",
},
},
{
name: "standard_format_with_path",
roleArn: "arn:aws:iam::999999999999:role/Path/To/MyRole",
expected: ARNInfo{
Original: "arn:aws:iam::999999999999:role/Path/To/MyRole",
RoleName: "Path/To/MyRole",
AccountID: "999999999999",
},
},
{
name: "invalid_arn_missing_prefix",
roleArn: "invalid-arn",
expected: ARNInfo{
Original: "invalid-arn",
RoleName: "",
AccountID: "",
},
},
{
name: "invalid_arn_no_role_marker",
roleArn: "arn:aws:iam::123456789012:user/username",
expected: ARNInfo{
Original: "arn:aws:iam::123456789012:user/username",
RoleName: "",
AccountID: "",
},
},
{
name: "invalid_arn_empty_role_name",
roleArn: "arn:aws:iam::role/",
expected: ARNInfo{
Original: "arn:aws:iam::role/",
RoleName: "",
AccountID: "",
},
},
{
name: "invalid_arn_empty_role_name_standard_format",
roleArn: "arn:aws:iam::123456789012:role/",
expected: ARNInfo{
Original: "arn:aws:iam::123456789012:role/",
RoleName: "",
AccountID: "",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := ParseRoleARN(tc.roleArn)
if result.Original != tc.expected.Original {
t.Errorf("ParseRoleARN(%q).Original = %q, want %q", tc.roleArn, result.Original, tc.expected.Original)
}
if result.RoleName != tc.expected.RoleName {
t.Errorf("ParseRoleARN(%q).RoleName = %q, want %q", tc.roleArn, result.RoleName, tc.expected.RoleName)
}
if result.AccountID != tc.expected.AccountID {
t.Errorf("ParseRoleARN(%q).AccountID = %q, want %q", tc.roleArn, result.AccountID, tc.expected.AccountID)
}
})
}
}
// TestParsePrincipalARN tests the ParsePrincipalARN function with structured ARNInfo output
func TestParsePrincipalARN(t *testing.T) {
testCases := []struct {
name string
principal string
expected ARNInfo
}{
{
name: "sts_assumed_role_legacy",
principal: "arn:aws:sts::assumed-role/MyRole/SessionName",
expected: ARNInfo{
Original: "arn:aws:sts::assumed-role/MyRole/SessionName",
RoleName: "MyRole",
AccountID: "",
},
},
{
name: "sts_assumed_role_standard",
principal: "arn:aws:sts::123456789012:assumed-role/MyRole/SessionName",
expected: ARNInfo{
Original: "arn:aws:sts::123456789012:assumed-role/MyRole/SessionName",
RoleName: "MyRole",
AccountID: "123456789012",
},
},
{
name: "sts_assumed_role_no_session",
principal: "arn:aws:sts::assumed-role/MyRole",
expected: ARNInfo{
Original: "arn:aws:sts::assumed-role/MyRole",
RoleName: "MyRole",
AccountID: "",
},
},
{
name: "iam_role_legacy",
principal: "arn:aws:iam::role/MyRole",
expected: ARNInfo{
Original: "arn:aws:iam::role/MyRole",
RoleName: "MyRole",
AccountID: "",
},
},
{
name: "iam_role_standard",
principal: "arn:aws:iam::123456789012:role/MyRole",
expected: ARNInfo{
Original: "arn:aws:iam::123456789012:role/MyRole",
RoleName: "MyRole",
AccountID: "123456789012",
},
},
{
name: "iam_role_with_path",
principal: "arn:aws:iam::999999999999:role/Division/Team/MyRole",
expected: ARNInfo{
Original: "arn:aws:iam::999999999999:role/Division/Team/MyRole",
RoleName: "Division/Team/MyRole",
AccountID: "999999999999",
},
},
{
name: "invalid_principal",
principal: "invalid-arn",
expected: ARNInfo{
Original: "invalid-arn",
RoleName: "",
AccountID: "",
},
},
{
name: "invalid_sts_empty_role_name",
principal: "arn:aws:sts::assumed-role/",
expected: ARNInfo{
Original: "arn:aws:sts::assumed-role/",
RoleName: "",
AccountID: "",
},
},
{
name: "invalid_sts_empty_role_name_standard_format",
principal: "arn:aws:sts::123456789012:assumed-role/",
expected: ARNInfo{
Original: "arn:aws:sts::123456789012:assumed-role/",
RoleName: "",
AccountID: "",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := ParsePrincipalARN(tc.principal)
if result.Original != tc.expected.Original {
t.Errorf("ParsePrincipalARN(%q).Original = %q, want %q", tc.principal, result.Original, tc.expected.Original)
}
if result.RoleName != tc.expected.RoleName {
t.Errorf("ParsePrincipalARN(%q).RoleName = %q, want %q", tc.principal, result.RoleName, tc.expected.RoleName)
}
if result.AccountID != tc.expected.AccountID {
t.Errorf("ParsePrincipalARN(%q).AccountID = %q, want %q", tc.principal, result.AccountID, tc.expected.AccountID)
}
})
}
}
// TestSecurityMaliciousUserARNs tests that user ARNs with "role/" in the path are correctly rejected
// to prevent security vulnerabilities where malicious ARNs could bypass validation.
func TestSecurityMaliciousUserARNs(t *testing.T) {
maliciousARNs := []struct {
arn string
description string
}{
{"arn:aws:iam::123456789012:user/role/malicious", "user ARN with role/ in path"},
{"arn:aws:iam::123456789012:policy/role/some-policy", "policy ARN with role/ in name"},
{"arn:aws:iam::123456789012:group/role/some-group", "group ARN with role/ in name"},
{"arn:aws:iam::user/role/test", "legacy user ARN with role/ in path"},
}
for _, tc := range maliciousARNs {
t.Run(tc.description, func(t *testing.T) {
roleName := ExtractRoleNameFromArn(tc.arn)
if roleName != "" {
t.Errorf("Security issue: %s was accepted and returned role name '%s'", tc.description, roleName)
}
arnInfo := ParseRoleARN(tc.arn)
if arnInfo.RoleName != "" {
t.Errorf("Security issue: %s was accepted by ParseRoleARN and returned role name '%s'", tc.description, arnInfo.RoleName)
}
})
}
}
// TestSecurityMaliciousSTSUserARNs tests that STS user ARNs with "assumed-role/" are correctly rejected
// to prevent security vulnerabilities.
func TestSecurityMaliciousSTSUserARNs(t *testing.T) {
maliciousARNs := []struct {
arn string
description string
}{
{"arn:aws:sts::123456789012:user/assumed-role/malicious", "STS user with assumed-role in path"},
{"arn:aws:sts::user/assumed-role/test", "legacy STS user with assumed-role in path"},
}
for _, tc := range maliciousARNs {
t.Run(tc.description, func(t *testing.T) {
roleName := ExtractRoleNameFromPrincipal(tc.arn)
if roleName != "" {
t.Errorf("Security issue: %s was accepted and returned role name '%s'", tc.description, roleName)
}
arnInfo := ParsePrincipalARN(tc.arn)
if arnInfo.RoleName != "" {
t.Errorf("Security issue: %s was accepted by ParsePrincipalARN and returned role name '%s'", tc.description, arnInfo.RoleName)
}
})
}
}
// TestEdgeCaseMultipleRoleMarkers tests ARNs with multiple "role/" markers in the path.
// AWS role names can legitimately contain slashes for path components, so "role/role/name"
// should be accepted as a valid role name "role/name".
func TestEdgeCaseMultipleRoleMarkers(t *testing.T) {
testCases := []struct {
name string
arn string
expected string
useSTS bool
}{
{
name: "legacy_format_role_in_path",
arn: "arn:aws:iam::role/role/name",
expected: "role/name",
useSTS: false,
},
{
name: "standard_format_role_in_path",
arn: "arn:aws:iam::123456789012:role/role/name",
expected: "role/name",
useSTS: false,
},
{
name: "multiple_role_markers_in_path",
arn: "arn:aws:iam::123456789012:role/role/role/role",
expected: "role/role/role",
useSTS: false,
},
{
name: "sts_assumed_role_with_role_in_path",
arn: "arn:aws:sts::123456789012:assumed-role/role/SessionId",
expected: "role",
useSTS: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.useSTS {
result := ExtractRoleNameFromPrincipal(tc.arn)
if result != tc.expected {
t.Errorf("ExtractRoleNameFromPrincipal(%q) = %q, want %q", tc.arn, result, tc.expected)
}
} else {
result := ExtractRoleNameFromArn(tc.arn)
if result != tc.expected {
t.Errorf("ExtractRoleNameFromArn(%q) = %q, want %q", tc.arn, result, tc.expected)
}
}
})
}
}
// TestEdgeCaseConsecutiveSlashes tests ARNs with consecutive slashes which are
// preserved as valid path components. These are technically allowed in role names,
// though they're rare in practice.
func TestEdgeCaseConsecutiveSlashes(t *testing.T) {
testCases := []struct {
name string
roleArn string
expected string
}{
{
name: "consecutive_slashes_immediately_after_role",
roleArn: "arn:aws:iam::role//name",
expected: "/name",
},
{
name: "consecutive_slashes_in_path",
roleArn: "arn:aws:iam::123456789012:role/Division//Team/Role",
expected: "Division//Team/Role",
},
{
name: "multiple_consecutive_slashes",
roleArn: "arn:aws:iam::123456789012:role/////Name",
expected: "////Name",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := ExtractRoleNameFromArn(tc.roleArn)
if result != tc.expected {
t.Errorf("ExtractRoleNameFromArn(%q) = %q, want %q", tc.roleArn, result, tc.expected)
}
})
}
}
// TestEdgeCaseSpecialCharactersInRoleName tests valid AWS role name special characters.
// AWS IAM role names support: letters, numbers, and special characters +=,.@-_
func TestEdgeCaseSpecialCharactersInRoleName(t *testing.T) {
testCases := []struct {
name string
roleArn string
expected string
}{
{
name: "role_name_with_hyphens",
roleArn: "arn:aws:iam::123456789012:role/My-Role-Name",
expected: "My-Role-Name",
},
{
name: "role_name_with_underscores",
roleArn: "arn:aws:iam::123456789012:role/My_Role_Name",
expected: "My_Role_Name",
},
{
name: "role_name_with_dots",
roleArn: "arn:aws:iam::123456789012:role/my.role.name",
expected: "my.role.name",
},
{
name: "role_name_with_at_sign",
roleArn: "arn:aws:iam::123456789012:role/Role@Domain",
expected: "Role@Domain",
},
{
name: "role_name_with_plus_and_equals",
roleArn: "arn:aws:iam::123456789012:role/Role+=Name",
expected: "Role+=Name",
},
{
name: "role_name_with_commas",
roleArn: "arn:aws:iam::123456789012:role/Role,Name",
expected: "Role,Name",
},
{
name: "role_name_with_mixed_special_chars",
roleArn: "arn:aws:iam::123456789012:role/App-Env.Region+Shard@Version",
expected: "App-Env.Region+Shard@Version",
},
{
name: "path_with_special_characters",
roleArn: "arn:aws:iam::123456789012:role/Org-1/Team.Dev+Staging@us-east-1/App",
expected: "Org-1/Team.Dev+Staging@us-east-1/App",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := ExtractRoleNameFromArn(tc.roleArn)
if result != tc.expected {
t.Errorf("ExtractRoleNameFromArn(%q) = %q, want %q", tc.roleArn, result, tc.expected)
}
// Also test ParseRoleARN to ensure structured parsing works
arnInfo := ParseRoleARN(tc.roleArn)
if arnInfo.RoleName != tc.expected {
t.Errorf("ParseRoleARN(%q).RoleName = %q, want %q", tc.roleArn, arnInfo.RoleName, tc.expected)
}
})
}
}
// TestEdgeCaseExtremelyLongRoleName tests role names near AWS limits.
// AWS IAM role names can be up to 64 characters, and paths can be up to 512 characters total.
func TestEdgeCaseExtremelyLongRoleName(t *testing.T) {
// Create a role name at the 64 character limit for a single role name segment
longRoleName := "a-role-name-that-is-nearly-at-the-sixty-four-character-limit-yes"
if len(longRoleName) > 64 {
t.Skipf("Test role name is too long: %d characters", len(longRoleName))
}
testCases := []struct {
name string
roleArn string
expected string
}{
{
name: "role_name_at_max_length",
roleArn: "arn:aws:iam::123456789012:role/" + longRoleName,
expected: longRoleName,
},
{
name: "role_with_long_path_components",
roleArn: "arn:aws:iam::123456789012:role/organization/department/team/application/environment/role",
expected: "organization/department/team/application/environment/role",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := ExtractRoleNameFromArn(tc.roleArn)
if result != tc.expected {
t.Errorf("ExtractRoleNameFromArn(%q) = %q, want %q", tc.roleArn, result, tc.expected)
}
// Also test ParseRoleARN
arnInfo := ParseRoleARN(tc.roleArn)
if arnInfo.RoleName != tc.expected {
t.Errorf("ParseRoleARN(%q).RoleName = %q, want %q", tc.roleArn, arnInfo.RoleName, tc.expected)
}
})
}
}