Skip to main content

fakecloud_iam/
resource_policy.rs

1//! STS implementation of [`ResourcePolicyProvider`].
2//!
3//! For `sts:AssumeRole*`, the "resource policy" is the role's trust
4//! policy (`assume_role_policy_document`). This provider looks up the
5//! role by ARN in the target account's IAM state and returns the trust
6//! policy document so the cross-account evaluator can apply the correct
7//! `identity AND resource` semantics.
8
9use std::sync::Arc;
10
11use fakecloud_core::auth::ResourcePolicyProvider;
12
13use crate::state::SharedIamState;
14
15pub struct StsResourcePolicyProvider {
16    state: SharedIamState,
17}
18
19impl StsResourcePolicyProvider {
20    pub fn new(state: SharedIamState) -> Self {
21        Self { state }
22    }
23
24    pub fn shared(state: SharedIamState) -> Arc<dyn ResourcePolicyProvider> {
25        Arc::new(Self::new(state))
26    }
27}
28
29impl ResourcePolicyProvider for StsResourcePolicyProvider {
30    fn resource_policy(&self, service: &str, resource_arn: &str) -> Option<String> {
31        if !service.eq_ignore_ascii_case("sts") {
32            return None;
33        }
34        // AssumeRole resource ARN: arn:aws:iam::<account>:role/<name>
35        // Extract account and role name from the ARN.
36        let parts: Vec<&str> = resource_arn.split(':').collect();
37        if parts.len() < 6 {
38            return None;
39        }
40        let account_id = parts[4];
41        let resource = parts[5];
42        let role_name = resource.strip_prefix("role/")?;
43        // Strip path if present: role/path/to/name -> name
44        let role_name = role_name.rsplit('/').next().unwrap_or(role_name);
45
46        let accounts = self.state.read();
47        let state = accounts.get(account_id)?;
48        let role = state.roles.get(role_name)?;
49        Some(role.assume_role_policy_document.clone())
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use crate::state::{IamRole, IamState};
57    use fakecloud_aws::arn::Arn;
58    use fakecloud_core::multi_account::MultiAccountState;
59    use parking_lot::RwLock;
60
61    fn make_state_with_role(
62        account_id: &str,
63        role_name: &str,
64        trust_policy: &str,
65    ) -> SharedIamState {
66        let mut mas = MultiAccountState::<IamState>::new(account_id, "us-east-1", "");
67        let state = mas.get_or_create(account_id);
68        state.roles.insert(
69            role_name.to_string(),
70            IamRole {
71                role_name: role_name.to_string(),
72                role_id: "AROATEST".to_string(),
73                arn: Arn::global("iam", account_id, &format!("role/{role_name}")).to_string(),
74                path: "/".to_string(),
75                assume_role_policy_document: trust_policy.to_string(),
76                created_at: chrono::Utc::now(),
77                description: None,
78                max_session_duration: 3600,
79                tags: Vec::new(),
80                permissions_boundary: None,
81            },
82        );
83        Arc::new(RwLock::new(mas))
84    }
85
86    #[test]
87    fn returns_trust_policy_for_sts_role_arn() {
88        let trust = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"sts:AssumeRole"}]}"#;
89        let state = make_state_with_role("222222222222", "my-role", trust);
90        let provider = StsResourcePolicyProvider::new(state);
91        assert_eq!(
92            provider.resource_policy("sts", "arn:aws:iam::222222222222:role/my-role"),
93            Some(trust.to_string())
94        );
95    }
96
97    #[test]
98    fn returns_none_for_non_sts_service() {
99        let state = make_state_with_role("222222222222", "r", "{}");
100        let provider = StsResourcePolicyProvider::new(state);
101        assert_eq!(
102            provider.resource_policy("s3", "arn:aws:iam::222222222222:role/r"),
103            None
104        );
105    }
106
107    #[test]
108    fn returns_none_for_missing_role() {
109        let state = make_state_with_role("222222222222", "existing", "{}");
110        let provider = StsResourcePolicyProvider::new(state);
111        assert_eq!(
112            provider.resource_policy("sts", "arn:aws:iam::222222222222:role/missing"),
113            None
114        );
115    }
116
117    #[test]
118    fn returns_none_for_missing_account() {
119        let state = make_state_with_role("111111111111", "r", "{}");
120        let provider = StsResourcePolicyProvider::new(state);
121        assert_eq!(
122            provider.resource_policy("sts", "arn:aws:iam::999999999999:role/r"),
123            None
124        );
125    }
126}