diff --git a/crates/fakecloud-e2e/tests/multi_account.rs b/crates/fakecloud-e2e/tests/multi_account.rs index 0797eef81..c83a23239 100644 --- a/crates/fakecloud-e2e/tests/multi_account.rs +++ b/crates/fakecloud-e2e/tests/multi_account.rs @@ -102,6 +102,139 @@ async fn sts_assume_role_routes_to_target_account() { .contains(ACCOUNT_B)); } +// ====================================================================== +// IAM: every entity ARN carries the account it was created in +// ====================================================================== + +const LIST_BUCKET_POLICY: &str = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:ListBucket","Resource":"*"}]}"#; + +/// Creates a user, a customer-managed policy, a role, and a group through +/// `iam` and asserts each ARN names `account`, and that the policy and user +/// resolve by those ARNs. +async fn assert_iam_entities_in_account(iam: &aws_sdk_iam::Client, account: &str) { + let user = iam.create_user().user_name("alice").send().await.unwrap(); + let user_arn = user.user().unwrap().arn(); + assert_eq!(user_arn, format!("arn:aws:iam::{account}:user/alice")); + + let policy = iam + .create_policy() + .policy_name("list") + .policy_document(LIST_BUCKET_POLICY) + .send() + .await + .unwrap(); + let policy_arn = policy.policy().unwrap().arn().unwrap().to_string(); + assert_eq!(policy_arn, format!("arn:aws:iam::{account}:policy/list")); + + let role = iam + .create_role() + .role_name("svc") + .assume_role_policy_document(r#"{"Version":"2012-10-17","Statement":[]}"#) + .send() + .await + .unwrap(); + assert_eq!( + role.role().unwrap().arn(), + format!("arn:aws:iam::{account}:role/svc") + ); + + let group = iam.create_group().group_name("ops").send().await.unwrap(); + assert_eq!( + group.group().unwrap().arn(), + format!("arn:aws:iam::{account}:group/ops") + ); + + // The policy resolves by the ARN CreatePolicy returned, and attaching it + // by that ARN reaches the user. + let fetched = iam + .get_policy() + .policy_arn(&policy_arn) + .send() + .await + .unwrap(); + assert_eq!(fetched.policy().unwrap().arn(), Some(policy_arn.as_str())); + iam.attach_user_policy() + .user_name("alice") + .policy_arn(&policy_arn) + .send() + .await + .unwrap(); + let attached = iam + .list_attached_user_policies() + .user_name("alice") + .send() + .await + .unwrap(); + assert_eq!( + attached.attached_policies()[0].policy_arn(), + Some(policy_arn.as_str()) + ); + + let fetched_user = iam.get_user().user_name("alice").send().await.unwrap(); + assert_eq!(fetched_user.user().unwrap().arn(), user_arn); +} + +#[tokio::test] +async fn iam_entity_arns_use_non_default_account() { + let server = start().await; + let (b_akid, b_secret) = server.create_admin(ACCOUNT_B, "admin-b").await; + let b_cfg = config_with(&server, &b_akid, &b_secret).await; + let iam_b = aws_sdk_iam::Client::new(&b_cfg); + + assert_iam_entities_in_account(&iam_b, ACCOUNT_B).await; +} + +#[tokio::test] +async fn iam_entity_arns_use_assumed_role_account() { + let server = start().await; + let (a_akid, a_secret) = server.create_admin(ACCOUNT_A, "admin-a").await; + let (b_akid, b_secret) = server.create_admin(ACCOUNT_B, "admin-b").await; + + // An admin role in account B that account A may assume. + let b_cfg = config_with(&server, &b_akid, &b_secret).await; + let iam_b = aws_sdk_iam::Client::new(&b_cfg); + iam_b + .create_role() + .role_name("admin-role") + .assume_role_policy_document( + r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"sts:AssumeRole"}]}"#, + ) + .send() + .await + .unwrap(); + iam_b + .attach_role_policy() + .role_name("admin-role") + .policy_arn("arn:aws:iam::aws:policy/AdministratorAccess") + .send() + .await + .unwrap(); + + let a_cfg = config_with(&server, &a_akid, &a_secret).await; + let assumed = StsClient::new(&a_cfg) + .assume_role() + .role_arn(format!("arn:aws:iam::{ACCOUNT_B}:role/admin-role")) + .role_session_name("cross") + .send() + .await + .unwrap(); + let creds = assumed.credentials().unwrap(); + let session_cfg = aws_config::defaults(aws_config::BehaviorVersion::latest()) + .endpoint_url(server.endpoint()) + .region(aws_config::Region::new("us-east-1")) + .credentials_provider(Credentials::new( + creds.access_key_id(), + creds.secret_access_key(), + Some(creds.session_token().to_string()), + None, + "multi-acct-session", + )) + .load() + .await; + + assert_iam_entities_in_account(&aws_sdk_iam::Client::new(&session_cfg), ACCOUNT_B).await; +} + // ====================================================================== // SQS: queues isolated per account // ====================================================================== diff --git a/crates/fakecloud-iam/src/iam_service/helpers.rs b/crates/fakecloud-iam/src/iam_service/helpers.rs index 2e13ded94..50062afe7 100644 --- a/crates/fakecloud-iam/src/iam_service/helpers.rs +++ b/crates/fakecloud-iam/src/iam_service/helpers.rs @@ -481,13 +481,6 @@ pub(crate) fn iam_action_resource( } } -/// Extract the caller's access key from the request's Authorization header. -pub(crate) fn extract_access_key(req: &AwsRequest) -> Option { - let auth = req.headers.get("authorization")?.to_str().ok()?; - let info = fakecloud_aws::sigv4::parse_sigv4(auth)?; - Some(info.access_key) -} - /// Convert a hyphenated service name to title case, handling known abbreviations. pub(crate) fn title_case_service(s: &str) -> String { s.split('-') diff --git a/crates/fakecloud-iam/src/iam_service/policies.rs b/crates/fakecloud-iam/src/iam_service/policies.rs index 222013200..e5a5050d4 100644 --- a/crates/fakecloud-iam/src/iam_service/policies.rs +++ b/crates/fakecloud-iam/src/iam_service/policies.rs @@ -42,14 +42,13 @@ impl IamService { let input = CreatePolicyInput::from_query(&req.query_params)?; let partition = partition_for_region(&req.region); - let effective_account = self.effective_account_id(req); let mut accounts = self.state.write(); let state = accounts.get_or_create(&req.account_id); let arn = format!( "arn:{}:iam::{}:policy{}{}", - partition, effective_account, input.path, input.policy_name + partition, state.account_id, input.path, input.policy_name ); if state.policies.contains_key(&arn) { diff --git a/crates/fakecloud-iam/src/iam_service/users.rs b/crates/fakecloud-iam/src/iam_service/users.rs index a402ffd36..1910298d8 100644 --- a/crates/fakecloud-iam/src/iam_service/users.rs +++ b/crates/fakecloud-iam/src/iam_service/users.rs @@ -9,7 +9,7 @@ use crate::state::{IamAccessKey, IamState, IamUser, SigningCertificate, SshPubli use crate::xml_responses; use super::{ - empty_response, extract_access_key, generate_id, generate_long_id, parse_tag_keys, parse_tags, + 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, @@ -185,20 +185,6 @@ fn rename_user_references(state: &mut IamState, old_name: &str, new_name: &str) // ========= User operations ========= impl IamService { - /// Determine the effective account ID for this request. - /// If the caller has assumed a role into a different account, use that account ID. - /// MUST be called before acquiring a write lock on self.state. - pub(super) fn effective_account_id(&self, req: &AwsRequest) -> String { - if let Some(access_key) = extract_access_key(req) { - let mut accounts = self.state.write(); - let state = accounts.get_or_create(&req.account_id); - if let Some(identity) = state.credential_identities.get(&access_key) { - return identity.account_id.clone(); - } - } - self.state.read().default_account_id().to_string() - } - pub(super) fn create_user(&self, req: &AwsRequest) -> Result { let user_name = required_param(&req.query_params, "UserName")?; validate_string_length("userName", &user_name, 1, 64)?; @@ -212,7 +198,6 @@ impl IamService { let permissions_boundary = req.query_params.get("PermissionsBoundary").cloned(); let partition = partition_for_region(&req.region); - let effective_account = self.effective_account_id(req); let mut accounts = self.state.write(); let state = accounts.get_or_create(&req.account_id); @@ -229,7 +214,7 @@ impl IamService { arn: format!( "arn:{}:iam::{}:user{}{}", partition, - effective_account, + state.account_id, if path == "/" { "/" } else { &path }, user_name ),