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,
51 },
52 session_policies: lookup.session_policies,
53 });
54 }
55 }
56 None
57 }
58}
59
60#[allow(dead_code)]
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 },
145 );
146 let resolver = IamCredentialResolver::new(shared(state));
147 let resolved = resolver.resolve("FSIATEMP").unwrap();
148 assert_eq!(
149 resolved.principal.principal_type,
150 PrincipalType::AssumedRole
151 );
152 assert_eq!(resolved.session_token.as_deref(), Some("temp-token"));
153 }
154
155 #[test]
156 fn resolves_across_accounts() {
157 let mas = MultiAccountState::<IamState>::new("111111111111", "us-east-1", "");
158 let shared_state: SharedIamState = Arc::new(RwLock::new(mas));
159
160 {
162 let mut states = shared_state.write();
163 let a = states.get_or_create("111111111111");
164 a.users.insert(
165 "alice".into(),
166 IamUser {
167 user_name: "alice".into(),
168 user_id: "AIDAALICE".into(),
169 arn: "arn:aws:iam::111111111111:user/alice".into(),
170 path: "/".into(),
171 created_at: Utc::now(),
172 tags: Vec::new(),
173 permissions_boundary: None,
174 },
175 );
176 a.access_keys.insert(
177 "alice".into(),
178 vec![IamAccessKey {
179 access_key_id: "FKIAALICE".into(),
180 secret_access_key: "secret-a".into(),
181 user_name: "alice".into(),
182 status: "Active".into(),
183 created_at: Utc::now(),
184 }],
185 );
186
187 let b = states.get_or_create("222222222222");
189 b.users.insert(
190 "bob".into(),
191 IamUser {
192 user_name: "bob".into(),
193 user_id: "AIDABOB".into(),
194 arn: "arn:aws:iam::222222222222:user/bob".into(),
195 path: "/".into(),
196 created_at: Utc::now(),
197 tags: Vec::new(),
198 permissions_boundary: None,
199 },
200 );
201 b.access_keys.insert(
202 "bob".into(),
203 vec![IamAccessKey {
204 access_key_id: "FKIABOB".into(),
205 secret_access_key: "secret-b".into(),
206 user_name: "bob".into(),
207 status: "Active".into(),
208 created_at: Utc::now(),
209 }],
210 );
211 }
212
213 let resolver = IamCredentialResolver::new(shared_state);
214
215 let a = resolver.resolve("FKIAALICE").unwrap();
217 assert_eq!(a.principal.account_id, "111111111111");
218
219 let b = resolver.resolve("FKIABOB").unwrap();
221 assert_eq!(b.principal.account_id, "222222222222");
222
223 assert!(resolver.resolve("FKIANONE").is_none());
225 }
226
227 #[test]
228 fn resolves_iam_user_tags_for_principal() {
229 use crate::state::Tag;
230 let mut state = IamState::new("123456789012");
231 state.users.insert(
232 "bob".to_string(),
233 IamUser {
234 user_name: "bob".into(),
235 user_id: "AIDABOB".into(),
236 arn: "arn:aws:iam::123456789012:user/bob".into(),
237 path: "/".into(),
238 created_at: Utc::now(),
239 tags: vec![
240 Tag {
241 key: "Team".into(),
242 value: "platform".into(),
243 },
244 Tag {
245 key: "Environment".into(),
246 value: "prod".into(),
247 },
248 ],
249 permissions_boundary: None,
250 },
251 );
252 state.access_keys.insert(
253 "bob".to_string(),
254 vec![IamAccessKey {
255 access_key_id: "FKIABOB".into(),
256 secret_access_key: "bob-secret".into(),
257 user_name: "bob".into(),
258 status: "Active".into(),
259 created_at: Utc::now(),
260 }],
261 );
262 let resolver = IamCredentialResolver::new(shared(state));
263 let resolved = resolver.resolve("FKIABOB").unwrap();
264 let tags = resolved.principal.tags.as_ref().unwrap();
265 assert_eq!(tags.get("Team").map(|s| s.as_str()), Some("platform"));
266 assert_eq!(tags.get("Environment").map(|s| s.as_str()), Some("prod"));
267 }
268
269 #[test]
270 fn resolves_assumed_role_tags_for_principal() {
271 use crate::state::{IamRole, StsTempCredential, Tag};
272 let mut state = IamState::new("123456789012");
273 state.roles.insert(
274 "ops".to_string(),
275 IamRole {
276 role_name: "ops".into(),
277 role_id: "AROAOPS".into(),
278 arn: "arn:aws:iam::123456789012:role/ops".into(),
279 path: "/".into(),
280 assume_role_policy_document: "{}".into(),
281 created_at: Utc::now(),
282 tags: vec![Tag {
283 key: "Department".into(),
284 value: "engineering".into(),
285 }],
286 max_session_duration: 3600,
287 permissions_boundary: None,
288 description: None,
289 },
290 );
291 state.sts_temp_credentials.insert(
292 "FSIAOPS".to_string(),
293 StsTempCredential {
294 access_key_id: "FSIAOPS".into(),
295 secret_access_key: "ops-secret".into(),
296 session_token: "ops-token".into(),
297 principal_arn: "arn:aws:sts::123456789012:assumed-role/ops/session".into(),
298 user_id: "AROAOPS:session".into(),
299 account_id: "123456789012".into(),
300 expiration: Utc::now() + chrono::Duration::minutes(30),
301 session_policies: Vec::new(),
302 },
303 );
304 let resolver = IamCredentialResolver::new(shared(state));
305 let resolved = resolver.resolve("FSIAOPS").unwrap();
306 let tags = resolved.principal.tags.as_ref().unwrap();
307 assert_eq!(
308 tags.get("Department").map(|s| s.as_str()),
309 Some("engineering")
310 );
311 }
312
313 #[test]
314 fn no_tags_yields_none() {
315 let mut state = IamState::new("123456789012");
316 state.users.insert(
317 "empty".to_string(),
318 IamUser {
319 user_name: "empty".into(),
320 user_id: "AIDAEMPTY".into(),
321 arn: "arn:aws:iam::123456789012:user/empty".into(),
322 path: "/".into(),
323 created_at: Utc::now(),
324 tags: Vec::new(),
325 permissions_boundary: None,
326 },
327 );
328 state.access_keys.insert(
329 "empty".to_string(),
330 vec![IamAccessKey {
331 access_key_id: "FKIAEMPTY".into(),
332 secret_access_key: "s".into(),
333 user_name: "empty".into(),
334 status: "Active".into(),
335 created_at: Utc::now(),
336 }],
337 );
338 let resolver = IamCredentialResolver::new(shared(state));
339 let resolved = resolver.resolve("FKIAEMPTY").unwrap();
340 assert!(resolved.principal.tags.is_none());
341 }
342}