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
133 changes: 133 additions & 0 deletions crates/fakecloud-e2e/tests/multi_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ======================================================================
Expand Down
7 changes: 0 additions & 7 deletions crates/fakecloud-iam/src/iam_service/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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('-')
Expand Down
3 changes: 1 addition & 2 deletions crates/fakecloud-iam/src/iam_service/policies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
19 changes: 2 additions & 17 deletions crates/fakecloud-iam/src/iam_service/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<AwsResponse, AwsServiceError> {
let user_name = required_param(&req.query_params, "UserName")?;
validate_string_length("userName", &user_name, 1, 64)?;
Expand All @@ -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);
Expand All @@ -229,7 +214,7 @@ impl IamService {
arn: format!(
"arn:{}:iam::{}:user{}{}",
partition,
effective_account,
state.account_id,
if path == "/" { "/" } else { &path },
user_name
),
Expand Down
Loading