Skip to main content

aws_credential_types/
credentials_impl.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use 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/// Marks credentials (or a token) as produced by a **built-in AWS provider that is eligible for
23/// static-stability caching** — i.e. the AWS `StaticStabilityCache` may keep serving them past
24/// expiration on a failed refresh (subject to backoff).
25///
26/// Built-in providers (IMDS, ECS/EKS container, STS AssumeRole[WithWebIdentity], SSO, Login,
27/// Cognito) stamp this on the `Credentials` they produce; the `From<Credentials> for Identity`
28/// conversion lifts it to an `Identity`-level property, and the cache reads it back generically via
29/// `Identity::property::<StaticStabilityEligible>()`. Custom/process providers stamp nothing and
30/// therefore get caching-only behavior.
31#[doc(hidden)]
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub struct StaticStabilityEligible;
34
35/// AWS SDK Credentials
36///
37/// An opaque struct representing credentials that may be used in an AWS SDK, modeled on
38/// the [CRT credentials implementation](https://github.com/awslabs/aws-c-auth/blob/main/source/credentials.c).
39///
40/// When `Credentials` is dropped, its contents are zeroed in memory. Credentials uses an interior Arc to ensure
41/// that even when cloned, credentials don't exist in multiple memory locations.
42pub 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] // specified in the output of cargo expand of the original `#[derive(PartialEq)]`
60    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    /// Credential Expiry
74    ///
75    /// A SystemTime at which the credentials should no longer be used because they have expired.
76    /// The primary purpose of this value is to allow credentials to communicate to the caching
77    /// provider when they need to be refreshed.
78    ///
79    /// If these credentials never expire, this value will be set to `None`
80    expires_after: Option<SystemTime>,
81
82    // Optional piece of data to support account-based endpoints.
83    // https://docs.aws.amazon.com/sdkref/latest/guide/feature-account-endpoints.html
84    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    /// Returns builder for `Credentials`.
124    pub fn builder() -> CredentialsBuilder {
125        CredentialsBuilder::default()
126    }
127
128    /// Creates `Credentials`.
129    ///
130    /// This is intended to be used from a custom credentials provider implementation.
131    /// It is __NOT__ secure to hardcode credentials into your application.
132    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    /// Creates `Credentials` from hardcoded access key, secret key, and session token.
153    ///
154    /// _Note: In general, you should prefer to use the credential providers that come
155    /// with the AWS SDK to get credentials. It is __NOT__ secure to hardcode credentials
156    /// into your application. If you're writing a custom credentials provider, then
157    /// use [`Credentials::new`] instead of this._
158    ///
159    /// This function requires the `hardcoded-credentials` feature to be enabled.
160    ///
161    /// [`Credentials`] implement
162    /// [`ProvideCredentials`](crate::provider::ProvideCredentials) directly, so no custom provider
163    /// implementation is required when wiring these up to a client:
164    /// ```rust
165    /// use aws_credential_types::Credentials;
166    /// # mod service {
167    /// #     use aws_credential_types::provider::ProvideCredentials;
168    /// #     pub struct Config;
169    /// #     impl Config {
170    /// #        pub fn builder() -> Self {
171    /// #            Config
172    /// #        }
173    /// #        pub fn credentials_provider(self, provider: impl ProvideCredentials + 'static) -> Self {
174    /// #            self
175    /// #        }
176    /// #        pub fn build(self) -> Config { Config }
177    /// #     }
178    /// #     pub struct Client;
179    /// #     impl Client {
180    /// #        pub fn from_conf(config: Config) -> Self {
181    /// #            Client
182    /// #        }
183    /// #     }
184    /// # }
185    /// # use service::{Config, Client};
186    ///
187    /// let creds = Credentials::from_keys("akid", "secret_key", None);
188    /// let config = Config::builder()
189    ///     .credentials_provider(creds)
190    ///     .build();
191    /// let client = Client::from_conf(config);
192    /// ```
193    #[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    /// Returns the access key ID.
209    pub fn access_key_id(&self) -> &str {
210        &self.0.access_key_id
211    }
212
213    /// Returns the secret access key.
214    pub fn secret_access_key(&self) -> &str {
215        &self.0.secret_access_key
216    }
217
218    /// Returns the time when the credentials will expire.
219    pub fn expiry(&self) -> Option<SystemTime> {
220        self.0.expires_after
221    }
222
223    /// Returns a mutable reference to the time when the credentials will expire.
224    pub fn expiry_mut(&mut self) -> &mut Option<SystemTime> {
225        &mut Arc::make_mut(&mut self.0).expires_after
226    }
227
228    /// Returns the account ID.
229    pub fn account_id(&self) -> Option<&AccountId> {
230        self.0.account_id.as_ref()
231    }
232
233    /// Returns the session token.
234    pub fn session_token(&self) -> Option<&str> {
235        self.0.session_token.as_deref()
236    }
237
238    /// Set arbitrary property for `Credentials`
239    #[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    /// Returns arbitrary property associated with this `Credentials`.
246    #[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    /// Attempts to retrieve a mutable reference to property of a given type `T`.
254    #[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    /// Returns a mutable reference to `T` if it is stored in the property, otherwise returns the
262    /// [`Default`] implementation of `T`.
263    #[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/// Builder for [`Credentials`]
276///
277/// Similar to [`Credentials::new`], the use of the builder is intended for a custom credentials provider implementation.
278/// It is __NOT__ secure to hardcode credentials into your application.
279#[derive(Default, Clone)]
280#[allow(missing_debug_implementations)] // for security reasons, and we can add manual `impl Debug` just like `Credentials`, if needed.
281pub 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    /// Set access key id for the builder.
292    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    /// Set secret access key for the builder.
298    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    /// Set session token for the builder.
304    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    /// Set session token for the builder.
310    pub fn set_session_token(&mut self, session_token: Option<String>) {
311        self.session_token = Zeroizing::new(session_token);
312    }
313
314    /// Set expiry for the builder.
315    pub fn expiry(mut self, expiry: SystemTime) -> Self {
316        self.set_expiry(Some(expiry));
317        self
318    }
319
320    /// Set expiry for the builder.
321    pub fn set_expiry(&mut self, expiry: Option<SystemTime>) {
322        self.expires_after = expiry;
323    }
324
325    /// Set account ID for the builder.
326    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    /// Set account ID for the builder.
332    pub fn set_account_id(&mut self, account_id: Option<AccountId>) {
333        self.account_id = account_id;
334    }
335
336    /// Set provider name for the builder.
337    pub fn provider_name(mut self, provider_name: &'static str) -> Self {
338        self.provider_name = Some(provider_name);
339        self
340    }
341
342    /// Build [`Credentials`] from the builder.
343    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    /// Creates a test `Credentials` with no session token.
367    pub fn for_tests() -> Self {
368        Self::new(
369            "ANOTREAL",
370            "notrealrnrELgWzOk3IfjzDKtFBhDby",
371            None,
372            None,
373            "test",
374        )
375    }
376
377    /// Creates a test `Credentials` that include a session token.
378    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    /// Creates a test `CredentialsBuilder` with the required fields:
392    /// `access_key_id`, `secret_access_key`, and `provider_name`.
393    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        // Lift the static-stability eligibility marker (stamped by built-in providers) from the
429        // Credentials onto the generic Identity, so the cache reads it without a downcast.
430        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        // with account ID
458        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        // The props get reversed when being popped out of the StoreAppend
510        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}