1use aws_smithy_types::config_bag::Layer;
7use aws_smithy_types::date_time::Format;
8use aws_smithy_types::type_erasure::TypeErasedBox;
9use std::any::{Any, TypeId};
10use std::collections::HashMap;
11use std::fmt;
12use std::fmt::{Debug, Formatter};
13use std::sync::Arc;
14use std::time::{SystemTime, UNIX_EPOCH};
15use zeroize::Zeroizing;
16
17use aws_smithy_runtime_api::client::identity::Identity;
18
19use crate::attributes::AccountId;
20use crate::credential_feature::AwsCredentialFeature;
21
22#[doc(hidden)]
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub struct StaticStabilityEligible;
34
35pub struct Credentials(Arc<Inner>, HashMap<TypeId, TypeErasedBox>);
43
44impl Clone for Credentials {
45 fn clone(&self) -> Self {
46 let mut new_map = HashMap::with_capacity(self.1.len());
47 for (k, v) in &self.1 {
48 new_map.insert(
49 *k,
50 v.try_clone()
51 .expect("values are guaranteed to implement `Clone` via `set_property`"),
52 );
53 }
54 Self(self.0.clone(), new_map)
55 }
56}
57
58impl PartialEq for Credentials {
59 #[inline] fn eq(&self, other: &Credentials) -> bool {
61 self.0 == other.0
62 }
63}
64
65impl Eq for Credentials {}
66
67#[derive(Clone, Eq, PartialEq)]
68struct Inner {
69 access_key_id: Zeroizing<String>,
70 secret_access_key: Zeroizing<String>,
71 session_token: Zeroizing<Option<String>>,
72
73 expires_after: Option<SystemTime>,
81
82 account_id: Option<AccountId>,
85
86 provider_name: &'static str,
87}
88
89impl Debug for Credentials {
90 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
91 let mut creds = f.debug_struct("Credentials");
92 creds
93 .field("provider_name", &self.0.provider_name)
94 .field("access_key_id", &self.0.access_key_id.as_str())
95 .field("secret_access_key", &"** redacted **");
96 if let Some(expiry) = self.expiry() {
97 if let Some(formatted) = expiry.duration_since(UNIX_EPOCH).ok().and_then(|dur| {
98 aws_smithy_types::DateTime::from_secs(dur.as_secs() as _)
99 .fmt(Format::DateTime)
100 .ok()
101 }) {
102 creds.field("expires_after", &formatted);
103 } else {
104 creds.field("expires_after", &expiry);
105 }
106 } else {
107 creds.field("expires_after", &"never");
108 }
109 if let Some(account_id) = &self.0.account_id {
110 creds.field("account_id", &account_id.as_str());
111 }
112 for (i, prop) in self.1.values().enumerate() {
113 creds.field(&format!("property_{i}"), prop);
114 }
115 creds.finish()
116 }
117}
118
119#[cfg(feature = "hardcoded-credentials")]
120const STATIC_CREDENTIALS: &str = "Static";
121
122impl Credentials {
123 pub fn builder() -> CredentialsBuilder {
125 CredentialsBuilder::default()
126 }
127
128 pub fn new(
133 access_key_id: impl Into<String>,
134 secret_access_key: impl Into<String>,
135 session_token: Option<String>,
136 expires_after: Option<SystemTime>,
137 provider_name: &'static str,
138 ) -> Self {
139 Credentials(
140 Arc::new(Inner {
141 access_key_id: Zeroizing::new(access_key_id.into()),
142 secret_access_key: Zeroizing::new(secret_access_key.into()),
143 session_token: Zeroizing::new(session_token),
144 expires_after,
145 account_id: None,
146 provider_name,
147 }),
148 HashMap::new(),
149 )
150 }
151
152 #[cfg(feature = "hardcoded-credentials")]
194 pub fn from_keys(
195 access_key_id: impl Into<String>,
196 secret_access_key: impl Into<String>,
197 session_token: Option<String>,
198 ) -> Self {
199 Self::new(
200 access_key_id,
201 secret_access_key,
202 session_token,
203 None,
204 STATIC_CREDENTIALS,
205 )
206 }
207
208 pub fn access_key_id(&self) -> &str {
210 &self.0.access_key_id
211 }
212
213 pub fn secret_access_key(&self) -> &str {
215 &self.0.secret_access_key
216 }
217
218 pub fn expiry(&self) -> Option<SystemTime> {
220 self.0.expires_after
221 }
222
223 pub fn expiry_mut(&mut self) -> &mut Option<SystemTime> {
225 &mut Arc::make_mut(&mut self.0).expires_after
226 }
227
228 pub fn account_id(&self) -> Option<&AccountId> {
230 self.0.account_id.as_ref()
231 }
232
233 pub fn session_token(&self) -> Option<&str> {
235 self.0.session_token.as_deref()
236 }
237
238 #[doc(hidden)]
240 pub fn set_property<T: Any + Clone + Debug + Send + Sync + 'static>(&mut self, prop: T) {
241 self.1
242 .insert(TypeId::of::<T>(), TypeErasedBox::new_with_clone(prop));
243 }
244
245 #[doc(hidden)]
247 pub fn get_property<T: Any + Debug + Send + Sync + 'static>(&self) -> Option<&T> {
248 self.1
249 .get(&TypeId::of::<T>())
250 .and_then(|b| b.downcast_ref())
251 }
252
253 #[doc(hidden)]
255 pub fn get_property_mut<T: Any + Debug + Send + Sync + 'static>(&mut self) -> Option<&mut T> {
256 self.1
257 .get_mut(&TypeId::of::<T>())
258 .and_then(|b| b.downcast_mut())
259 }
260
261 #[doc(hidden)]
264 pub fn get_property_mut_or_default<T: Any + Clone + Debug + Default + Send + Sync + 'static>(
265 &mut self,
266 ) -> &mut T {
267 self.1
268 .entry(TypeId::of::<T>())
269 .or_insert_with(|| TypeErasedBox::new_with_clone(T::default()))
270 .downcast_mut()
271 .expect("typechecked")
272 }
273}
274
275#[derive(Default, Clone)]
280#[allow(missing_debug_implementations)] pub struct CredentialsBuilder {
282 access_key_id: Option<Zeroizing<String>>,
283 secret_access_key: Option<Zeroizing<String>>,
284 session_token: Zeroizing<Option<String>>,
285 expires_after: Option<SystemTime>,
286 account_id: Option<AccountId>,
287 provider_name: Option<&'static str>,
288}
289
290impl CredentialsBuilder {
291 pub fn access_key_id(mut self, access_key_id: impl Into<String>) -> Self {
293 self.access_key_id = Some(Zeroizing::new(access_key_id.into()));
294 self
295 }
296
297 pub fn secret_access_key(mut self, secret_access_key: impl Into<String>) -> Self {
299 self.secret_access_key = Some(Zeroizing::new(secret_access_key.into()));
300 self
301 }
302
303 pub fn session_token(mut self, session_token: impl Into<String>) -> Self {
305 self.set_session_token(Some(session_token.into()));
306 self
307 }
308
309 pub fn set_session_token(&mut self, session_token: Option<String>) {
311 self.session_token = Zeroizing::new(session_token);
312 }
313
314 pub fn expiry(mut self, expiry: SystemTime) -> Self {
316 self.set_expiry(Some(expiry));
317 self
318 }
319
320 pub fn set_expiry(&mut self, expiry: Option<SystemTime>) {
322 self.expires_after = expiry;
323 }
324
325 pub fn account_id(mut self, account_id: impl Into<AccountId>) -> Self {
327 self.set_account_id(Some(account_id.into()));
328 self
329 }
330
331 pub fn set_account_id(&mut self, account_id: Option<AccountId>) {
333 self.account_id = account_id;
334 }
335
336 pub fn provider_name(mut self, provider_name: &'static str) -> Self {
338 self.provider_name = Some(provider_name);
339 self
340 }
341
342 pub fn build(self) -> Credentials {
344 Credentials(
345 Arc::new(Inner {
346 access_key_id: self
347 .access_key_id
348 .expect("required field `access_key_id` missing"),
349 secret_access_key: self
350 .secret_access_key
351 .expect("required field `secret_access_key` missing"),
352 session_token: self.session_token,
353 expires_after: self.expires_after,
354 account_id: self.account_id,
355 provider_name: self
356 .provider_name
357 .expect("required field `provider_name` missing"),
358 }),
359 HashMap::new(),
360 )
361 }
362}
363
364#[cfg(feature = "test-util")]
365impl Credentials {
366 pub fn for_tests() -> Self {
368 Self::new(
369 "ANOTREAL",
370 "notrealrnrELgWzOk3IfjzDKtFBhDby",
371 None,
372 None,
373 "test",
374 )
375 }
376
377 pub fn for_tests_with_session_token() -> Self {
379 Self::new(
380 "ANOTREAL",
381 "notrealrnrELgWzOk3IfjzDKtFBhDby",
382 Some("notarealsessiontoken".to_string()),
383 None,
384 "test",
385 )
386 }
387}
388
389#[cfg(feature = "test-util")]
390impl CredentialsBuilder {
391 pub fn for_tests() -> Self {
394 CredentialsBuilder::default()
395 .access_key_id("ANOTREAL")
396 .secret_access_key("notrealrnrELgWzOk3IfjzDKtFBhDby")
397 .provider_name("test")
398 }
399}
400
401impl From<Credentials> for Identity {
402 fn from(val: Credentials) -> Self {
403 let expiry = val.expiry();
404 let mut builder = if let Some(account_id) = val.account_id() {
405 Identity::builder().property(account_id.clone())
406 } else {
407 Identity::builder()
408 };
409
410 builder.set_expiration(expiry);
411
412 let features = val.get_property::<Vec<AwsCredentialFeature>>().cloned();
413 let has_account_id = val.account_id().is_some();
414
415 if features.is_some() || has_account_id {
416 let mut layer = Layer::new("IdentityResolutionFeatureIdTracking");
417 if let Some(features) = features {
418 for feat in features {
419 layer.store_append(feat);
420 }
421 }
422 if has_account_id {
423 layer.store_append(AwsCredentialFeature::ResolvedAccountId);
424 }
425 builder.set_property(layer.freeze());
426 }
427
428 if val.get_property::<StaticStabilityEligible>().is_some() {
431 builder.set_property(StaticStabilityEligible);
432 }
433
434 builder.data(val).build().expect("set required fields")
435 }
436}
437
438#[cfg(test)]
439mod test {
440 use crate::Credentials;
441 use std::time::{Duration, UNIX_EPOCH};
442
443 #[test]
444 fn debug_impl() {
445 let creds = Credentials::new(
446 "akid",
447 "secret",
448 Some("token".into()),
449 Some(UNIX_EPOCH + Duration::from_secs(1234567890)),
450 "debug tester",
451 );
452 assert_eq!(
453 format!("{:?}", creds),
454 r#"Credentials { provider_name: "debug tester", access_key_id: "akid", secret_access_key: "** redacted **", expires_after: "2009-02-13T23:31:30Z" }"#
455 );
456
457 let creds = Credentials::builder()
459 .access_key_id("akid")
460 .secret_access_key("secret")
461 .session_token("token")
462 .expiry(UNIX_EPOCH + Duration::from_secs(1234567890))
463 .account_id("012345678901")
464 .provider_name("debug tester")
465 .build();
466 assert_eq!(
467 format!("{:?}", creds),
468 r#"Credentials { provider_name: "debug tester", access_key_id: "akid", secret_access_key: "** redacted **", expires_after: "2009-02-13T23:31:30Z", account_id: "012345678901" }"#
469 );
470 }
471
472 #[cfg(feature = "test-util")]
473 #[test]
474 fn equality_ignores_properties() {
475 #[derive(Clone, Debug)]
476 struct Foo;
477 let mut creds1 = Credentials::for_tests_with_session_token();
478 creds1.set_property(crate::credential_feature::AwsCredentialFeature::CredentialsCode);
479
480 let mut creds2 = Credentials::for_tests_with_session_token();
481 creds2.set_property(Foo);
482
483 assert_eq!(creds1, creds2)
484 }
485
486 #[cfg(feature = "test-util")]
487 #[test]
488 fn identity_inherits_feature_properties() {
489 use crate::credential_feature::AwsCredentialFeature;
490 use aws_smithy_runtime_api::client::identity::Identity;
491 use aws_smithy_types::config_bag::FrozenLayer;
492
493 let mut creds = Credentials::for_tests_with_session_token();
494 let mut feature_props = vec![
495 AwsCredentialFeature::CredentialsCode,
496 AwsCredentialFeature::CredentialsStsSessionToken,
497 ];
498 creds.set_property(feature_props.clone());
499
500 let identity = Identity::from(creds);
501
502 let maybe_props = identity
503 .property::<FrozenLayer>()
504 .unwrap()
505 .load::<AwsCredentialFeature>()
506 .cloned()
507 .collect::<Vec<AwsCredentialFeature>>();
508
509 feature_props.reverse();
511 assert_eq!(maybe_props, feature_props)
512 }
513
514 #[cfg(feature = "test-util")]
515 #[test]
516 fn from_credentials_adds_resolved_account_id_feature() {
517 use crate::credential_feature::AwsCredentialFeature;
518 use aws_smithy_runtime_api::client::identity::Identity;
519 use aws_smithy_types::config_bag::FrozenLayer;
520
521 let creds = Credentials::builder()
522 .access_key_id("test")
523 .secret_access_key("test")
524 .account_id("123456789012")
525 .provider_name("test")
526 .build();
527
528 let identity = Identity::from(creds);
529
530 let layer = identity.property::<FrozenLayer>().unwrap();
531 let features = layer
532 .load::<AwsCredentialFeature>()
533 .cloned()
534 .collect::<Vec<_>>();
535 assert!(features.contains(&AwsCredentialFeature::ResolvedAccountId));
536 }
537}