Skip to main content

fakecloud_iam/
state.rs

1use chrono::{DateTime, Utc};
2use parking_lot::RwLock;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone)]
6pub struct IamUser {
7    pub user_name: String,
8    pub user_id: String,
9    pub arn: String,
10    pub path: String,
11    pub created_at: DateTime<Utc>,
12    pub tags: Vec<Tag>,
13    pub permissions_boundary: Option<String>,
14}
15
16#[derive(Debug, Clone)]
17pub struct IamAccessKey {
18    pub access_key_id: String,
19    pub secret_access_key: String,
20    pub user_name: String,
21    pub status: String,
22    pub created_at: DateTime<Utc>,
23}
24
25#[derive(Debug, Clone)]
26pub struct IamRole {
27    pub role_name: String,
28    pub role_id: String,
29    pub arn: String,
30    pub path: String,
31    pub assume_role_policy_document: String,
32    pub created_at: DateTime<Utc>,
33    pub description: Option<String>,
34    pub max_session_duration: i32,
35    pub tags: Vec<Tag>,
36    pub permissions_boundary: Option<String>,
37}
38
39#[derive(Debug, Clone)]
40pub struct IamPolicy {
41    pub policy_name: String,
42    pub policy_id: String,
43    pub arn: String,
44    pub path: String,
45    pub description: String,
46    pub created_at: DateTime<Utc>,
47    pub tags: Vec<Tag>,
48    pub default_version_id: String,
49    pub versions: Vec<PolicyVersion>,
50    pub next_version_num: u32,
51    pub attachment_count: u32,
52}
53
54#[derive(Debug, Clone)]
55pub struct PolicyVersion {
56    pub version_id: String,
57    pub document: String,
58    pub is_default: bool,
59    pub created_at: DateTime<Utc>,
60}
61
62#[derive(Debug, Clone)]
63pub struct IamGroup {
64    pub group_name: String,
65    pub group_id: String,
66    pub arn: String,
67    pub path: String,
68    pub created_at: DateTime<Utc>,
69    pub members: Vec<String>,                     // user names
70    pub inline_policies: HashMap<String, String>, // policy_name -> document
71    pub attached_policies: Vec<String>,           // policy ARNs
72}
73
74#[derive(Debug, Clone)]
75pub struct IamInstanceProfile {
76    pub instance_profile_name: String,
77    pub instance_profile_id: String,
78    pub arn: String,
79    pub path: String,
80    pub created_at: DateTime<Utc>,
81    pub roles: Vec<String>, // role names
82    pub tags: Vec<Tag>,
83}
84
85#[derive(Debug, Clone)]
86pub struct Tag {
87    pub key: String,
88    pub value: String,
89}
90
91#[derive(Debug, Clone)]
92pub struct LoginProfile {
93    pub user_name: String,
94    pub created_at: DateTime<Utc>,
95    pub password_reset_required: bool,
96}
97
98#[derive(Debug, Clone)]
99pub struct SamlProvider {
100    pub arn: String,
101    pub name: String,
102    pub saml_metadata_document: String,
103    pub created_at: DateTime<Utc>,
104    pub valid_until: DateTime<Utc>,
105    pub tags: Vec<Tag>,
106}
107
108#[derive(Debug, Clone)]
109pub struct OidcProvider {
110    pub arn: String,
111    pub url: String,
112    pub client_id_list: Vec<String>,
113    pub thumbprint_list: Vec<String>,
114    pub created_at: DateTime<Utc>,
115    pub tags: Vec<Tag>,
116}
117
118#[derive(Debug, Clone)]
119pub struct ServerCertificate {
120    pub server_certificate_name: String,
121    pub server_certificate_id: String,
122    pub arn: String,
123    pub path: String,
124    pub certificate_body: String,
125    pub certificate_chain: Option<String>,
126    pub upload_date: DateTime<Utc>,
127    pub expiration: DateTime<Utc>,
128    pub tags: Vec<Tag>,
129}
130
131#[derive(Debug, Clone)]
132pub struct SigningCertificate {
133    pub certificate_id: String,
134    pub user_name: String,
135    pub certificate_body: String,
136    pub status: String,
137    pub upload_date: DateTime<Utc>,
138}
139
140#[derive(Debug, Clone)]
141pub struct AccountPasswordPolicy {
142    pub minimum_password_length: u32,
143    pub require_symbols: bool,
144    pub require_numbers: bool,
145    pub require_uppercase_characters: bool,
146    pub require_lowercase_characters: bool,
147    pub allow_users_to_change_password: bool,
148    pub max_password_age: u32,
149    pub password_reuse_prevention: u32,
150    pub hard_expiry: bool,
151}
152
153impl Default for AccountPasswordPolicy {
154    fn default() -> Self {
155        Self {
156            minimum_password_length: 6,
157            require_symbols: false,
158            require_numbers: false,
159            require_uppercase_characters: false,
160            require_lowercase_characters: false,
161            allow_users_to_change_password: false,
162            max_password_age: 0,
163            password_reuse_prevention: 0,
164            hard_expiry: false,
165        }
166    }
167}
168
169#[derive(Debug, Clone)]
170pub struct VirtualMfaDevice {
171    pub serial_number: String,
172    pub base32_string_seed: String,
173    pub qr_code_png: String,
174    pub enable_date: Option<DateTime<Utc>>,
175    pub user: Option<String>,
176    pub tags: Vec<Tag>,
177}
178
179#[derive(Debug, Clone)]
180pub struct ServiceLinkedRoleDeletion {
181    pub deletion_task_id: String,
182    pub status: String,
183}
184
185/// Identity associated with a set of credentials, for GetCallerIdentity resolution.
186#[derive(Debug, Clone)]
187pub struct CredentialIdentity {
188    pub arn: String,
189    pub user_id: String,
190    pub account_id: String,
191}
192
193/// A temporary credential issued by STS (`AssumeRole`, `AssumeRoleWithWebIdentity`,
194/// `AssumeRoleWithSAML`, `GetSessionToken`, `GetFederationToken`).
195///
196/// Unlike [`CredentialIdentity`], which only remembers the principal ARN for
197/// `GetCallerIdentity`, this struct also retains the secret access key and
198/// session token so that SigV4 verification and IAM enforcement (added in
199/// later batches) can look them up when a client signs a request with
200/// temporary credentials. `expiration` is the absolute wall-clock time at
201/// which the credential becomes invalid.
202#[derive(Debug, Clone)]
203pub struct StsTempCredential {
204    pub access_key_id: String,
205    pub secret_access_key: String,
206    pub session_token: String,
207    pub principal_arn: String,
208    pub user_id: String,
209    pub account_id: String,
210    pub expiration: DateTime<Utc>,
211}
212
213/// Result of looking up a set of credentials by access key ID.
214///
215/// Carries the secret + resolved principal + owning account id. The account
216/// id is intentionally read from the credential itself rather than from
217/// global config, so that once #381 (multi-account isolation) lands, the same
218/// lookup already returns the correct account for the credential.
219#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct SecretLookup {
221    pub secret_access_key: String,
222    pub session_token: Option<String>,
223    pub principal_arn: String,
224    pub user_id: String,
225    pub account_id: String,
226}
227
228#[derive(Debug, Clone)]
229pub struct SshPublicKey {
230    pub ssh_public_key_id: String,
231    pub user_name: String,
232    pub ssh_public_key_body: String,
233    pub status: String,
234    pub upload_date: DateTime<Utc>,
235    pub fingerprint: String,
236}
237
238/// Tracks when an access key was last used.
239#[derive(Debug, Clone)]
240pub struct AccessKeyLastUsed {
241    pub last_used_date: DateTime<Utc>,
242    pub service_name: String,
243    pub region: String,
244}
245
246pub struct IamState {
247    pub account_id: String,
248    pub users: HashMap<String, IamUser>,
249    pub access_keys: HashMap<String, Vec<IamAccessKey>>, // username -> keys
250    pub roles: HashMap<String, IamRole>,
251    pub policies: HashMap<String, IamPolicy>, // arn -> policy
252    pub role_policies: HashMap<String, Vec<String>>, // role_name -> managed policy arns
253    pub role_inline_policies: HashMap<String, HashMap<String, String>>, // role_name -> {policy_name -> doc}
254    pub user_policies: HashMap<String, Vec<String>>, // user_name -> managed policy arns
255    pub user_inline_policies: HashMap<String, HashMap<String, String>>, // user_name -> {policy_name -> doc}
256    pub groups: HashMap<String, IamGroup>,
257    pub instance_profiles: HashMap<String, IamInstanceProfile>,
258    pub login_profiles: HashMap<String, LoginProfile>,
259    pub saml_providers: HashMap<String, SamlProvider>, // arn -> provider
260    pub oidc_providers: HashMap<String, OidcProvider>, // arn -> provider
261    pub server_certificates: HashMap<String, ServerCertificate>, // name -> cert
262    pub signing_certificates: HashMap<String, Vec<SigningCertificate>>, // user_name -> certs
263    pub account_aliases: Vec<String>,
264    pub account_password_policy: Option<AccountPasswordPolicy>,
265    pub virtual_mfa_devices: HashMap<String, VirtualMfaDevice>, // serial_number -> device
266    pub service_linked_role_deletions: HashMap<String, ServiceLinkedRoleDeletion>,
267    /// Maps access key ID to the identity that should be returned by GetCallerIdentity.
268    pub credential_identities: HashMap<String, CredentialIdentity>,
269    /// Temporary credentials issued by STS, keyed by access key ID. Includes
270    /// the secret access key and session token — required for SigV4
271    /// verification and IAM enforcement. Expired entries are purged lazily on
272    /// lookup.
273    pub sts_temp_credentials: HashMap<String, StsTempCredential>,
274    pub credential_report_generated: bool,
275    pub ssh_public_keys: HashMap<String, Vec<SshPublicKey>>, // user_name -> keys
276    pub access_key_last_used: HashMap<String, AccessKeyLastUsed>,
277}
278
279impl IamState {
280    pub fn new(account_id: &str) -> Self {
281        Self {
282            account_id: account_id.to_string(),
283            users: HashMap::new(),
284            access_keys: HashMap::new(),
285            roles: HashMap::new(),
286            policies: HashMap::new(),
287            role_policies: HashMap::new(),
288            role_inline_policies: HashMap::new(),
289            user_policies: HashMap::new(),
290            user_inline_policies: HashMap::new(),
291            groups: HashMap::new(),
292            instance_profiles: HashMap::new(),
293            login_profiles: HashMap::new(),
294            saml_providers: HashMap::new(),
295            oidc_providers: HashMap::new(),
296            server_certificates: HashMap::new(),
297            signing_certificates: HashMap::new(),
298            account_aliases: Vec::new(),
299            account_password_policy: None,
300            virtual_mfa_devices: HashMap::new(),
301            service_linked_role_deletions: HashMap::new(),
302            credential_identities: HashMap::new(),
303            sts_temp_credentials: HashMap::new(),
304            credential_report_generated: false,
305            ssh_public_keys: HashMap::new(),
306            access_key_last_used: HashMap::new(),
307        }
308    }
309
310    pub fn reset(&mut self) {
311        let account_id = self.account_id.clone();
312        *self = Self::new(&account_id);
313    }
314
315    /// Look up the secret access key, session token, and resolved principal
316    /// for a given access key ID.
317    ///
318    /// Searches IAM user access keys first, then STS temporary credentials.
319    /// Expired STS temporary credentials are purged in-place and skipped.
320    ///
321    /// Returns `None` if the AKID is unknown or its STS credential has
322    /// expired.
323    ///
324    /// Required for SigV4 signature verification (batch 3) and principal
325    /// resolution (batch 4). Callers must hold a write lock on
326    /// [`IamState`] to allow the lazy purge; read-only callers should use
327    /// [`IamState::credential_secret_readonly`].
328    pub fn credential_secret(&mut self, access_key_id: &str) -> Option<SecretLookup> {
329        // IAM user access keys: look up by scanning (same pattern the
330        // existing GetCallerIdentity path uses).
331        for keys in self.access_keys.values() {
332            for key in keys {
333                if key.access_key_id == access_key_id {
334                    if let Some(user) = self.users.get(&key.user_name) {
335                        return Some(SecretLookup {
336                            secret_access_key: key.secret_access_key.clone(),
337                            session_token: None,
338                            principal_arn: user.arn.clone(),
339                            user_id: user.user_id.clone(),
340                            account_id: self.account_id.clone(),
341                        });
342                    }
343                }
344            }
345        }
346
347        // STS temporary credentials: direct hash lookup, with lazy expiry
348        // purging so expired entries don't accumulate.
349        let now = Utc::now();
350        if let Some(temp) = self.sts_temp_credentials.get(access_key_id) {
351            if temp.expiration > now {
352                return Some(SecretLookup {
353                    secret_access_key: temp.secret_access_key.clone(),
354                    session_token: Some(temp.session_token.clone()),
355                    principal_arn: temp.principal_arn.clone(),
356                    user_id: temp.user_id.clone(),
357                    account_id: temp.account_id.clone(),
358                });
359            }
360            self.sts_temp_credentials.remove(access_key_id);
361        }
362        None
363    }
364
365    /// Read-only variant of [`IamState::credential_secret`] that does not
366    /// purge expired entries. Prefer the mutable variant wherever possible
367    /// to keep the temp-credential table small.
368    pub fn credential_secret_readonly(&self, access_key_id: &str) -> Option<SecretLookup> {
369        for keys in self.access_keys.values() {
370            for key in keys {
371                if key.access_key_id == access_key_id {
372                    if let Some(user) = self.users.get(&key.user_name) {
373                        return Some(SecretLookup {
374                            secret_access_key: key.secret_access_key.clone(),
375                            session_token: None,
376                            principal_arn: user.arn.clone(),
377                            user_id: user.user_id.clone(),
378                            account_id: self.account_id.clone(),
379                        });
380                    }
381                }
382            }
383        }
384
385        let now = Utc::now();
386        let temp = self.sts_temp_credentials.get(access_key_id)?;
387        if temp.expiration <= now {
388            return None;
389        }
390        Some(SecretLookup {
391            secret_access_key: temp.secret_access_key.clone(),
392            session_token: Some(temp.session_token.clone()),
393            principal_arn: temp.principal_arn.clone(),
394            user_id: temp.user_id.clone(),
395            account_id: temp.account_id.clone(),
396        })
397    }
398}
399
400pub type SharedIamState = std::sync::Arc<RwLock<IamState>>;
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405
406    fn iam_user(name: &str, account_id: &str) -> IamUser {
407        IamUser {
408            user_name: name.to_string(),
409            user_id: format!("AIDA{}", name.to_uppercase()),
410            arn: format!("arn:aws:iam::{}:user/{}", account_id, name),
411            path: "/".to_string(),
412            created_at: Utc::now(),
413            tags: Vec::new(),
414            permissions_boundary: None,
415        }
416    }
417
418    fn iam_key(user: &str, akid: &str, secret: &str) -> IamAccessKey {
419        IamAccessKey {
420            access_key_id: akid.to_string(),
421            secret_access_key: secret.to_string(),
422            user_name: user.to_string(),
423            status: "Active".to_string(),
424            created_at: Utc::now(),
425        }
426    }
427
428    #[test]
429    fn credential_secret_returns_iam_user_key() {
430        let mut state = IamState::new("123456789012");
431        state
432            .users
433            .insert("alice".to_string(), iam_user("alice", "123456789012"));
434        state.access_keys.insert(
435            "alice".to_string(),
436            vec![iam_key("alice", "FKIAALICE", "secret-alice")],
437        );
438        let lookup = state.credential_secret("FKIAALICE").unwrap();
439        assert_eq!(lookup.secret_access_key, "secret-alice");
440        assert_eq!(lookup.principal_arn, "arn:aws:iam::123456789012:user/alice");
441        assert_eq!(lookup.account_id, "123456789012");
442        assert_eq!(lookup.session_token, None);
443    }
444
445    #[test]
446    fn credential_secret_returns_sts_temp_credential_when_unexpired() {
447        let mut state = IamState::new("123456789012");
448        state.sts_temp_credentials.insert(
449            "FSIATEMPKEY".to_string(),
450            StsTempCredential {
451                access_key_id: "FSIATEMPKEY".to_string(),
452                secret_access_key: "temp-secret".to_string(),
453                session_token: "temp-token".to_string(),
454                principal_arn: "arn:aws:sts::123456789012:assumed-role/R/s".to_string(),
455                user_id: "AROA:session".to_string(),
456                account_id: "123456789012".to_string(),
457                expiration: Utc::now() + chrono::Duration::minutes(30),
458            },
459        );
460        let lookup = state.credential_secret("FSIATEMPKEY").unwrap();
461        assert_eq!(lookup.secret_access_key, "temp-secret");
462        assert_eq!(lookup.session_token.as_deref(), Some("temp-token"));
463        assert_eq!(
464            lookup.principal_arn,
465            "arn:aws:sts::123456789012:assumed-role/R/s"
466        );
467    }
468
469    #[test]
470    fn credential_secret_purges_expired_sts_credentials() {
471        let mut state = IamState::new("123456789012");
472        state.sts_temp_credentials.insert(
473            "FSIAOLD".to_string(),
474            StsTempCredential {
475                access_key_id: "FSIAOLD".to_string(),
476                secret_access_key: "s".to_string(),
477                session_token: "t".to_string(),
478                principal_arn: "arn".to_string(),
479                user_id: "id".to_string(),
480                account_id: "123456789012".to_string(),
481                expiration: Utc::now() - chrono::Duration::seconds(1),
482            },
483        );
484        assert!(state.credential_secret("FSIAOLD").is_none());
485        assert!(!state.sts_temp_credentials.contains_key("FSIAOLD"));
486    }
487
488    #[test]
489    fn credential_secret_readonly_does_not_purge() {
490        let mut state = IamState::new("123456789012");
491        state.sts_temp_credentials.insert(
492            "FSIAOLD".to_string(),
493            StsTempCredential {
494                access_key_id: "FSIAOLD".to_string(),
495                secret_access_key: "s".to_string(),
496                session_token: "t".to_string(),
497                principal_arn: "arn".to_string(),
498                user_id: "id".to_string(),
499                account_id: "123456789012".to_string(),
500                expiration: Utc::now() - chrono::Duration::seconds(1),
501            },
502        );
503        assert!(state.credential_secret_readonly("FSIAOLD").is_none());
504        assert!(state.sts_temp_credentials.contains_key("FSIAOLD"));
505    }
506
507    #[test]
508    fn credential_secret_returns_none_for_unknown_akid() {
509        let mut state = IamState::new("123456789012");
510        assert!(state.credential_secret("FKIAUNKNOWN").is_none());
511    }
512}