Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions crates/fakecloud-iam/src/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,12 @@ fn classify_aws_principal(s: &str) -> PrincipalRef {
if s == "*" {
return PrincipalRef::AnyAws;
}
// `arn:aws:iam::<account>:root` → account root
if let Some(rest) = s.strip_prefix("arn:aws:iam::") {
// `arn:<partition>:iam::<account>: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());
Expand Down
15 changes: 15 additions & 0 deletions crates/fakecloud-iam/src/evaluator_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
3 changes: 2 additions & 1 deletion crates/fakecloud-iam/src/iam_service/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,8 @@ impl IamService {

// Root account row (after users)
csv.push_str(&format!(
"<root_account>,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",
"<root_account>,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")
));
Expand Down
19 changes: 12 additions & 7 deletions crates/fakecloud-iam/src/iam_service/extras.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -1288,7 +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::")
arn.strip_prefix("arn:")
.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())
});
Expand Down Expand Up @@ -1506,7 +1510,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 = existing_arn_partition(&updated.arn, &req.region),
account = req.account_id,
path = if updated.path.starts_with('/') {
updated.path.clone()
Expand Down
11 changes: 8 additions & 3 deletions crates/fakecloud-iam/src/iam_service/groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -37,7 +40,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
Expand Down Expand Up @@ -357,7 +361,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{}{}",
existing_arn_partition(&group.arn, &req.region),
state.account_id,
if group.path == "/" { "/" } else { &group.path },
actual_new_name
Expand Down
12 changes: 12 additions & 0 deletions crates/fakecloud-iam/src/iam_service/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 9 additions & 5 deletions crates/fakecloud-iam/src/iam_service/oidc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading