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>, pub inline_policies: HashMap<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}
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#[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#[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 #[serde(default)]
220 pub session_policies: Vec<String>,
221}
222
223#[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 pub session_policies: Vec<String>,
239 pub principal_tags: Option<HashMap<String, String>>,
242}
243
244pub 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#[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>>, pub roles: HashMap<String, IamRole>,
281 pub policies: HashMap<String, IamPolicy>, pub role_policies: HashMap<String, Vec<String>>, pub role_inline_policies: HashMap<String, HashMap<String, String>>, pub user_policies: HashMap<String, Vec<String>>, pub user_inline_policies: HashMap<String, HashMap<String, String>>, 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>, pub oidc_providers: HashMap<String, OidcProvider>, pub server_certificates: HashMap<String, ServerCertificate>, pub signing_certificates: HashMap<String, Vec<SigningCertificate>>, pub account_aliases: Vec<String>,
294 pub account_password_policy: Option<AccountPasswordPolicy>,
295 pub virtual_mfa_devices: HashMap<String, VirtualMfaDevice>, pub service_linked_role_deletions: HashMap<String, ServiceLinkedRoleDeletion>,
297 pub credential_identities: HashMap<String, CredentialIdentity>,
299 pub sts_temp_credentials: HashMap<String, StsTempCredential>,
304 pub credential_report_generated: bool,
305 pub ssh_public_keys: HashMap<String, Vec<SshPublicKey>>, pub access_key_last_used: HashMap<String, AccessKeyLastUsed>,
307 #[serde(default)]
309 pub service_specific_credentials: HashMap<String, Vec<ServiceSpecificCredential>>, #[serde(default)]
312 pub delegation_requests: HashMap<String, DelegationRequest>,
313 #[serde(default)]
315 pub extra_tags: HashMap<String, Vec<(String, String)>>,
316 #[serde(default)]
318 pub organizations_root_credentials_management: bool,
319 #[serde(default)]
320 pub organizations_root_sessions: bool,
321 #[serde(default)]
323 pub outbound_web_identity_federation: Option<OutboundWebIdentityFederation>,
324 #[serde(default)]
326 pub service_last_accessed_jobs: HashMap<String, ServiceLastAccessedJob>,
327 #[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 pub fn credential_secret(&mut self, access_key_id: &str) -> Option<SecretLookup> {
434 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 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 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 fn resolve_role_tags(&self, principal_arn: &str) -> Option<HashMap<String, String>> {
518 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#[derive(Debug, Clone, Serialize, Deserialize)]
547pub struct IamSnapshot {
548 pub schema_version: u32,
549 #[serde(default)]
551 pub accounts: Option<MultiAccountState<IamState>>,
552 #[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}