Skip to main content

fakecloud_iam/
state.rs

1use chrono::{DateTime, Utc};
2use parking_lot::RwLock;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use fakecloud_core::multi_account::{AccountState, MultiAccountState};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct IamUser {
10    pub user_name: String,
11    pub user_id: String,
12    pub arn: String,
13    pub path: String,
14    pub created_at: DateTime<Utc>,
15    pub tags: Vec<Tag>,
16    pub permissions_boundary: Option<String>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct IamAccessKey {
21    pub access_key_id: String,
22    pub secret_access_key: String,
23    pub user_name: String,
24    pub status: String,
25    pub created_at: DateTime<Utc>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct IamRole {
30    pub role_name: String,
31    pub role_id: String,
32    pub arn: String,
33    pub path: String,
34    pub assume_role_policy_document: String,
35    pub created_at: DateTime<Utc>,
36    pub description: Option<String>,
37    pub max_session_duration: i32,
38    pub tags: Vec<Tag>,
39    pub permissions_boundary: Option<String>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct IamPolicy {
44    pub policy_name: String,
45    pub policy_id: String,
46    pub arn: String,
47    pub path: String,
48    pub description: String,
49    pub created_at: DateTime<Utc>,
50    pub tags: Vec<Tag>,
51    pub default_version_id: String,
52    pub versions: Vec<PolicyVersion>,
53    pub next_version_num: u32,
54    pub attachment_count: u32,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct PolicyVersion {
59    pub version_id: String,
60    pub document: String,
61    pub is_default: bool,
62    pub created_at: DateTime<Utc>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct IamGroup {
67    pub group_name: String,
68    pub group_id: String,
69    pub arn: String,
70    pub path: String,
71    pub created_at: DateTime<Utc>,
72    pub members: Vec<String>,                     // user names
73    pub inline_policies: HashMap<String, String>, // policy_name -> document
74    pub attached_policies: Vec<String>,           // policy ARNs
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct IamInstanceProfile {
79    pub instance_profile_name: String,
80    pub instance_profile_id: String,
81    pub arn: String,
82    pub path: String,
83    pub created_at: DateTime<Utc>,
84    pub roles: Vec<String>, // role names
85    pub tags: Vec<Tag>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct Tag {
90    pub key: String,
91    pub value: String,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct LoginProfile {
96    pub user_name: String,
97    pub created_at: DateTime<Utc>,
98    pub password_reset_required: bool,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct SamlProvider {
103    pub arn: String,
104    pub name: String,
105    pub saml_metadata_document: String,
106    pub created_at: DateTime<Utc>,
107    pub valid_until: DateTime<Utc>,
108    pub tags: Vec<Tag>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct OidcProvider {
113    pub arn: String,
114    pub url: String,
115    pub client_id_list: Vec<String>,
116    pub thumbprint_list: Vec<String>,
117    pub created_at: DateTime<Utc>,
118    pub tags: Vec<Tag>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct ServerCertificate {
123    pub server_certificate_name: String,
124    pub server_certificate_id: String,
125    pub arn: String,
126    pub path: String,
127    pub certificate_body: String,
128    pub certificate_chain: Option<String>,
129    pub upload_date: DateTime<Utc>,
130    pub expiration: DateTime<Utc>,
131    pub tags: Vec<Tag>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct SigningCertificate {
136    pub certificate_id: String,
137    pub user_name: String,
138    pub certificate_body: String,
139    pub status: String,
140    pub upload_date: DateTime<Utc>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct AccountPasswordPolicy {
145    pub minimum_password_length: u32,
146    pub require_symbols: bool,
147    pub require_numbers: bool,
148    pub require_uppercase_characters: bool,
149    pub require_lowercase_characters: bool,
150    pub allow_users_to_change_password: bool,
151    pub max_password_age: u32,
152    pub password_reuse_prevention: u32,
153    pub hard_expiry: bool,
154}
155
156impl Default for AccountPasswordPolicy {
157    fn default() -> Self {
158        Self {
159            minimum_password_length: 6,
160            require_symbols: false,
161            require_numbers: false,
162            require_uppercase_characters: false,
163            require_lowercase_characters: false,
164            allow_users_to_change_password: false,
165            max_password_age: 0,
166            password_reuse_prevention: 0,
167            hard_expiry: false,
168        }
169    }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct VirtualMfaDevice {
174    pub serial_number: String,
175    pub base32_string_seed: String,
176    pub qr_code_png: String,
177    pub enable_date: Option<DateTime<Utc>>,
178    pub user: Option<String>,
179    pub tags: Vec<Tag>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct ServiceLinkedRoleDeletion {
184    pub deletion_task_id: String,
185    pub status: String,
186}
187
188/// Identity associated with a set of credentials, for GetCallerIdentity resolution.
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct CredentialIdentity {
191    pub arn: String,
192    pub user_id: String,
193    pub account_id: String,
194}
195
196/// A temporary credential issued by STS (`AssumeRole`, `AssumeRoleWithWebIdentity`,
197/// `AssumeRoleWithSAML`, `GetSessionToken`, `GetFederationToken`).
198///
199/// Unlike [`CredentialIdentity`], which only remembers the principal ARN for
200/// `GetCallerIdentity`, this struct also retains the secret access key and
201/// session token so that SigV4 verification and IAM enforcement (added in
202/// later batches) can look them up when a client signs a request with
203/// temporary credentials. `expiration` is the absolute wall-clock time at
204/// which the credential becomes invalid.
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct StsTempCredential {
207    pub access_key_id: String,
208    pub secret_access_key: String,
209    pub session_token: String,
210    pub principal_arn: String,
211    pub user_id: String,
212    pub account_id: String,
213    pub expiration: DateTime<Utc>,
214    /// Session policies passed to the STS call that minted this credential.
215    /// Raw JSON policy documents. The `Policy` parameter contributes one
216    /// entry; `PolicyArns` contribute additional entries (resolved to
217    /// documents at mint time). Empty when the STS call carried no
218    /// session policies.
219    #[serde(default)]
220    pub session_policies: Vec<String>,
221}
222
223/// Result of looking up a set of credentials by access key ID.
224///
225/// Carries the secret + resolved principal + owning account id. The account
226/// id is intentionally read from the credential itself rather than from
227/// global config, so that once #381 (multi-account isolation) lands, the same
228/// lookup already returns the correct account for the credential.
229#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct SecretLookup {
231    pub secret_access_key: String,
232    pub session_token: Option<String>,
233    pub principal_arn: String,
234    pub user_id: String,
235    pub account_id: String,
236    /// Session policies from the STS call that minted this credential.
237    /// Empty for IAM user access keys.
238    pub session_policies: Vec<String>,
239    /// Tags on the principal (IAM user or assumed role) for
240    /// `aws:PrincipalTag/<key>` condition evaluation.
241    pub principal_tags: Option<HashMap<String, String>>,
242}
243
244/// Convert a `Vec<Tag>` to a `HashMap<String, String>`.
245/// Returns `None` when the input is empty (no tags to evaluate).
246pub fn tags_to_hashmap(tags: &[Tag]) -> Option<HashMap<String, String>> {
247    if tags.is_empty() {
248        return None;
249    }
250    Some(
251        tags.iter()
252            .map(|t| (t.key.clone(), t.value.clone()))
253            .collect(),
254    )
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct SshPublicKey {
259    pub ssh_public_key_id: String,
260    pub user_name: String,
261    pub ssh_public_key_body: String,
262    pub status: String,
263    pub upload_date: DateTime<Utc>,
264    pub fingerprint: String,
265}
266
267/// Tracks when an access key was last used.
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct AccessKeyLastUsed {
270    pub last_used_date: DateTime<Utc>,
271    pub service_name: String,
272    pub region: String,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct IamState {
277    pub account_id: String,
278    pub users: HashMap<String, IamUser>,
279    pub access_keys: HashMap<String, Vec<IamAccessKey>>, // username -> keys
280    pub roles: HashMap<String, IamRole>,
281    pub policies: HashMap<String, IamPolicy>, // arn -> policy
282    pub role_policies: HashMap<String, Vec<String>>, // role_name -> managed policy arns
283    pub role_inline_policies: HashMap<String, HashMap<String, String>>, // role_name -> {policy_name -> doc}
284    pub user_policies: HashMap<String, Vec<String>>, // user_name -> managed policy arns
285    pub user_inline_policies: HashMap<String, HashMap<String, String>>, // user_name -> {policy_name -> doc}
286    pub groups: HashMap<String, IamGroup>,
287    pub instance_profiles: HashMap<String, IamInstanceProfile>,
288    pub login_profiles: HashMap<String, LoginProfile>,
289    pub saml_providers: HashMap<String, SamlProvider>, // arn -> provider
290    pub oidc_providers: HashMap<String, OidcProvider>, // arn -> provider
291    pub server_certificates: HashMap<String, ServerCertificate>, // name -> cert
292    pub signing_certificates: HashMap<String, Vec<SigningCertificate>>, // user_name -> certs
293    pub account_aliases: Vec<String>,
294    pub account_password_policy: Option<AccountPasswordPolicy>,
295    pub virtual_mfa_devices: HashMap<String, VirtualMfaDevice>, // serial_number -> device
296    pub service_linked_role_deletions: HashMap<String, ServiceLinkedRoleDeletion>,
297    /// Maps access key ID to the identity that should be returned by GetCallerIdentity.
298    pub credential_identities: HashMap<String, CredentialIdentity>,
299    /// Temporary credentials issued by STS, keyed by access key ID. Includes
300    /// the secret access key and session token — required for SigV4
301    /// verification and IAM enforcement. Expired entries are purged lazily on
302    /// lookup.
303    pub sts_temp_credentials: HashMap<String, StsTempCredential>,
304    pub credential_report_generated: bool,
305    pub ssh_public_keys: HashMap<String, Vec<SshPublicKey>>, // user_name -> keys
306    pub access_key_last_used: HashMap<String, AccessKeyLastUsed>,
307    /// Per-user service-specific credentials (Codecommit/Keyspaces).
308    #[serde(default)]
309    pub service_specific_credentials: HashMap<String, Vec<ServiceSpecificCredential>>, // user -> creds
310    /// Active delegation requests keyed by id.
311    #[serde(default)]
312    pub delegation_requests: HashMap<String, DelegationRequest>,
313    /// Per-resource-arn tag map for SAML/Server cert/MFA device tags.
314    #[serde(default)]
315    pub extra_tags: HashMap<String, Vec<(String, String)>>,
316    /// Organizations integration toggles.
317    #[serde(default)]
318    pub organizations_root_credentials_management: bool,
319    #[serde(default)]
320    pub organizations_root_sessions: bool,
321    /// Outbound web identity federation configuration.
322    #[serde(default)]
323    pub outbound_web_identity_federation: Option<OutboundWebIdentityFederation>,
324    /// Generated ServiceLastAccessed jobs keyed by job id.
325    #[serde(default)]
326    pub service_last_accessed_jobs: HashMap<String, ServiceLastAccessedJob>,
327    /// Organizations access reports keyed by job id.
328    #[serde(default)]
329    pub organizations_access_reports: HashMap<String, OrganizationsAccessReport>,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct ServiceSpecificCredential {
334    pub credential_id: String,
335    pub user_name: String,
336    pub service_name: String,
337    pub service_user_name: String,
338    pub service_password: String,
339    pub status: String,
340    pub create_date: DateTime<Utc>,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct DelegationRequest {
345    pub id: String,
346    pub source_account: String,
347    pub target_account: String,
348    pub status: String,
349    pub create_date: DateTime<Utc>,
350    pub permissions: Vec<String>,
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct OutboundWebIdentityFederation {
355    pub enabled: bool,
356    pub issuer_url: String,
357    pub audience: Vec<String>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct ServiceLastAccessedJob {
362    pub job_id: String,
363    pub status: String,
364    pub job_creation_date: DateTime<Utc>,
365    pub arn: String,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct OrganizationsAccessReport {
370    pub job_id: String,
371    pub status: String,
372    pub created_at: DateTime<Utc>,
373    pub entity_path: String,
374}
375
376impl IamState {
377    pub fn new(account_id: &str) -> Self {
378        Self {
379            account_id: account_id.to_string(),
380            users: HashMap::new(),
381            access_keys: HashMap::new(),
382            roles: HashMap::new(),
383            policies: HashMap::new(),
384            role_policies: HashMap::new(),
385            role_inline_policies: HashMap::new(),
386            user_policies: HashMap::new(),
387            user_inline_policies: HashMap::new(),
388            groups: HashMap::new(),
389            instance_profiles: HashMap::new(),
390            login_profiles: HashMap::new(),
391            saml_providers: HashMap::new(),
392            oidc_providers: HashMap::new(),
393            server_certificates: HashMap::new(),
394            signing_certificates: HashMap::new(),
395            account_aliases: Vec::new(),
396            account_password_policy: None,
397            virtual_mfa_devices: HashMap::new(),
398            service_linked_role_deletions: HashMap::new(),
399            credential_identities: HashMap::new(),
400            sts_temp_credentials: HashMap::new(),
401            credential_report_generated: false,
402            ssh_public_keys: HashMap::new(),
403            access_key_last_used: HashMap::new(),
404            service_specific_credentials: HashMap::new(),
405            delegation_requests: HashMap::new(),
406            extra_tags: HashMap::new(),
407            organizations_root_credentials_management: false,
408            organizations_root_sessions: false,
409            outbound_web_identity_federation: None,
410            service_last_accessed_jobs: HashMap::new(),
411            organizations_access_reports: HashMap::new(),
412        }
413    }
414
415    pub fn reset(&mut self) {
416        let account_id = self.account_id.clone();
417        *self = Self::new(&account_id);
418    }
419
420    /// Look up the secret access key, session token, and resolved principal
421    /// for a given access key ID.
422    ///
423    /// Searches IAM user access keys first, then STS temporary credentials.
424    /// Expired STS temporary credentials are purged in-place and skipped.
425    ///
426    /// Returns `None` if the AKID is unknown or its STS credential has
427    /// expired.
428    ///
429    /// Required for SigV4 signature verification (batch 3) and principal
430    /// resolution (batch 4). Callers must hold a write lock on
431    /// [`IamState`] to allow the lazy purge; read-only callers should use
432    /// [`IamState::credential_secret_readonly`].
433    pub fn credential_secret(&mut self, access_key_id: &str) -> Option<SecretLookup> {
434        // IAM user access keys: look up by scanning (same pattern the
435        // existing GetCallerIdentity path uses).
436        for keys in self.access_keys.values() {
437            for key in keys {
438                if key.access_key_id == access_key_id {
439                    if let Some(user) = self.users.get(&key.user_name) {
440                        return Some(SecretLookup {
441                            secret_access_key: key.secret_access_key.clone(),
442                            session_token: None,
443                            principal_arn: user.arn.clone(),
444                            user_id: user.user_id.clone(),
445                            account_id: self.account_id.clone(),
446                            session_policies: Vec::new(),
447                            principal_tags: tags_to_hashmap(&user.tags),
448                        });
449                    }
450                }
451            }
452        }
453
454        // STS temporary credentials: direct hash lookup, with lazy expiry
455        // purging so expired entries don't accumulate.
456        let now = Utc::now();
457        if let Some(temp) = self.sts_temp_credentials.get(access_key_id) {
458            if temp.expiration > now {
459                let principal_tags = self.resolve_role_tags(&temp.principal_arn);
460                return Some(SecretLookup {
461                    secret_access_key: temp.secret_access_key.clone(),
462                    session_token: Some(temp.session_token.clone()),
463                    principal_arn: temp.principal_arn.clone(),
464                    user_id: temp.user_id.clone(),
465                    account_id: temp.account_id.clone(),
466                    session_policies: temp.session_policies.clone(),
467                    principal_tags,
468                });
469            }
470            self.sts_temp_credentials.remove(access_key_id);
471        }
472        None
473    }
474
475    /// Read-only variant of [`IamState::credential_secret`] that does not
476    /// purge expired entries. Prefer the mutable variant wherever possible
477    /// to keep the temp-credential table small.
478    pub fn credential_secret_readonly(&self, access_key_id: &str) -> Option<SecretLookup> {
479        for keys in self.access_keys.values() {
480            for key in keys {
481                if key.access_key_id == access_key_id {
482                    if let Some(user) = self.users.get(&key.user_name) {
483                        return Some(SecretLookup {
484                            secret_access_key: key.secret_access_key.clone(),
485                            session_token: None,
486                            principal_arn: user.arn.clone(),
487                            user_id: user.user_id.clone(),
488                            account_id: self.account_id.clone(),
489                            session_policies: Vec::new(),
490                            principal_tags: tags_to_hashmap(&user.tags),
491                        });
492                    }
493                }
494            }
495        }
496
497        let now = Utc::now();
498        let temp = self.sts_temp_credentials.get(access_key_id)?;
499        if temp.expiration <= now {
500            return None;
501        }
502        let principal_tags = self.resolve_role_tags(&temp.principal_arn);
503        Some(SecretLookup {
504            secret_access_key: temp.secret_access_key.clone(),
505            session_token: Some(temp.session_token.clone()),
506            principal_arn: temp.principal_arn.clone(),
507            user_id: temp.user_id.clone(),
508            account_id: temp.account_id.clone(),
509            session_policies: temp.session_policies.clone(),
510            principal_tags,
511        })
512    }
513
514    /// Resolve role tags from an assumed-role principal ARN.
515    /// ARN format: `arn:aws:sts::<account>:assumed-role/<role-name>/<session>`
516    /// Looks up the role by name and returns its tags.
517    fn resolve_role_tags(&self, principal_arn: &str) -> Option<HashMap<String, String>> {
518        // assumed-role ARNs: arn:aws:sts::<account>:assumed-role/<role>/<session>
519        let parts: Vec<&str> = principal_arn.split(':').collect();
520        if parts.len() < 6 {
521            return None;
522        }
523        let resource = parts[5];
524        if let Some(rest) = resource.strip_prefix("assumed-role/") {
525            let role_name = rest.split('/').next()?;
526            let role = self.roles.get(role_name)?;
527            return tags_to_hashmap(&role.tags);
528        }
529        None
530    }
531}
532
533impl AccountState for IamState {
534    fn new_for_account(account_id: &str, _region: &str, _endpoint: &str) -> Self {
535        Self::new(account_id)
536    }
537}
538
539pub type SharedIamState = std::sync::Arc<RwLock<MultiAccountState<IamState>>>;
540
541/// On-disk snapshot envelope for IAM state. Versioned so future schema
542/// changes fail loudly instead of silently corrupting state.
543///
544/// Schema v2 stores multi-account state. v1 snapshots are migrated on
545/// load by wrapping the single `IamState` as the default account.
546#[derive(Debug, Clone, Serialize, Deserialize)]
547pub struct IamSnapshot {
548    pub schema_version: u32,
549    /// v2+: multi-account state. Present when `schema_version >= 2`.
550    #[serde(default)]
551    pub accounts: Option<MultiAccountState<IamState>>,
552    /// v1 compat: single-account state. Present when `schema_version == 1`.
553    #[serde(default)]
554    pub state: Option<IamState>,
555}
556
557pub const IAM_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    fn iam_user(name: &str, account_id: &str) -> IamUser {
564        IamUser {
565            user_name: name.to_string(),
566            user_id: format!("AIDA{}", name.to_uppercase()),
567            arn: format!("arn:aws:iam::{}:user/{}", account_id, name),
568            path: "/".to_string(),
569            created_at: Utc::now(),
570            tags: Vec::new(),
571            permissions_boundary: None,
572        }
573    }
574
575    fn iam_key(user: &str, akid: &str, secret: &str) -> IamAccessKey {
576        IamAccessKey {
577            access_key_id: akid.to_string(),
578            secret_access_key: secret.to_string(),
579            user_name: user.to_string(),
580            status: "Active".to_string(),
581            created_at: Utc::now(),
582        }
583    }
584
585    #[test]
586    fn credential_secret_returns_iam_user_key() {
587        let mut state = IamState::new("123456789012");
588        state
589            .users
590            .insert("alice".to_string(), iam_user("alice", "123456789012"));
591        state.access_keys.insert(
592            "alice".to_string(),
593            vec![iam_key("alice", "FKIAALICE", "secret-alice")],
594        );
595        let lookup = state.credential_secret("FKIAALICE").unwrap();
596        assert_eq!(lookup.secret_access_key, "secret-alice");
597        assert_eq!(lookup.principal_arn, "arn:aws:iam::123456789012:user/alice");
598        assert_eq!(lookup.account_id, "123456789012");
599        assert_eq!(lookup.session_token, None);
600    }
601
602    #[test]
603    fn credential_secret_returns_sts_temp_credential_when_unexpired() {
604        let mut state = IamState::new("123456789012");
605        state.sts_temp_credentials.insert(
606            "FSIATEMPKEY".to_string(),
607            StsTempCredential {
608                access_key_id: "FSIATEMPKEY".to_string(),
609                secret_access_key: "temp-secret".to_string(),
610                session_token: "temp-token".to_string(),
611                principal_arn: "arn:aws:sts::123456789012:assumed-role/R/s".to_string(),
612                user_id: "AROA:session".to_string(),
613                account_id: "123456789012".to_string(),
614                expiration: Utc::now() + chrono::Duration::minutes(30),
615                session_policies: Vec::new(),
616            },
617        );
618        let lookup = state.credential_secret("FSIATEMPKEY").unwrap();
619        assert_eq!(lookup.secret_access_key, "temp-secret");
620        assert_eq!(lookup.session_token.as_deref(), Some("temp-token"));
621        assert_eq!(
622            lookup.principal_arn,
623            "arn:aws:sts::123456789012:assumed-role/R/s"
624        );
625    }
626
627    #[test]
628    fn credential_secret_purges_expired_sts_credentials() {
629        let mut state = IamState::new("123456789012");
630        state.sts_temp_credentials.insert(
631            "FSIAOLD".to_string(),
632            StsTempCredential {
633                access_key_id: "FSIAOLD".to_string(),
634                secret_access_key: "s".to_string(),
635                session_token: "t".to_string(),
636                principal_arn: "arn".to_string(),
637                user_id: "id".to_string(),
638                account_id: "123456789012".to_string(),
639                expiration: Utc::now() - chrono::Duration::seconds(1),
640                session_policies: Vec::new(),
641            },
642        );
643        assert!(state.credential_secret("FSIAOLD").is_none());
644        assert!(!state.sts_temp_credentials.contains_key("FSIAOLD"));
645    }
646
647    #[test]
648    fn credential_secret_readonly_does_not_purge() {
649        let mut state = IamState::new("123456789012");
650        state.sts_temp_credentials.insert(
651            "FSIAOLD".to_string(),
652            StsTempCredential {
653                access_key_id: "FSIAOLD".to_string(),
654                secret_access_key: "s".to_string(),
655                session_token: "t".to_string(),
656                principal_arn: "arn".to_string(),
657                user_id: "id".to_string(),
658                account_id: "123456789012".to_string(),
659                expiration: Utc::now() - chrono::Duration::seconds(1),
660                session_policies: Vec::new(),
661            },
662        );
663        assert!(state.credential_secret_readonly("FSIAOLD").is_none());
664        assert!(state.sts_temp_credentials.contains_key("FSIAOLD"));
665    }
666
667    #[test]
668    fn credential_secret_returns_none_for_unknown_akid() {
669        let mut state = IamState::new("123456789012");
670        assert!(state.credential_secret("FKIAUNKNOWN").is_none());
671    }
672}