From 05849690062191cfbfa6557c08ef8fe23b6e09ee Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Thu, 24 Sep 2026 17:32:22 -0300 Subject: [PATCH 1/2] fix(iam): derive every IAM ARN's partition from the region Users, roles, policies, instance profiles and MFA devices already build their ARN with the region's partition, but groups (create and rename), OIDC and SAML providers, server certificates (upload and rename), the credential report's root row, the policy-simulation root fallback and the web-identity fallback provider all hardcoded arn:aws:. In a China or GovCloud region those entities disagreed with the rest of the account. ChangePassword parsed its caller only from an arn:aws:iam:: prefix, so an aws-cn or aws-us-gov user fell through to the anonymous no-op path and got a success without the old password being checked or the new one written. Parse the principal partition-agnostically. --- .../fakecloud-iam/src/iam_service/account.rs | 3 +- .../fakecloud-iam/src/iam_service/extras.rs | 16 +- .../fakecloud-iam/src/iam_service/groups.rs | 6 +- crates/fakecloud-iam/src/iam_service/oidc.rs | 14 +- crates/fakecloud-iam/src/iam_service/tests.rs | 152 ++++++++++++++++++ .../fakecloud-iam/src/sts_service/assume.rs | 8 +- 6 files changed, 184 insertions(+), 15 deletions(-) diff --git a/crates/fakecloud-iam/src/iam_service/account.rs b/crates/fakecloud-iam/src/iam_service/account.rs index f373e57fe..1997738ec 100644 --- a/crates/fakecloud-iam/src/iam_service/account.rs +++ b/crates/fakecloud-iam/src/iam_service/account.rs @@ -932,7 +932,8 @@ impl IamService { // Root account row (after users) csv.push_str(&format!( - ",arn:aws:iam::{}:root,{},not_supported,not_supported,not_supported,not_supported,false,false,N/A,N/A,N/A,N/A,false,N/A,N/A,N/A,N/A,false,N/A,false,N/A\n", + ",arn:{}:iam::{}:root,{},not_supported,not_supported,not_supported,not_supported,false,false,N/A,N/A,N/A,N/A,false,N/A,N/A,N/A,N/A,false,N/A,false,N/A\n", + fakecloud_aws::arn::partition_for(&req.region), state.account_id, Utc::now().format("%Y-%m-%dT%H:%M:%S+00:00") )); diff --git a/crates/fakecloud-iam/src/iam_service/extras.rs b/crates/fakecloud-iam/src/iam_service/extras.rs index 95aaa70dc..93a0d127f 100644 --- a/crates/fakecloud-iam/src/iam_service/extras.rs +++ b/crates/fakecloud-iam/src/iam_service/extras.rs @@ -1093,9 +1093,11 @@ impl IamService { .map(|(_, doc)| doc.clone()) .collect(); - let principal_arn_str = caller_arn - .clone() - .unwrap_or_else(|| Arn::global("iam", &req.account_id, "root").to_string()); + let principal_arn_str = caller_arn.clone().unwrap_or_else(|| { + Arn::global("iam", &req.account_id, "root") + .with_partition(fakecloud_aws::arn::partition_for(&req.region)) + .to_string() + }); let principal = Principal { arn: principal_arn_str.clone(), user_id: principal_arn_str.clone(), @@ -1288,8 +1290,9 @@ impl IamService { // payload (OldPassword != NewPassword) above. let user_name = req.principal.as_ref().and_then(|p| { let arn = &p.arn; - arn.strip_prefix("arn:aws:iam::") - .and_then(|rest| rest.split_once(":user/")) + arn.strip_prefix("arn:") + .and_then(|rest| rest.split_once(":iam::")) + .and_then(|(_, rest)| rest.split_once(":user/")) .map(|(_, name)| name.to_string()) }); let Some(user_name) = user_name else { @@ -1506,7 +1509,8 @@ impl IamService { updated.server_certificate_name = final_name.clone(); // Rebuild ARN with the new path/name so metadata responses reflect the rename. updated.arn = format!( - "arn:aws:iam::{account}:server-certificate{path}{name}", + "arn:{partition}:iam::{account}:server-certificate{path}{name}", + partition = fakecloud_aws::arn::partition_for(&req.region), account = req.account_id, path = if updated.path.starts_with('/') { updated.path.clone() diff --git a/crates/fakecloud-iam/src/iam_service/groups.rs b/crates/fakecloud-iam/src/iam_service/groups.rs index b44ba3247..44e5f7a7b 100644 --- a/crates/fakecloud-iam/src/iam_service/groups.rs +++ b/crates/fakecloud-iam/src/iam_service/groups.rs @@ -37,7 +37,8 @@ impl IamService { let group = IamGroup { group_id: format!("AGPA{}", generate_id()), arn: format!( - "arn:aws:iam::{}:group{}{}", + "arn:{}:iam::{}:group{}{}", + fakecloud_aws::arn::partition_for(&req.region), state.account_id, if path == "/" { "/" } else { &path }, group_name @@ -357,7 +358,8 @@ impl IamService { let actual_new_name = new_group_name.unwrap_or_else(|| group_name.clone()); group.group_name = actual_new_name.clone(); group.arn = format!( - "arn:aws:iam::{}:group{}{}", + "arn:{}:iam::{}:group{}{}", + fakecloud_aws::arn::partition_for(&req.region), state.account_id, if group.path == "/" { "/" } else { &group.path }, actual_new_name diff --git a/crates/fakecloud-iam/src/iam_service/oidc.rs b/crates/fakecloud-iam/src/iam_service/oidc.rs index faa4ecb57..edb89d4a7 100644 --- a/crates/fakecloud-iam/src/iam_service/oidc.rs +++ b/crates/fakecloud-iam/src/iam_service/oidc.rs @@ -139,8 +139,9 @@ impl IamService { let mut accounts = self.state.write(); let state = accounts.get_or_create(&req.account_id); - let arn = - Arn::global("iam", &state.account_id, &format!("saml-provider/{name}")).to_string(); + let arn = Arn::global("iam", &state.account_id, &format!("saml-provider/{name}")) + .with_partition(fakecloud_aws::arn::partition_for(&req.region)) + .to_string(); let provider = SamlProvider { arn: arn.clone(), @@ -357,8 +358,10 @@ impl IamService { .next() .unwrap_or(&url_without_scheme); let arn = format!( - "arn:aws:iam::{}:oidc-provider/{}", - state.account_id, url_for_arn + "arn:{}:iam::{}:oidc-provider/{}", + fakecloud_aws::arn::partition_for(&req.region), + state.account_id, + url_for_arn ); if state.oidc_providers.contains_key(&arn) { @@ -693,7 +696,8 @@ impl IamService { let cert = ServerCertificate { server_certificate_id: format!("ASCA{}", generate_id()), arn: format!( - "arn:aws:iam::{}:server-certificate{}{}", + "arn:{}:iam::{}:server-certificate{}{}", + fakecloud_aws::arn::partition_for(&req.region), state.account_id, if path == "/" { "/" } else { &path }, name diff --git a/crates/fakecloud-iam/src/iam_service/tests.rs b/crates/fakecloud-iam/src/iam_service/tests.rs index 6833abef5..29a810224 100644 --- a/crates/fakecloud-iam/src/iam_service/tests.rs +++ b/crates/fakecloud-iam/src/iam_service/tests.rs @@ -5370,3 +5370,155 @@ fn list_instance_profiles_paginates() { assert!(!body.contains("ip-a")); assert!(!body.contains("ip-b")); } + +/// Every IAM entity ARN minted in a China region carries the `aws-cn` +/// partition, the same way users, roles and policies already do. +#[test] +fn entity_arns_carry_region_partition() { + let svc = make_service(); + let cn = |action: &str, params: Vec<(&str, &str)>| { + let mut req = make_request(action, params); + req.region = "cn-north-1".to_string(); + req + }; + let arn_of = |resp: AwsResponse, tag: &str| { + let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string(); + extract_xml_tag(&body, tag).to_string() + }; + + let group = svc + .create_group(&cn("CreateGroup", vec![("GroupName", "ops")])) + .unwrap(); + assert_eq!( + arn_of(group, "Arn"), + "arn:aws-cn:iam::123456789012:group/ops" + ); + svc.update_group(&cn( + "UpdateGroup", + vec![("GroupName", "ops"), ("NewGroupName", "ops2")], + )) + .unwrap(); + let group = svc + .get_group(&cn("GetGroup", vec![("GroupName", "ops2")])) + .unwrap(); + assert_eq!( + arn_of(group, "Arn"), + "arn:aws-cn:iam::123456789012:group/ops2" + ); + + let oidc = svc + .create_oidc_provider(&cn( + "CreateOpenIDConnectProvider", + vec![ + ("Url", "https://oidc.example.com"), + ( + "ThumbprintList.member.1", + "abcdef1234567890abcdef1234567890abcdef12", + ), + ], + )) + .unwrap(); + assert_eq!( + arn_of(oidc, "OpenIDConnectProviderArn"), + "arn:aws-cn:iam::123456789012:oidc-provider/oidc.example.com" + ); + + let metadata = format!("{}", "x".repeat(1000)); + let saml = svc + .create_saml_provider(&cn( + "CreateSAMLProvider", + vec![("Name", "idp"), ("SAMLMetadataDocument", &metadata)], + )) + .unwrap(); + assert_eq!( + arn_of(saml, "SAMLProviderArn"), + "arn:aws-cn:iam::123456789012:saml-provider/idp" + ); + + let cert = svc + .upload_server_certificate(&cn( + "UploadServerCertificate", + vec![ + ("ServerCertificateName", "web"), + ( + "CertificateBody", + "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----", + ), + ( + "PrivateKey", + "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----", + ), + ], + )) + .unwrap(); + assert_eq!( + arn_of(cert, "Arn"), + "arn:aws-cn:iam::123456789012:server-certificate/web" + ); + svc.update_server_certificate(&cn( + "UpdateServerCertificate", + vec![ + ("ServerCertificateName", "web"), + ("NewServerCertificateName", "web2"), + ], + )) + .unwrap(); + let cert = svc + .get_server_certificate(&cn( + "GetServerCertificate", + vec![("ServerCertificateName", "web2")], + )) + .unwrap(); + assert_eq!( + arn_of(cert, "Arn"), + "arn:aws-cn:iam::123456789012:server-certificate/web2" + ); + + svc.generate_credential_report(&cn("GenerateCredentialReport", vec![])) + .unwrap(); + let report = svc + .get_credential_report(&cn("GetCredentialReport", vec![])) + .unwrap(); + let encoded = arn_of(report, "Content"); + let csv = String::from_utf8( + base64::Engine::decode(&base64::engine::general_purpose::STANDARD, encoded).unwrap(), + ) + .unwrap(); + assert!( + csv.contains(",arn:aws-cn:iam::123456789012:root,"), + "root row must carry the region partition: {csv}" + ); +} + +/// ChangePassword resolves the caller from an `aws-cn` principal ARN rather +/// than silently no-oping because the ARN is not `arn:aws:`. +#[test] +fn change_password_resolves_non_aws_partition_principal() { + let svc = make_service(); + svc.create_user(&make_request("CreateUser", vec![("UserName", "u1")])) + .unwrap(); + svc.create_login_profile(&make_request( + "CreateLoginProfile", + vec![("UserName", "u1"), ("Password", "old")], + )) + .unwrap(); + let mut principal = change_password_principal("u1"); + principal.arn = "arn:aws-cn:iam::123456789012:user/u1".to_string(); + + let mut wrong = make_request( + "ChangePassword", + vec![("OldPassword", "not-old"), ("NewPassword", "fresh")], + ); + wrong.principal = Some(principal.clone()); + assert!( + svc.change_password(&wrong).is_err(), + "a wrong old password must be rejected, not accepted as an anonymous no-op" + ); + + let mut right = make_request( + "ChangePassword", + vec![("OldPassword", "old"), ("NewPassword", "fresh")], + ); + right.principal = Some(principal); + svc.change_password(&right).unwrap(); +} diff --git a/crates/fakecloud-iam/src/sts_service/assume.rs b/crates/fakecloud-iam/src/sts_service/assume.rs index 3762740d3..eb466b4b7 100644 --- a/crates/fakecloud-iam/src/sts_service/assume.rs +++ b/crates/fakecloud-iam/src/sts_service/assume.rs @@ -489,7 +489,13 @@ impl StsService { .as_ref() .map(|(_iss, p)| p.arn.clone()) .or(provider_id_param.clone()) - .unwrap_or_else(|| format!("arn:aws:iam::{}:oidc-provider/web-identity", account_id)); + .unwrap_or_else(|| { + format!( + "arn:{}:iam::{}:oidc-provider/web-identity", + partition_for_region(&req.region), + account_id + ) + }); // Trust-policy gate: same shape as AssumeRole, but the caller // principal is the federated provider and the action is From 804e332614ce5e7382154a1fe51793b6b63ae6fe Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Thu, 24 Sep 2026 17:34:14 -0300 Subject: [PATCH 2/2] fix(iam): keep an entity's partition across renames and parse IAM ARNs in any partition UpdateGroup and UpdateServerCertificate rebuilt the ARN from the rename request's region. IAM is global, so a rename from a commercial region flipped a cn-/us-gov-/iso- entity to arn:aws:. Take the partition from the stored ARN, falling back to the region, as UpdateUser already did; the three share one helper now. The pass-role trust check and the policy evaluator's account-root classification only parsed arn:aws:iam::, so role ARNs and :root principals in other partitions were not recognized. --- crates/fakecloud-iam/src/evaluator.rs | 8 +- crates/fakecloud-iam/src/evaluator_tests.rs | 15 ++++ .../fakecloud-iam/src/iam_service/extras.rs | 11 +-- .../fakecloud-iam/src/iam_service/groups.rs | 7 +- .../fakecloud-iam/src/iam_service/helpers.rs | 12 +++ crates/fakecloud-iam/src/iam_service/tests.rs | 75 +++++++++++++++++++ crates/fakecloud-iam/src/iam_service/users.rs | 20 ++--- crates/fakecloud-iam/src/pass_role.rs | 20 ++++- 8 files changed, 140 insertions(+), 28 deletions(-) diff --git a/crates/fakecloud-iam/src/evaluator.rs b/crates/fakecloud-iam/src/evaluator.rs index 32047033e..0c2a25e27 100644 --- a/crates/fakecloud-iam/src/evaluator.rs +++ b/crates/fakecloud-iam/src/evaluator.rs @@ -384,8 +384,12 @@ fn classify_aws_principal(s: &str) -> PrincipalRef { if s == "*" { return PrincipalRef::AnyAws; } - // `arn:aws:iam:::root` → account root - if let Some(rest) = s.strip_prefix("arn:aws:iam::") { + // `arn::iam:::root` → account root + if let Some(rest) = s + .strip_prefix("arn:") + .and_then(|r| r.split_once(':')) + .and_then(|(_partition, r)| r.strip_prefix("iam::")) + { if let Some((account, tail)) = rest.split_once(':') { if tail == "root" && !account.is_empty() { return PrincipalRef::AwsAccountRoot(account.to_string()); diff --git a/crates/fakecloud-iam/src/evaluator_tests.rs b/crates/fakecloud-iam/src/evaluator_tests.rs index 9489afe73..a6bef07ec 100644 --- a/crates/fakecloud-iam/src/evaluator_tests.rs +++ b/crates/fakecloud-iam/src/evaluator_tests.rs @@ -1459,6 +1459,21 @@ fn classify_aws_principal_recognizes_root_arn() { ); } +#[test] +fn classify_aws_principal_recognizes_root_arn_in_any_partition() { + for partition in ["aws-cn", "aws-us-gov", "aws-iso"] { + assert_eq!( + classify_aws_principal(&format!("arn:{partition}:iam::111111111111:root")), + PrincipalRef::AwsAccountRoot("111111111111".to_string()) + ); + } + // A non-IAM ARN ending in `:root` is not an account root. + assert_eq!( + classify_aws_principal("arn:aws-cn:sts::111111111111:root"), + PrincipalRef::AwsArn("arn:aws-cn:sts::111111111111:root".to_string()) + ); +} + #[test] fn classify_aws_principal_keeps_user_arn_as_arn() { assert_eq!( diff --git a/crates/fakecloud-iam/src/iam_service/extras.rs b/crates/fakecloud-iam/src/iam_service/extras.rs index 93a0d127f..4918aaebd 100644 --- a/crates/fakecloud-iam/src/iam_service/extras.rs +++ b/crates/fakecloud-iam/src/iam_service/extras.rs @@ -15,8 +15,8 @@ use crate::state::{ }; use super::{ - empty_response, parse_tags, required_param_with_code, resolve_calling_user, tags_xml, - validate_string_length_with_code, IamService, + empty_response, existing_arn_partition, parse_tags, required_param_with_code, + resolve_calling_user, tags_xml, validate_string_length_with_code, IamService, }; use fakecloud_core::query::required_param; @@ -1291,8 +1291,9 @@ impl IamService { let user_name = req.principal.as_ref().and_then(|p| { let arn = &p.arn; arn.strip_prefix("arn:") - .and_then(|rest| rest.split_once(":iam::")) - .and_then(|(_, rest)| rest.split_once(":user/")) + .and_then(|rest| rest.split_once(':')) + .and_then(|(_partition, rest)| rest.strip_prefix("iam::")) + .and_then(|rest| rest.split_once(":user/")) .map(|(_, name)| name.to_string()) }); let Some(user_name) = user_name else { @@ -1510,7 +1511,7 @@ impl IamService { // Rebuild ARN with the new path/name so metadata responses reflect the rename. updated.arn = format!( "arn:{partition}:iam::{account}:server-certificate{path}{name}", - partition = fakecloud_aws::arn::partition_for(&req.region), + partition = existing_arn_partition(&updated.arn, &req.region), account = req.account_id, path = if updated.path.starts_with('/') { updated.path.clone() diff --git a/crates/fakecloud-iam/src/iam_service/groups.rs b/crates/fakecloud-iam/src/iam_service/groups.rs index 44e5f7a7b..402966bb2 100644 --- a/crates/fakecloud-iam/src/iam_service/groups.rs +++ b/crates/fakecloud-iam/src/iam_service/groups.rs @@ -6,7 +6,10 @@ use fakecloud_core::validation::*; use crate::state::IamGroup; -use super::{empty_response, generate_id, url_encode, validate_list_pagination, IamService}; +use super::{ + empty_response, existing_arn_partition, generate_id, url_encode, validate_list_pagination, + IamService, +}; use fakecloud_core::query::required_param; use fakecloud_aws::xml::xml_escape; @@ -359,7 +362,7 @@ impl IamService { group.group_name = actual_new_name.clone(); group.arn = format!( "arn:{}:iam::{}:group{}{}", - fakecloud_aws::arn::partition_for(&req.region), + existing_arn_partition(&group.arn, &req.region), state.account_id, if group.path == "/" { "/" } else { &group.path }, actual_new_name diff --git a/crates/fakecloud-iam/src/iam_service/helpers.rs b/crates/fakecloud-iam/src/iam_service/helpers.rs index 50062afe7..b0573c6ec 100644 --- a/crates/fakecloud-iam/src/iam_service/helpers.rs +++ b/crates/fakecloud-iam/src/iam_service/helpers.rs @@ -7,6 +7,18 @@ pub(crate) fn partition_for_region(region: &str) -> &str { fakecloud_aws::arn::partition_for(region) } +/// Partition of an entity's existing ARN, falling back to the request +/// region's. A rename rebuilds the ARN, and IAM being global means the +/// renaming request can come from any region; taking the partition from the +/// stored ARN keeps a cn-/us-gov-/iso- entity in its partition. +pub(crate) fn existing_arn_partition(arn: &str, region: &str) -> String { + arn.split(':') + .nth(1) + .filter(|p| !p.is_empty()) + .unwrap_or_else(|| partition_for_region(region)) + .to_string() +} + /// Actions on the IAM service that mutate state. Kept in sync with the /// dispatch table in `handle`. pub(crate) fn is_mutating_action(action: &str) -> bool { diff --git a/crates/fakecloud-iam/src/iam_service/tests.rs b/crates/fakecloud-iam/src/iam_service/tests.rs index 29a810224..ee851a9fc 100644 --- a/crates/fakecloud-iam/src/iam_service/tests.rs +++ b/crates/fakecloud-iam/src/iam_service/tests.rs @@ -5522,3 +5522,78 @@ fn change_password_resolves_non_aws_partition_principal() { right.principal = Some(principal); svc.change_password(&right).unwrap(); } + +/// IAM is global, so a rename can arrive from any region. Renaming a group +/// or server certificate from a commercial region must keep the `aws-cn` +/// partition it was created with, the same way UpdateUser does. +#[test] +fn rename_from_other_region_keeps_partition() { + let svc = make_service(); + let in_region = |action: &str, params: Vec<(&str, &str)>, region: &str| { + let mut req = make_request(action, params); + req.region = region.to_string(); + req + }; + let arn_of = |resp: AwsResponse| { + let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string(); + extract_xml_tag(&body, "Arn").to_string() + }; + + svc.create_group(&in_region( + "CreateGroup", + vec![("GroupName", "ops")], + "cn-north-1", + )) + .unwrap(); + svc.update_group(&in_region( + "UpdateGroup", + vec![("GroupName", "ops"), ("NewGroupName", "ops2")], + "us-east-1", + )) + .unwrap(); + let group = svc + .get_group(&in_region( + "GetGroup", + vec![("GroupName", "ops2")], + "us-east-1", + )) + .unwrap(); + assert_eq!(arn_of(group), "arn:aws-cn:iam::123456789012:group/ops2"); + + svc.upload_server_certificate(&in_region( + "UploadServerCertificate", + vec![ + ("ServerCertificateName", "web"), + ( + "CertificateBody", + "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----", + ), + ( + "PrivateKey", + "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----", + ), + ], + "cn-north-1", + )) + .unwrap(); + svc.update_server_certificate(&in_region( + "UpdateServerCertificate", + vec![ + ("ServerCertificateName", "web"), + ("NewServerCertificateName", "web2"), + ], + "us-east-1", + )) + .unwrap(); + let cert = svc + .get_server_certificate(&in_region( + "GetServerCertificate", + vec![("ServerCertificateName", "web2")], + "us-east-1", + )) + .unwrap(); + assert_eq!( + arn_of(cert), + "arn:aws-cn:iam::123456789012:server-certificate/web2" + ); +} diff --git a/crates/fakecloud-iam/src/iam_service/users.rs b/crates/fakecloud-iam/src/iam_service/users.rs index 1910298d8..b165e5a72 100644 --- a/crates/fakecloud-iam/src/iam_service/users.rs +++ b/crates/fakecloud-iam/src/iam_service/users.rs @@ -9,10 +9,10 @@ use crate::state::{IamAccessKey, IamState, IamUser, SigningCertificate, SshPubli use crate::xml_responses; use super::{ - empty_response, generate_id, generate_long_id, parse_tag_keys, parse_tags, - partition_for_region, required_param_with_code, resolve_calling_user, tags_xml, url_encode, - validate_optional_string_length_with_code, validate_string_length_with_code, validate_tags, - validate_untag_keys, IamService, + empty_response, existing_arn_partition, generate_id, generate_long_id, parse_tag_keys, + parse_tags, partition_for_region, required_param_with_code, resolve_calling_user, tags_xml, + url_encode, validate_optional_string_length_with_code, validate_string_length_with_code, + validate_tags, validate_untag_keys, IamService, }; use fakecloud_core::query::required_param; @@ -437,17 +437,7 @@ impl IamService { let actual_new_name = new_user_name.unwrap_or_else(|| user_name.clone()); user.user_name = actual_new_name.clone(); - // Preserve the existing ARN's partition (aws / aws-cn / aws-us-gov - // / aws-iso*) rather than hardcoding `arn:aws:`, which would flip - // the partition on a rename in cn-/us-gov-/iso- regions. Derive it - // from the user's current ARN, falling back to the region. - let partition = user - .arn - .split(':') - .nth(1) - .filter(|p| !p.is_empty()) - .map(str::to_string) - .unwrap_or_else(|| partition_for_region(&req.region).to_string()); + let partition = existing_arn_partition(&user.arn, &req.region); user.arn = format!( "arn:{}:iam::{}:user{}{}", partition, diff --git a/crates/fakecloud-iam/src/pass_role.rs b/crates/fakecloud-iam/src/pass_role.rs index c01dc5372..3d2406e23 100644 --- a/crates/fakecloud-iam/src/pass_role.rs +++ b/crates/fakecloud-iam/src/pass_role.rs @@ -35,12 +35,12 @@ impl IamRoleTrustValidator { } } -/// Parse `arn:aws:iam:::role[/]/` into role name. -/// Returns `None` if the ARN is not an IAM role ARN. +/// Parse `arn::iam:::role[/]/` into role +/// name, in any partition. Returns `None` if the ARN is not an IAM role ARN. fn role_name_from_arn(role_arn: &str) -> Option<&str> { - // Format: arn:aws:iam:::role// // Extract the substring after the last "/". - let role_part = role_arn.strip_prefix("arn:aws:iam::")?; + let (_partition, rest) = role_arn.strip_prefix("arn:")?.split_once(':')?; + let role_part = rest.strip_prefix("iam::")?; let (_account, rest) = role_part.split_once(':')?; let role_path = rest.strip_prefix("role/")?; role_path.rsplit('/').next() @@ -182,6 +182,18 @@ mod tests { role_name_from_arn("arn:aws:iam::000000000000:role/service-role/MyRole"), Some("MyRole") ); + assert_eq!( + role_name_from_arn("arn:aws-cn:iam::000000000000:role/MyRole"), + Some("MyRole") + ); + assert_eq!( + role_name_from_arn("arn:aws-us-gov:iam::000000000000:role/svc/MyRole"), + Some("MyRole") + ); + assert_eq!( + role_name_from_arn("arn:aws-cn:sts::000000000000:role/MyRole"), + None + ); assert_eq!(role_name_from_arn("not-an-arn"), None); }