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