1use chrono::{DateTime, Utc};
2use parking_lot::RwLock;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
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>, pub inline_policies: BTreeMap<String, String>, pub attached_policies: Vec<String>, }
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>, 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 #[serde(default)]
105 pub password: String,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct SamlProvider {
110 pub arn: String,
111 pub name: String,
112 pub saml_metadata_document: String,
113 pub created_at: DateTime<Utc>,
114 pub valid_until: DateTime<Utc>,
115 pub tags: Vec<Tag>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct OidcProvider {
120 pub arn: String,
121 pub url: String,
122 pub client_id_list: Vec<String>,
123 pub thumbprint_list: Vec<String>,
124 pub created_at: DateTime<Utc>,
125 pub tags: Vec<Tag>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct ServerCertificate {
130 pub server_certificate_name: String,
131 pub server_certificate_id: String,
132 pub arn: String,
133 pub path: String,
134 pub certificate_body: String,
135 pub certificate_chain: Option<String>,
136 pub upload_date: DateTime<Utc>,
137 pub expiration: DateTime<Utc>,
138 pub tags: Vec<Tag>,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct SigningCertificate {
143 pub certificate_id: String,
144 pub user_name: String,
145 pub certificate_body: String,
146 pub status: String,
147 pub upload_date: DateTime<Utc>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct AccountPasswordPolicy {
152 pub minimum_password_length: u32,
153 pub require_symbols: bool,
154 pub require_numbers: bool,
155 pub require_uppercase_characters: bool,
156 pub require_lowercase_characters: bool,
157 pub allow_users_to_change_password: bool,
158 pub max_password_age: u32,
159 pub password_reuse_prevention: u32,
160 pub hard_expiry: bool,
161}
162
163impl Default for AccountPasswordPolicy {
164 fn default() -> Self {
165 Self {
166 minimum_password_length: 6,
167 require_symbols: false,
168 require_numbers: false,
169 require_uppercase_characters: false,
170 require_lowercase_characters: false,
171 allow_users_to_change_password: false,
172 max_password_age: 0,
173 password_reuse_prevention: 0,
174 hard_expiry: false,
175 }
176 }
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct VirtualMfaDevice {
181 pub serial_number: String,
182 pub base32_string_seed: String,
183 pub qr_code_png: String,
184 pub enable_date: Option<DateTime<Utc>>,
185 pub user: Option<String>,
186 pub tags: Vec<Tag>,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct ServiceLinkedRoleDeletion {
191 pub deletion_task_id: String,
192 pub status: String,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct CredentialIdentity {
198 pub arn: String,
199 pub user_id: String,
200 pub account_id: String,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct StsTempCredential {
214 pub access_key_id: String,
215 pub secret_access_key: String,
216 pub session_token: String,
217 pub principal_arn: String,
218 pub user_id: String,
219 pub account_id: String,
220 pub expiration: DateTime<Utc>,
221 #[serde(default)]
227 pub session_policies: Vec<String>,
228 #[serde(default)]
232 pub mfa_present: bool,
233 #[serde(default = "default_issued_at")]
238 pub issued_at: DateTime<Utc>,
239 #[serde(default)]
245 pub federated_provider: Option<String>,
246}
247
248fn default_issued_at() -> DateTime<Utc> {
251 Utc::now()
252}
253
254#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct SecretLookup {
262 pub secret_access_key: String,
263 pub session_token: Option<String>,
264 pub principal_arn: String,
265 pub user_id: String,
266 pub account_id: String,
267 pub session_policies: Vec<String>,
270 pub principal_tags: Option<BTreeMap<String, String>>,
273 pub mfa_present: bool,
277 pub token_issued_at: Option<DateTime<Utc>>,
283 pub federated_provider: Option<String>,
288}
289
290pub fn tags_to_hashmap(tags: &[Tag]) -> Option<BTreeMap<String, String>> {
293 if tags.is_empty() {
294 return None;
295 }
296 Some(
297 tags.iter()
298 .map(|t| (t.key.clone(), t.value.clone()))
299 .collect(),
300 )
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct SshPublicKey {
305 pub ssh_public_key_id: String,
306 pub user_name: String,
307 pub ssh_public_key_body: String,
308 pub status: String,
309 pub upload_date: DateTime<Utc>,
310 pub fingerprint: String,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct AccessKeyLastUsed {
316 pub last_used_date: DateTime<Utc>,
317 pub service_name: String,
318 pub region: String,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct IamState {
323 pub account_id: String,
324 pub users: BTreeMap<String, IamUser>,
325 pub access_keys: BTreeMap<String, Vec<IamAccessKey>>, pub roles: BTreeMap<String, IamRole>,
327 pub policies: BTreeMap<String, IamPolicy>, pub role_policies: BTreeMap<String, Vec<String>>, pub role_inline_policies: BTreeMap<String, BTreeMap<String, String>>, pub user_policies: BTreeMap<String, Vec<String>>, pub user_inline_policies: BTreeMap<String, BTreeMap<String, String>>, pub groups: BTreeMap<String, IamGroup>,
333 pub instance_profiles: BTreeMap<String, IamInstanceProfile>,
334 pub login_profiles: BTreeMap<String, LoginProfile>,
335 pub saml_providers: BTreeMap<String, SamlProvider>, pub oidc_providers: BTreeMap<String, OidcProvider>, pub server_certificates: BTreeMap<String, ServerCertificate>, pub signing_certificates: BTreeMap<String, Vec<SigningCertificate>>, pub account_aliases: Vec<String>,
340 pub account_password_policy: Option<AccountPasswordPolicy>,
341 pub virtual_mfa_devices: BTreeMap<String, VirtualMfaDevice>, pub service_linked_role_deletions: BTreeMap<String, ServiceLinkedRoleDeletion>,
343 pub credential_identities: BTreeMap<String, CredentialIdentity>,
345 pub sts_temp_credentials: BTreeMap<String, StsTempCredential>,
350 pub credential_report_generated: bool,
351 pub ssh_public_keys: BTreeMap<String, Vec<SshPublicKey>>, pub access_key_last_used: BTreeMap<String, AccessKeyLastUsed>,
353 #[serde(default)]
355 pub service_specific_credentials: BTreeMap<String, Vec<ServiceSpecificCredential>>, #[serde(default)]
358 pub extra_tags: BTreeMap<String, Vec<(String, String)>>,
359 #[serde(default)]
361 pub organizations_root_credentials_management: bool,
362 #[serde(default)]
363 pub organizations_root_sessions: bool,
364 #[serde(default)]
366 pub service_last_accessed_jobs: BTreeMap<String, ServiceLastAccessedJob>,
367 #[serde(default)]
369 pub organizations_access_reports: BTreeMap<String, OrganizationsAccessReport>,
370 #[serde(default)]
373 pub global_endpoint_token_version: Option<String>,
374 #[serde(default)]
379 pub delegation_requests: BTreeMap<String, DelegationRequest>,
380 #[serde(default)]
384 pub outbound_web_identity_federation_enabled: bool,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct DelegationRequest {
389 pub id: String,
390 pub owner_account_id: Option<String>,
391 pub description: String,
392 pub request_message: Option<String>,
393 pub requestor_workflow_id: String,
394 pub redirect_url: Option<String>,
395 pub notification_channel: String,
396 pub session_duration: i64,
397 pub only_send_by_owner: bool,
398 pub status: String,
399 pub notes: Option<String>,
400 pub created_at: DateTime<Utc>,
401 pub policy_template_arn: Option<String>,
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct ServiceSpecificCredential {
406 pub credential_id: String,
407 pub user_name: String,
408 pub service_name: String,
409 pub service_user_name: String,
410 pub service_password: String,
411 pub status: String,
412 pub create_date: DateTime<Utc>,
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct ServiceLastAccessedJob {
417 pub job_id: String,
418 pub status: String,
419 pub job_creation_date: DateTime<Utc>,
420 pub arn: String,
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct OrganizationsAccessReport {
425 pub job_id: String,
426 pub status: String,
427 pub created_at: DateTime<Utc>,
428 pub entity_path: String,
429}
430
431impl IamState {
432 pub fn new(account_id: &str) -> Self {
433 let mut state = Self {
434 account_id: account_id.to_string(),
435 users: BTreeMap::new(),
436 access_keys: BTreeMap::new(),
437 roles: BTreeMap::new(),
438 policies: BTreeMap::new(),
439 role_policies: BTreeMap::new(),
440 role_inline_policies: BTreeMap::new(),
441 user_policies: BTreeMap::new(),
442 user_inline_policies: BTreeMap::new(),
443 groups: BTreeMap::new(),
444 instance_profiles: BTreeMap::new(),
445 login_profiles: BTreeMap::new(),
446 saml_providers: BTreeMap::new(),
447 oidc_providers: BTreeMap::new(),
448 server_certificates: BTreeMap::new(),
449 signing_certificates: BTreeMap::new(),
450 account_aliases: Vec::new(),
451 account_password_policy: None,
452 virtual_mfa_devices: BTreeMap::new(),
453 service_linked_role_deletions: BTreeMap::new(),
454 credential_identities: BTreeMap::new(),
455 sts_temp_credentials: BTreeMap::new(),
456 credential_report_generated: false,
457 ssh_public_keys: BTreeMap::new(),
458 access_key_last_used: BTreeMap::new(),
459 service_specific_credentials: BTreeMap::new(),
460 extra_tags: BTreeMap::new(),
461 organizations_root_credentials_management: false,
462 organizations_root_sessions: false,
463 service_last_accessed_jobs: BTreeMap::new(),
464 organizations_access_reports: BTreeMap::new(),
465 global_endpoint_token_version: None,
466 delegation_requests: BTreeMap::new(),
467 outbound_web_identity_federation_enabled: false,
468 };
469 state.seed_default_service_linked_roles();
470 state
471 }
472
473 fn seed_default_service_linked_roles(&mut self) {
477 let now = Utc::now();
478 for (service, name) in [
479 ("support.amazonaws.com", "AWSServiceRoleForSupport"),
480 (
481 "trustedadvisor.amazonaws.com",
482 "AWSServiceRoleForTrustedAdvisor",
483 ),
484 ] {
485 let path = format!("/aws-service-role/{service}/");
486 let arn = format!("arn:aws:iam::{}:role{}{}", self.account_id, path, name);
487 self.roles.insert(
488 name.to_string(),
489 IamRole {
490 role_name: name.to_string(),
491 role_id: format!("AROA{:0>17}", name.len()),
492 arn,
493 path,
494 assume_role_policy_document: format!(
495 "{{\"Version\":\"2012-10-17\",\"Statement\":[{{\"Effect\":\"Allow\",\"Principal\":{{\"Service\":\"{service}\"}},\"Action\":\"sts:AssumeRole\"}}]}}"
496 ),
497 created_at: now,
498 description: None,
499 max_session_duration: 3600,
500 tags: Vec::new(),
501 permissions_boundary: None,
502 },
503 );
504 }
505 }
506
507 pub fn reset(&mut self) {
508 let account_id = self.account_id.clone();
509 *self = Self::new(&account_id);
510 }
511
512 pub fn credential_secret(&mut self, access_key_id: &str) -> Option<SecretLookup> {
526 for keys in self.access_keys.values() {
529 for key in keys {
530 if key.access_key_id == access_key_id && key.status == "Active" {
534 if let Some(user) = self.users.get(&key.user_name) {
535 return Some(SecretLookup {
536 secret_access_key: key.secret_access_key.clone(),
537 session_token: None,
538 principal_arn: user.arn.clone(),
539 user_id: user.user_id.clone(),
540 account_id: self.account_id.clone(),
541 session_policies: Vec::new(),
542 principal_tags: tags_to_hashmap(&user.tags),
543 mfa_present: false,
544 token_issued_at: None,
545 federated_provider: None,
546 });
547 }
548 }
549 }
550 }
551
552 let now = Utc::now();
555 if let Some(temp) = self.sts_temp_credentials.get(access_key_id) {
556 if temp.expiration > now {
557 let principal_tags = self.resolve_role_tags(&temp.principal_arn);
558 return Some(SecretLookup {
559 secret_access_key: temp.secret_access_key.clone(),
560 session_token: Some(temp.session_token.clone()),
561 principal_arn: temp.principal_arn.clone(),
562 user_id: temp.user_id.clone(),
563 account_id: temp.account_id.clone(),
564 session_policies: temp.session_policies.clone(),
565 principal_tags,
566 mfa_present: temp.mfa_present,
567 token_issued_at: Some(temp.issued_at),
568 federated_provider: temp.federated_provider.clone(),
569 });
570 }
571 self.sts_temp_credentials.remove(access_key_id);
572 }
573 None
574 }
575
576 pub fn credential_secret_readonly(&self, access_key_id: &str) -> Option<SecretLookup> {
580 for keys in self.access_keys.values() {
581 for key in keys {
582 if key.access_key_id == access_key_id && key.status == "Active" {
586 if let Some(user) = self.users.get(&key.user_name) {
587 return Some(SecretLookup {
588 secret_access_key: key.secret_access_key.clone(),
589 session_token: None,
590 principal_arn: user.arn.clone(),
591 user_id: user.user_id.clone(),
592 account_id: self.account_id.clone(),
593 session_policies: Vec::new(),
594 principal_tags: tags_to_hashmap(&user.tags),
595 mfa_present: false,
596 token_issued_at: None,
597 federated_provider: None,
598 });
599 }
600 }
601 }
602 }
603
604 let now = Utc::now();
605 let temp = self.sts_temp_credentials.get(access_key_id)?;
606 if temp.expiration <= now {
607 return None;
608 }
609 let principal_tags = self.resolve_role_tags(&temp.principal_arn);
610 Some(SecretLookup {
611 secret_access_key: temp.secret_access_key.clone(),
612 session_token: Some(temp.session_token.clone()),
613 principal_arn: temp.principal_arn.clone(),
614 user_id: temp.user_id.clone(),
615 account_id: temp.account_id.clone(),
616 session_policies: temp.session_policies.clone(),
617 principal_tags,
618 mfa_present: temp.mfa_present,
619 token_issued_at: Some(temp.issued_at),
620 federated_provider: temp.federated_provider.clone(),
621 })
622 }
623
624 fn resolve_role_tags(&self, principal_arn: &str) -> Option<BTreeMap<String, String>> {
628 let parts: Vec<&str> = principal_arn.split(':').collect();
630 if parts.len() < 6 {
631 return None;
632 }
633 let resource = parts[5];
634 if let Some(rest) = resource.strip_prefix("assumed-role/") {
635 let role_name = rest.split('/').next()?;
636 let role = self.roles.get(role_name)?;
637 return tags_to_hashmap(&role.tags);
638 }
639 None
640 }
641}
642
643impl AccountState for IamState {
644 fn new_for_account(account_id: &str, _region: &str, _endpoint: &str) -> Self {
645 Self::new(account_id)
646 }
647}
648
649pub type SharedIamState = std::sync::Arc<RwLock<MultiAccountState<IamState>>>;
650
651#[derive(Debug, Clone, Serialize, Deserialize)]
657pub struct IamSnapshot {
658 pub schema_version: u32,
659 #[serde(default)]
661 pub accounts: Option<MultiAccountState<IamState>>,
662 #[serde(default)]
664 pub state: Option<IamState>,
665}
666
667pub const IAM_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672 use fakecloud_aws::arn::Arn;
673
674 fn iam_user(name: &str, account_id: &str) -> IamUser {
675 IamUser {
676 user_name: name.to_string(),
677 user_id: format!("AIDA{}", name.to_uppercase()),
678 arn: Arn::global("iam", account_id, &format!("user/{name}")).to_string(),
679 path: "/".to_string(),
680 created_at: Utc::now(),
681 tags: Vec::new(),
682 permissions_boundary: None,
683 }
684 }
685
686 fn iam_key(user: &str, akid: &str, secret: &str) -> IamAccessKey {
687 IamAccessKey {
688 access_key_id: akid.to_string(),
689 secret_access_key: secret.to_string(),
690 user_name: user.to_string(),
691 status: "Active".to_string(),
692 created_at: Utc::now(),
693 }
694 }
695
696 #[test]
697 fn credential_secret_returns_iam_user_key() {
698 let mut state = IamState::new("123456789012");
699 state
700 .users
701 .insert("alice".to_string(), iam_user("alice", "123456789012"));
702 state.access_keys.insert(
703 "alice".to_string(),
704 vec![iam_key("alice", "FKIAALICE", "secret-alice")],
705 );
706 let lookup = state.credential_secret("FKIAALICE").unwrap();
707 assert_eq!(lookup.secret_access_key, "secret-alice");
708 assert_eq!(lookup.principal_arn, "arn:aws:iam::123456789012:user/alice");
709 assert_eq!(lookup.account_id, "123456789012");
710 assert_eq!(lookup.session_token, None);
711 }
712
713 #[test]
714 fn credential_secret_returns_sts_temp_credential_when_unexpired() {
715 let mut state = IamState::new("123456789012");
716 state.sts_temp_credentials.insert(
717 "FSIATEMPKEY".to_string(),
718 StsTempCredential {
719 access_key_id: "FSIATEMPKEY".to_string(),
720 secret_access_key: "temp-secret".to_string(),
721 session_token: "temp-token".to_string(),
722 principal_arn: "arn:aws:sts::123456789012:assumed-role/R/s".to_string(),
723 user_id: "AROA:session".to_string(),
724 account_id: "123456789012".to_string(),
725 expiration: Utc::now() + chrono::Duration::minutes(30),
726 session_policies: Vec::new(),
727 mfa_present: false,
728 issued_at: Utc::now(),
729 federated_provider: None,
730 },
731 );
732 let lookup = state.credential_secret("FSIATEMPKEY").unwrap();
733 assert_eq!(lookup.secret_access_key, "temp-secret");
734 assert_eq!(lookup.session_token.as_deref(), Some("temp-token"));
735 assert_eq!(
736 lookup.principal_arn,
737 "arn:aws:sts::123456789012:assumed-role/R/s"
738 );
739 }
740
741 #[test]
742 fn credential_secret_purges_expired_sts_credentials() {
743 let mut state = IamState::new("123456789012");
744 state.sts_temp_credentials.insert(
745 "FSIAOLD".to_string(),
746 StsTempCredential {
747 access_key_id: "FSIAOLD".to_string(),
748 secret_access_key: "s".to_string(),
749 session_token: "t".to_string(),
750 principal_arn: "arn".to_string(),
751 user_id: "id".to_string(),
752 account_id: "123456789012".to_string(),
753 expiration: Utc::now() - chrono::Duration::seconds(1),
754 session_policies: Vec::new(),
755 mfa_present: false,
756 issued_at: Utc::now(),
757 federated_provider: None,
758 },
759 );
760 assert!(state.credential_secret("FSIAOLD").is_none());
761 assert!(!state.sts_temp_credentials.contains_key("FSIAOLD"));
762 }
763
764 #[test]
765 fn credential_secret_readonly_does_not_purge() {
766 let mut state = IamState::new("123456789012");
767 state.sts_temp_credentials.insert(
768 "FSIAOLD".to_string(),
769 StsTempCredential {
770 access_key_id: "FSIAOLD".to_string(),
771 secret_access_key: "s".to_string(),
772 session_token: "t".to_string(),
773 principal_arn: "arn".to_string(),
774 user_id: "id".to_string(),
775 account_id: "123456789012".to_string(),
776 expiration: Utc::now() - chrono::Duration::seconds(1),
777 session_policies: Vec::new(),
778 mfa_present: false,
779 issued_at: Utc::now(),
780 federated_provider: None,
781 },
782 );
783 assert!(state.credential_secret_readonly("FSIAOLD").is_none());
784 assert!(state.sts_temp_credentials.contains_key("FSIAOLD"));
785 }
786
787 #[test]
788 fn credential_secret_returns_none_for_unknown_akid() {
789 let mut state = IamState::new("123456789012");
790 assert!(state.credential_secret("FKIAUNKNOWN").is_none());
791 }
792
793 #[test]
796 fn credential_secret_skips_inactive_key() {
797 let mut state = IamState::new("123456789012");
798 let mut key = iam_key("alice", "FKIAALICE", "secret123");
799 key.status = "Inactive".to_string();
800 state.access_keys.insert("alice".to_string(), vec![key]);
801 assert!(state.credential_secret("FKIAALICE").is_none());
802 }
803}