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>, pub inline_policies: HashMap<String, String>, pub attached_policies: Vec<String>, }
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>, 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#[derive(Debug, Clone)]
187pub struct CredentialIdentity {
188 pub arn: String,
189 pub user_id: String,
190 pub account_id: String,
191}
192
193#[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#[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#[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>>, pub roles: HashMap<String, IamRole>,
251 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>,
257 pub instance_profiles: HashMap<String, IamInstanceProfile>,
258 pub login_profiles: HashMap<String, LoginProfile>,
259 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>,
264 pub account_password_policy: Option<AccountPasswordPolicy>,
265 pub virtual_mfa_devices: HashMap<String, VirtualMfaDevice>, pub service_linked_role_deletions: HashMap<String, ServiceLinkedRoleDeletion>,
267 pub credential_identities: HashMap<String, CredentialIdentity>,
269 pub sts_temp_credentials: HashMap<String, StsTempCredential>,
274 pub credential_report_generated: bool,
275 pub ssh_public_keys: HashMap<String, Vec<SshPublicKey>>, 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 pub fn credential_secret(&mut self, access_key_id: &str) -> Option<SecretLookup> {
329 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 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 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}