1use std::sync::Arc;
10
11use fakecloud_core::auth::{CredentialResolver, Principal, PrincipalType, ResolvedCredential};
12
13use crate::state::SharedIamState;
14
15#[derive(Clone)]
19pub struct IamCredentialResolver {
20 state: SharedIamState,
21}
22
23impl IamCredentialResolver {
24 pub fn new(state: SharedIamState) -> Self {
25 Self { state }
26 }
27
28 pub fn shared(state: SharedIamState) -> Arc<dyn CredentialResolver> {
29 Arc::new(Self::new(state))
30 }
31}
32
33impl CredentialResolver for IamCredentialResolver {
34 fn resolve(&self, access_key_id: &str) -> Option<ResolvedCredential> {
35 let mut states = self.state.write();
36 for (_, account_state) in states.iter_mut() {
39 if let Some(lookup) = account_state.credential_secret(access_key_id) {
40 let principal_type = PrincipalType::from_arn(&lookup.principal_arn);
41 return Some(ResolvedCredential {
42 secret_access_key: lookup.secret_access_key,
43 session_token: lookup.session_token,
44 principal: Principal {
45 arn: lookup.principal_arn,
46 user_id: lookup.user_id,
47 account_id: lookup.account_id,
48 principal_type,
49 source_identity: None,
50 tags: lookup.principal_tags.map(|m| m.into_iter().collect()),
51 },
52 session_policies: lookup.session_policies,
53 mfa_present: lookup.mfa_present,
54 token_issued_at: lookup.token_issued_at,
55 federated_provider: lookup.federated_provider,
56 });
57 }
58 }
59 None
60 }
61}
62
63fn _assert_impl<T: CredentialResolver>() {}
64const _: fn() = || {
65 _assert_impl::<IamCredentialResolver>();
66};
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use crate::state::{IamAccessKey, IamState, IamUser};
72 use chrono::Utc;
73 use fakecloud_core::multi_account::MultiAccountState;
74 use parking_lot::RwLock;
75
76 fn shared(state: IamState) -> SharedIamState {
79 let account_id = state.account_id.clone();
80 let mut mas = MultiAccountState::<IamState>::new(&account_id, "us-east-1", "");
81 *mas.get_or_create(&account_id) = state;
83 Arc::new(RwLock::new(mas))
84 }
85
86 #[test]
87 fn resolves_iam_user_secret_from_state() {
88 let mut state = IamState::new("123456789012");
89 state.users.insert(
90 "alice".to_string(),
91 IamUser {
92 user_name: "alice".into(),
93 user_id: "AIDAALICE".into(),
94 arn: "arn:aws:iam::123456789012:user/alice".into(),
95 path: "/".into(),
96 created_at: Utc::now(),
97 tags: Vec::new(),
98 permissions_boundary: None,
99 },
100 );
101 state.access_keys.insert(
102 "alice".to_string(),
103 vec![IamAccessKey {
104 access_key_id: "FKIAALICE".into(),
105 secret_access_key: "the-secret".into(),
106 user_name: "alice".into(),
107 status: "Active".into(),
108 created_at: Utc::now(),
109 }],
110 );
111 let resolver = IamCredentialResolver::new(shared(state));
112 let resolved = resolver.resolve("FKIAALICE").unwrap();
113 assert_eq!(resolved.secret_access_key, "the-secret");
114 assert_eq!(
115 resolved.principal.arn,
116 "arn:aws:iam::123456789012:user/alice"
117 );
118 assert_eq!(resolved.principal.principal_type, PrincipalType::User);
119 assert_eq!(resolved.session_token, None);
120 }
121
122 #[test]
123 fn returns_none_for_unknown_akid() {
124 let state = IamState::new("123456789012");
125 let resolver = IamCredentialResolver::new(shared(state));
126 assert!(resolver.resolve("FKIANONE").is_none());
127 }
128
129 #[test]
130 fn classifies_sts_assumed_role_principal() {
131 use crate::state::StsTempCredential;
132 let mut state = IamState::new("123456789012");
133 state.sts_temp_credentials.insert(
134 "FSIATEMP".to_string(),
135 StsTempCredential {
136 access_key_id: "FSIATEMP".into(),
137 secret_access_key: "temp-secret".into(),
138 session_token: "temp-token".into(),
139 principal_arn: "arn:aws:sts::123456789012:assumed-role/ops/session".into(),
140 user_id: "AROA:session".into(),
141 account_id: "123456789012".into(),
142 expiration: Utc::now() + chrono::Duration::minutes(30),
143 session_policies: Vec::new(),
144 mfa_present: false,
145 issued_at: Utc::now(),
146 federated_provider: None,
147 },
148 );
149 let resolver = IamCredentialResolver::new(shared(state));
150 let resolved = resolver.resolve("FSIATEMP").unwrap();
151 assert_eq!(
152 resolved.principal.principal_type,
153 PrincipalType::AssumedRole
154 );
155 assert_eq!(resolved.session_token.as_deref(), Some("temp-token"));
156 }
157
158 #[test]
159 fn resolves_across_accounts() {
160 let mas = MultiAccountState::<IamState>::new("111111111111", "us-east-1", "");
161 let shared_state: SharedIamState = Arc::new(RwLock::new(mas));
162
163 {
165 let mut states = shared_state.write();
166 let a = states.get_or_create("111111111111");
167 a.users.insert(
168 "alice".into(),
169 IamUser {
170 user_name: "alice".into(),
171 user_id: "AIDAALICE".into(),
172 arn: "arn:aws:iam::111111111111:user/alice".into(),
173 path: "/".into(),
174 created_at: Utc::now(),
175 tags: Vec::new(),
176 permissions_boundary: None,
177 },
178 );
179 a.access_keys.insert(
180 "alice".into(),
181 vec![IamAccessKey {
182 access_key_id: "FKIAALICE".into(),
183 secret_access_key: "secret-a".into(),
184 user_name: "alice".into(),
185 status: "Active".into(),
186 created_at: Utc::now(),
187 }],
188 );
189
190 let b = states.get_or_create("222222222222");
192 b.users.insert(
193 "bob".into(),
194 IamUser {
195 user_name: "bob".into(),
196 user_id: "AIDABOB".into(),
197 arn: "arn:aws:iam::222222222222:user/bob".into(),
198 path: "/".into(),
199 created_at: Utc::now(),
200 tags: Vec::new(),
201 permissions_boundary: None,
202 },
203 );
204 b.access_keys.insert(
205 "bob".into(),
206 vec![IamAccessKey {
207 access_key_id: "FKIABOB".into(),
208 secret_access_key: "secret-b".into(),
209 user_name: "bob".into(),
210 status: "Active".into(),
211 created_at: Utc::now(),
212 }],
213 );
214 }
215
216 let resolver = IamCredentialResolver::new(shared_state);
217
218 let a = resolver.resolve("FKIAALICE").unwrap();
220 assert_eq!(a.principal.account_id, "111111111111");
221
222 let b = resolver.resolve("FKIABOB").unwrap();
224 assert_eq!(b.principal.account_id, "222222222222");
225
226 assert!(resolver.resolve("FKIANONE").is_none());
228 }
229
230 #[test]
231 fn resolves_iam_user_tags_for_principal() {
232 use crate::state::Tag;
233 let mut state = IamState::new("123456789012");
234 state.users.insert(
235 "bob".to_string(),
236 IamUser {
237 user_name: "bob".into(),
238 user_id: "AIDABOB".into(),
239 arn: "arn:aws:iam::123456789012:user/bob".into(),
240 path: "/".into(),
241 created_at: Utc::now(),
242 tags: vec![
243 Tag {
244 key: "Team".into(),
245 value: "platform".into(),
246 },
247 Tag {
248 key: "Environment".into(),
249 value: "prod".into(),
250 },
251 ],
252 permissions_boundary: None,
253 },
254 );
255 state.access_keys.insert(
256 "bob".to_string(),
257 vec![IamAccessKey {
258 access_key_id: "FKIABOB".into(),
259 secret_access_key: "bob-secret".into(),
260 user_name: "bob".into(),
261 status: "Active".into(),
262 created_at: Utc::now(),
263 }],
264 );
265 let resolver = IamCredentialResolver::new(shared(state));
266 let resolved = resolver.resolve("FKIABOB").unwrap();
267 let tags = resolved.principal.tags.as_ref().unwrap();
268 assert_eq!(tags.get("Team").map(|s| s.as_str()), Some("platform"));
269 assert_eq!(tags.get("Environment").map(|s| s.as_str()), Some("prod"));
270 }
271
272 #[test]
273 fn resolves_assumed_role_tags_for_principal() {
274 use crate::state::{IamRole, StsTempCredential, Tag};
275 let mut state = IamState::new("123456789012");
276 state.roles.insert(
277 "ops".to_string(),
278 IamRole {
279 role_name: "ops".into(),
280 role_id: "AROAOPS".into(),
281 arn: "arn:aws:iam::123456789012:role/ops".into(),
282 path: "/".into(),
283 assume_role_policy_document: "{}".into(),
284 created_at: Utc::now(),
285 tags: vec![Tag {
286 key: "Department".into(),
287 value: "engineering".into(),
288 }],
289 max_session_duration: 3600,
290 permissions_boundary: None,
291 description: None,
292 },
293 );
294 state.sts_temp_credentials.insert(
295 "FSIAOPS".to_string(),
296 StsTempCredential {
297 access_key_id: "FSIAOPS".into(),
298 secret_access_key: "ops-secret".into(),
299 session_token: "ops-token".into(),
300 principal_arn: "arn:aws:sts::123456789012:assumed-role/ops/session".into(),
301 user_id: "AROAOPS:session".into(),
302 account_id: "123456789012".into(),
303 expiration: Utc::now() + chrono::Duration::minutes(30),
304 session_policies: Vec::new(),
305 mfa_present: false,
306 issued_at: Utc::now(),
307 federated_provider: None,
308 },
309 );
310 let resolver = IamCredentialResolver::new(shared(state));
311 let resolved = resolver.resolve("FSIAOPS").unwrap();
312 let tags = resolved.principal.tags.as_ref().unwrap();
313 assert_eq!(
314 tags.get("Department").map(|s| s.as_str()),
315 Some("engineering")
316 );
317 }
318
319 #[test]
320 fn no_tags_yields_none() {
321 let mut state = IamState::new("123456789012");
322 state.users.insert(
323 "empty".to_string(),
324 IamUser {
325 user_name: "empty".into(),
326 user_id: "AIDAEMPTY".into(),
327 arn: "arn:aws:iam::123456789012:user/empty".into(),
328 path: "/".into(),
329 created_at: Utc::now(),
330 tags: Vec::new(),
331 permissions_boundary: None,
332 },
333 );
334 state.access_keys.insert(
335 "empty".to_string(),
336 vec![IamAccessKey {
337 access_key_id: "FKIAEMPTY".into(),
338 secret_access_key: "s".into(),
339 user_name: "empty".into(),
340 status: "Active".into(),
341 created_at: Utc::now(),
342 }],
343 );
344 let resolver = IamCredentialResolver::new(shared(state));
345 let resolved = resolver.resolve("FKIAEMPTY").unwrap();
346 assert!(resolved.principal.tags.is_none());
347 }
348}