Skip to main content

fakecloud_iam/
credential_resolver.rs

1//! Adapter that implements [`fakecloud_core::auth::CredentialResolver`] over
2//! the shared IAM state.
3//!
4//! SigV4 verification (and later IAM enforcement) runs in `fakecloud-core`,
5//! which intentionally doesn't depend on `fakecloud-iam`. The trait lives in
6//! core and the concrete resolver lives here, keeping the dependency edge
7//! pointing the right way.
8
9use std::sync::Arc;
10
11use fakecloud_core::auth::{CredentialResolver, Principal, PrincipalType, ResolvedCredential};
12
13use crate::state::SharedIamState;
14
15/// [`CredentialResolver`] backed by an [`IamState`] shared via
16/// [`SharedIamState`]. Acquires a write lock on lookup so expired STS
17/// temporary credentials are purged in place.
18#[derive(Clone)]
19pub struct IamCredentialResolver {
20    state: SharedIamState,
21}
22
23impl IamCredentialResolver {
24    pub fn new(state: SharedIamState) -> Self {
25        Self { state }
26    }
27
28    pub fn shared(state: SharedIamState) -> Arc<dyn CredentialResolver> {
29        Arc::new(Self::new(state))
30    }
31}
32
33impl CredentialResolver for IamCredentialResolver {
34    fn resolve(&self, access_key_id: &str) -> Option<ResolvedCredential> {
35        let mut state = self.state.write();
36        let lookup = state.credential_secret(access_key_id)?;
37        // Classify the principal by ARN shape. IAM user access keys carry
38        // a `:user/` ARN; STS temp credentials carry the assumed-role /
39        // federated-user / root ARN that was stashed when the credential
40        // was issued (batch 2).
41        let principal_type = PrincipalType::from_arn(&lookup.principal_arn);
42        Some(ResolvedCredential {
43            secret_access_key: lookup.secret_access_key,
44            session_token: lookup.session_token,
45            principal: Principal {
46                arn: lookup.principal_arn,
47                user_id: lookup.user_id,
48                account_id: lookup.account_id,
49                principal_type,
50                source_identity: None,
51            },
52        })
53    }
54}
55
56// Prevent rustc from warning on the unused import when the module is
57// included via `lib.rs` without tests referencing it.
58#[allow(dead_code)]
59fn _assert_impl<T: CredentialResolver>() {}
60const _: fn() = || {
61    _assert_impl::<IamCredentialResolver>();
62};
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use crate::state::{IamAccessKey, IamState, IamUser};
68    use chrono::Utc;
69    use parking_lot::RwLock;
70
71    #[test]
72    fn resolves_iam_user_secret_from_state() {
73        let mut state = IamState::new("123456789012");
74        state.users.insert(
75            "alice".to_string(),
76            IamUser {
77                user_name: "alice".into(),
78                user_id: "AIDAALICE".into(),
79                arn: "arn:aws:iam::123456789012:user/alice".into(),
80                path: "/".into(),
81                created_at: Utc::now(),
82                tags: Vec::new(),
83                permissions_boundary: None,
84            },
85        );
86        state.access_keys.insert(
87            "alice".to_string(),
88            vec![IamAccessKey {
89                access_key_id: "FKIAALICE".into(),
90                secret_access_key: "the-secret".into(),
91                user_name: "alice".into(),
92                status: "Active".into(),
93                created_at: Utc::now(),
94            }],
95        );
96        let resolver = IamCredentialResolver::new(Arc::new(RwLock::new(state)));
97        let resolved = resolver.resolve("FKIAALICE").unwrap();
98        assert_eq!(resolved.secret_access_key, "the-secret");
99        assert_eq!(
100            resolved.principal.arn,
101            "arn:aws:iam::123456789012:user/alice"
102        );
103        assert_eq!(resolved.principal.principal_type, PrincipalType::User);
104        assert_eq!(resolved.session_token, None);
105    }
106
107    #[test]
108    fn returns_none_for_unknown_akid() {
109        let state = IamState::new("123456789012");
110        let resolver = IamCredentialResolver::new(Arc::new(RwLock::new(state)));
111        assert!(resolver.resolve("FKIANONE").is_none());
112    }
113
114    #[test]
115    fn classifies_sts_assumed_role_principal() {
116        use crate::state::StsTempCredential;
117        let mut state = IamState::new("123456789012");
118        state.sts_temp_credentials.insert(
119            "FSIATEMP".to_string(),
120            StsTempCredential {
121                access_key_id: "FSIATEMP".into(),
122                secret_access_key: "temp-secret".into(),
123                session_token: "temp-token".into(),
124                principal_arn: "arn:aws:sts::123456789012:assumed-role/ops/session".into(),
125                user_id: "AROA:session".into(),
126                account_id: "123456789012".into(),
127                expiration: Utc::now() + chrono::Duration::minutes(30),
128            },
129        );
130        let resolver = IamCredentialResolver::new(Arc::new(RwLock::new(state)));
131        let resolved = resolver.resolve("FSIATEMP").unwrap();
132        assert_eq!(
133            resolved.principal.principal_type,
134            PrincipalType::AssumedRole
135        );
136        assert_eq!(resolved.session_token.as_deref(), Some("temp-token"));
137    }
138}