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/// AWS SDK Credentials
23///
24/// An opaque struct representing credentials that may be used in an AWS SDK, modeled on
25/// the [CRT credentials implementation](https://github.com/awslabs/aws-c-auth/blob/main/source/credentials.c).
26///
27/// When `Credentials` is dropped, its contents are zeroed in memory. Credentials uses an interior Arc to ensure
28/// that even when cloned, credentials don't exist in multiple memory locations.
29pub struct Credentials(Arc<Inner>, HashMap<TypeId, TypeErasedBox>);
30
31impl Clone for Credentials {
32    fn clone(&self) -> Self {
33        let mut new_map = HashMap::with_capacity(self.1.len());
34        for (k, v) in &self.1 {
35            new_map.insert(
36                *k,
37                v.try_clone()
38                    .expect("values are guaranteed to implement `Clone` via `set_property`"),
39            );
40        }
41        Self(self.0.clone(), new_map)
42    }
43}
44
45impl PartialEq for Credentials {
46    #[inline] // specified in the output of cargo expand of the original `#[derive(PartialEq)]`
47    fn eq(&self, other: &Credentials) -> bool {
48        self.0 == other.0
49    }
50}
51
52impl Eq for Credentials {}
53
54#[derive(Clone, Eq, PartialEq)]
55struct Inner {
56    access_key_id: Zeroizing<String>,
57    secret_access_key: Zeroizing<String>,
58    session_token: Zeroizing<Option<String>>,
59
60    /// Credential Expiry
61    ///
62    /// A SystemTime at which the credentials should no longer be used because they have expired.
63    /// The primary purpose of this value is to allow credentials to communicate to the caching
64    /// provider when they need to be refreshed.
65    ///
66    /// If these credentials never expire, this value will be set to `None`
67    expires_after: Option<SystemTime>,
68
69    // Optional piece of data to support account-based endpoints.
70    // https://docs.aws.amazon.com/sdkref/latest/guide/feature-account-endpoints.html
71    account_id: Option<AccountId>,
72
73    provider_name: &'static str,
74}
75
76impl Debug for Credentials {
77    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
78        let mut creds = f.debug_struct("Credentials");
79        creds
80            .field("provider_name", &self.0.provider_name)
81            .field("access_key_id", &self.0.access_key_id.as_str())
82            .field("secret_access_key", &"** redacted **");
83        if let Some(expiry) = self.expiry() {
84            if let Some(formatted) = expiry.duration_since(UNIX_EPOCH).ok().and_then(|dur| {
85                aws_smithy_types::DateTime::from_secs(dur.as_secs() as _)
86                    .fmt(Format::DateTime)
87                    .ok()
88            }) {
89                creds.field("expires_after", &formatted);
90            } else {
91                creds.field("expires_after", &expiry);
92            }
93        } else {
94            creds.field("expires_after", &"never");
95        }
96        if let Some(account_id) = &self.0.account_id {
97            creds.field("account_id", &account_id.as_str());
98        }
99        for (i, prop) in self.1.values().enumerate() {
100            creds.field(&format!("property_{i}"), prop);
101        }
102        creds.finish()
103    }
104}
105
106#[cfg(feature = "hardcoded-credentials")]
107const STATIC_CREDENTIALS: &str = "Static";
108
109impl Credentials {
110    /// Returns builder for `Credentials`.
111    pub fn builder() -> CredentialsBuilder {
112        CredentialsBuilder::default()
113    }
114
115    /// Creates `Credentials`.
116    ///
117    /// This is intended to be used from a custom credentials provider implementation.
118    /// It is __NOT__ secure to hardcode credentials into your application.
119    pub fn new(
120        access_key_id: impl Into<String>,
121        secret_access_key: impl Into<String>,
122        session_token: Option<String>,
123        expires_after: Option<SystemTime>,
124        provider_name: &'static str,
125    ) -> Self {
126        Credentials(
127            Arc::new(Inner {
128                access_key_id: Zeroizing::new(access_key_id.into()),
129                secret_access_key: Zeroizing::new(secret_access_key.into()),
130                session_token: Zeroizing::new(session_token),
131                expires_after,
132                account_id: None,
133                provider_name,
134            }),
135            HashMap::new(),
136        )
137    }
138
139    /// Creates `Credentials` from hardcoded access key, secret key, and session token.
140    ///
141    /// _Note: In general, you should prefer to use the credential providers that come
142    /// with the AWS SDK to get credentials. It is __NOT__ secure to hardcode credentials
143    /// into your application. If you're writing a custom credentials provider, then
144    /// use [`Credentials::new`] instead of this._
145    ///
146    /// This function requires the `hardcoded-credentials` feature to be enabled.
147    ///
148    /// [`Credentials`] implement
149    /// [`ProvideCredentials`](crate::provider::ProvideCredentials) directly, so no custom provider
150    /// implementation is required when wiring these up to a client:
151    /// ```rust
152    /// use aws_credential_types::Credentials;
153    /// # mod service {
154    /// #     use aws_credential_types::provider::ProvideCredentials;
155    /// #     pub struct Config;
156    /// #     impl Config {
157    /// #        pub fn builder() -> Self {
158    /// #            Config
159    /// #        }
160    /// #        pub fn credentials_provider(self, provider: impl ProvideCredentials + 'static) -> Self {
161    /// #            self
162    /// #        }
163    /// #        pub fn build(self) -> Config { Config }
164    /// #     }
165    /// #     pub struct Client;
166    /// #     impl Client {
167    /// #        pub fn from_conf(config: Config) -> Self {
168    /// #            Client
169    /// #        }
170    /// #     }
171    /// # }
172    /// # use service::{Config, Client};
173    ///
174    /// let creds = Credentials::from_keys("akid", "secret_key", None);
175    /// let config = Config::builder()
176    ///     .credentials_provider(creds)
177    ///     .build();
178    /// let client = Client::from_conf(config);
179    /// ```
180    #[cfg(feature = "hardcoded-credentials")]
181    pub fn from_keys(
182        access_key_id: impl Into<String>,
183        secret_access_key: impl Into<String>,
184        session_token: Option<String>,
185    ) -> Self {
186        Self::new(
187            access_key_id,
188            secret_access_key,
189            session_token,
190            None,
191            STATIC_CREDENTIALS,
192        )
193    }
194
195    /// Returns the access key ID.
196    pub fn access_key_id(&self) -> &str {
197        &self.0.access_key_id
198    }
199
200    /// Returns the secret access key.
201    pub fn secret_access_key(&self) -> &str {
202        &self.0.secret_access_key
203    }
204
205    /// Returns the time when the credentials will expire.
206    pub fn expiry(&self) -> Option<SystemTime> {
207        self.0.expires_after
208    }
209
210    /// Returns a mutable reference to the time when the credentials will expire.
211    pub fn expiry_mut(&mut self) -> &mut Option<SystemTime> {
212        &mut Arc::make_mut(&mut self.0).expires_after
213    }
214
215    /// Returns the account ID.
216    pub fn account_id(&self) -> Option<&AccountId> {
217        self.0.account_id.as_ref()
218    }
219
220    /// Returns the session token.
221    pub fn session_token(&self) -> Option<&str> {
222        self.0.session_token.as_deref()
223    }
224
225    /// Set arbitrary property for `Credentials`
226    #[doc(hidden)]
227    pub fn set_property<T: Any + Clone + Debug + Send + Sync + 'static>(&mut self, prop: T) {
228        self.1
229            .insert(TypeId::of::<T>(), TypeErasedBox::new_with_clone(prop));
230    }
231
232    /// Returns arbitrary property associated with this `Credentials`.
233    #[doc(hidden)]
234    pub fn get_property<T: Any + Debug + Send + Sync + 'static>(&self) -> Option<&T> {
235        self.1
236            .get(&TypeId::of::<T>())
237            .and_then(|b| b.downcast_ref())
238    }
239
240    /// Attempts to retrieve a mutable reference to property of a given type `T`.
241    #[doc(hidden)]
242    pub fn get_property_mut<T: Any + Debug + Send + Sync + 'static>(&mut self) -> Option<&mut T> {
243        self.1
244            .get_mut(&TypeId::of::<T>())
245            .and_then(|b| b.downcast_mut())
246    }
247
248    /// Returns a mutable reference to `T` if it is stored in the property, otherwise returns the
249    /// [`Default`] implementation of `T`.
250    #[doc(hidden)]
251    pub fn get_property_mut_or_default<T: Any + Clone + Debug + Default + Send + Sync + 'static>(
252        &mut self,
253    ) -> &mut T {
254        self.1
255            .entry(TypeId::of::<T>())
256            .or_insert_with(|| TypeErasedBox::new_with_clone(T::default()))
257            .downcast_mut()
258            .expect("typechecked")
259    }
260}
261
262/// Builder for [`Credentials`]
263///
264/// Similar to [`Credentials::new`], the use of the builder is intended for a custom credentials provider implementation.
265/// It is __NOT__ secure to hardcode credentials into your application.
266#[derive(Default, Clone)]
267#[allow(missing_debug_implementations)] // for security reasons, and we can add manual `impl Debug` just like `Credentials`, if needed.
268pub struct CredentialsBuilder {
269    access_key_id: Option<Zeroizing<String>>,
270    secret_access_key: Option<Zeroizing<String>>,
271    session_token: Zeroizing<Option<String>>,
272    expires_after: Option<SystemTime>,
273    account_id: Option<AccountId>,
274    provider_name: Option<&'static str>,
275}
276
277impl CredentialsBuilder {
278    /// Set access key id for the builder.
279    pub fn access_key_id(mut self, access_key_id: impl Into<String>) -> Self {
280        self.access_key_id = Some(Zeroizing::new(access_key_id.into()));
281        self
282    }
283
284    /// Set secret access key for the builder.
285    pub fn secret_access_key(mut self, secret_access_key: impl Into<String>) -> Self {
286        self.secret_access_key = Some(Zeroizing::new(secret_access_key.into()));
287        self
288    }
289
290    /// Set session token for the builder.
291    pub fn session_token(mut self, session_token: impl Into<String>) -> Self {
292        self.set_session_token(Some(session_token.into()));
293        self
294    }
295
296    /// Set session token for the builder.
297    pub fn set_session_token(&mut self, session_token: Option<String>) {
298        self.session_token = Zeroizing::new(session_token);
299    }
300
301    /// Set expiry for the builder.
302    pub fn expiry(mut self, expiry: SystemTime) -> Self {
303        self.set_expiry(Some(expiry));
304        self
305    }
306
307    /// Set expiry for the builder.
308    pub fn set_expiry(&mut self, expiry: Option<SystemTime>) {
309        self.expires_after = expiry;
310    }
311
312    /// Set account ID for the builder.
313    pub fn account_id(mut self, account_id: impl Into<AccountId>) -> Self {
314        self.set_account_id(Some(account_id.into()));
315        self
316    }
317
318    /// Set account ID for the builder.
319    pub fn set_account_id(&mut self, account_id: Option<AccountId>) {
320        self.account_id = account_id;
321    }
322
323    /// Set provider name for the builder.
324    pub fn provider_name(mut self, provider_name: &'static str) -> Self {
325        self.provider_name = Some(provider_name);
326        self
327    }
328
329    /// Build [`Credentials`] from the builder.
330    pub fn build(self) -> Credentials {
331        Credentials(
332            Arc::new(Inner {
333                access_key_id: self
334                    .access_key_id
335                    .expect("required field `access_key_id` missing"),
336                secret_access_key: self
337                    .secret_access_key
338                    .expect("required field `secret_access_key` missing"),
339                session_token: self.session_token,
340                expires_after: self.expires_after,
341                account_id: self.account_id,
342                provider_name: self
343                    .provider_name
344                    .expect("required field `provider_name` missing"),
345            }),
346            HashMap::new(),
347        )
348    }
349}
350
351#[cfg(feature = "test-util")]
352impl Credentials {
353    /// Creates a test `Credentials` with no session token.
354    pub fn for_tests() -> Self {
355        Self::new(
356            "ANOTREAL",
357            "notrealrnrELgWzOk3IfjzDKtFBhDby",
358            None,
359            None,
360            "test",
361        )
362    }
363
364    /// Creates a test `Credentials` that include a session token.
365    pub fn for_tests_with_session_token() -> Self {
366        Self::new(
367            "ANOTREAL",
368            "notrealrnrELgWzOk3IfjzDKtFBhDby",
369            Some("notarealsessiontoken".to_string()),
370            None,
371            "test",
372        )
373    }
374}
375
376#[cfg(feature = "test-util")]
377impl CredentialsBuilder {
378    /// Creates a test `CredentialsBuilder` with the required fields:
379    /// `access_key_id`, `secret_access_key`, and `provider_name`.
380    pub fn for_tests() -> Self {
381        CredentialsBuilder::default()
382            .access_key_id("ANOTREAL")
383            .secret_access_key("notrealrnrELgWzOk3IfjzDKtFBhDby")
384            .provider_name("test")
385    }
386}
387
388impl From<Credentials> for Identity {
389    fn from(val: Credentials) -> Self {
390        let expiry = val.expiry();
391        let mut builder = if let Some(account_id) = val.account_id() {
392            Identity::builder().property(account_id.clone())
393        } else {
394            Identity::builder()
395        };
396
397        builder.set_expiration(expiry);
398
399        if let Some(features) = val.get_property::<Vec<AwsCredentialFeature>>().cloned() {
400            let mut layer = Layer::new("IdentityResolutionFeatureIdTracking");
401            for feat in features {
402                layer.store_append(feat);
403            }
404            builder.set_property(layer.freeze());
405        }
406
407        builder.data(val).build().expect("set required fields")
408    }
409}
410
411#[cfg(test)]
412mod test {
413    use crate::Credentials;
414    use std::time::{Duration, UNIX_EPOCH};
415
416    #[cfg(feature = "test-util")]
417    use crate::credential_feature::AwsCredentialFeature;
418
419    #[test]
420    fn debug_impl() {
421        let creds = Credentials::new(
422            "akid",
423            "secret",
424            Some("token".into()),
425            Some(UNIX_EPOCH + Duration::from_secs(1234567890)),
426            "debug tester",
427        );
428        assert_eq!(
429            format!("{:?}", creds),
430            r#"Credentials { provider_name: "debug tester", access_key_id: "akid", secret_access_key: "** redacted **", expires_after: "2009-02-13T23:31:30Z" }"#
431        );
432
433        // with account ID
434        let creds = Credentials::builder()
435            .access_key_id("akid")
436            .secret_access_key("secret")
437            .session_token("token")
438            .expiry(UNIX_EPOCH + Duration::from_secs(1234567890))
439            .account_id("012345678901")
440            .provider_name("debug tester")
441            .build();
442        assert_eq!(
443            format!("{:?}", creds),
444            r#"Credentials { provider_name: "debug tester", access_key_id: "akid", secret_access_key: "** redacted **", expires_after: "2009-02-13T23:31:30Z", account_id: "012345678901" }"#
445        );
446    }
447
448    #[cfg(feature = "test-util")]
449    #[test]
450    fn equality_ignores_properties() {
451        #[derive(Clone, Debug)]
452        struct Foo;
453        let mut creds1 = Credentials::for_tests_with_session_token();
454        creds1.set_property(AwsCredentialFeature::CredentialsCode);
455
456        let mut creds2 = Credentials::for_tests_with_session_token();
457        creds2.set_property(Foo);
458
459        assert_eq!(creds1, creds2)
460    }
461
462    #[cfg(feature = "test-util")]
463    #[test]
464    fn identity_inherits_feature_properties() {
465        use aws_smithy_runtime_api::client::identity::Identity;
466        use aws_smithy_types::config_bag::FrozenLayer;
467
468        let mut creds = Credentials::for_tests_with_session_token();
469        let mut feature_props = vec![
470            AwsCredentialFeature::CredentialsCode,
471            AwsCredentialFeature::CredentialsStsSessionToken,
472        ];
473        creds.set_property(feature_props.clone());
474
475        let identity = Identity::from(creds);
476
477        let maybe_props = identity
478            .property::<FrozenLayer>()
479            .unwrap()
480            .load::<AwsCredentialFeature>()
481            .cloned()
482            .collect::<Vec<AwsCredentialFeature>>();
483
484        // The props get reversed when being popped out of the StoreAppend
485        feature_props.reverse();
486        assert_eq!(maybe_props, feature_props)
487    }
488}