Skip to main content

aws_config/meta/credentials/
chain.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use aws_credential_types::{
7    provider::{self, error::CredentialsError, future, ProvideCredentials},
8    Credentials,
9};
10use aws_smithy_types::error::display::DisplayErrorContext;
11use std::borrow::Cow;
12use std::fmt::Debug;
13use tracing::Instrument;
14
15use crate::meta::{ProviderAttempt, ProviderChainError};
16
17/// Credentials provider that checks a series of inner providers
18///
19/// Each provider will be evaluated in order:
20/// * If a provider returns valid [`Credentials`] they will be returned immediately.
21///   No other credential providers will be used.
22/// * Otherwise, if a provider returns [`CredentialsError::CredentialsNotLoaded`], the next provider will be checked.
23/// * Finally, if a provider returns any other error condition, an error will be returned immediately.
24///
25/// # Examples
26///
27/// ```no_run
28/// # fn example() {
29/// use aws_config::meta::credentials::CredentialsProviderChain;
30/// use aws_config::environment::credentials::EnvironmentVariableCredentialsProvider;
31/// use aws_config::profile::ProfileFileCredentialsProvider;
32///
33/// let provider = CredentialsProviderChain::first_try("Environment", EnvironmentVariableCredentialsProvider::new())
34///     .or_else("Profile", ProfileFileCredentialsProvider::builder().build());
35/// # }
36/// ```
37pub struct CredentialsProviderChain {
38    providers: Vec<(Cow<'static, str>, Box<dyn ProvideCredentials>)>,
39}
40
41impl Debug for CredentialsProviderChain {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        f.debug_struct("CredentialsProviderChain")
44            .field(
45                "providers",
46                &self
47                    .providers
48                    .iter()
49                    .map(|provider| &provider.0)
50                    .collect::<Vec<&Cow<'static, str>>>(),
51            )
52            .finish()
53    }
54}
55
56impl CredentialsProviderChain {
57    /// Create a `CredentialsProviderChain` that begins by evaluating this provider
58    pub fn first_try(
59        name: impl Into<Cow<'static, str>>,
60        provider: impl ProvideCredentials + 'static,
61    ) -> Self {
62        CredentialsProviderChain {
63            providers: vec![(name.into(), Box::new(provider))],
64        }
65    }
66
67    /// Add a fallback provider to the credentials provider chain
68    pub fn or_else(
69        mut self,
70        name: impl Into<Cow<'static, str>>,
71        provider: impl ProvideCredentials + 'static,
72    ) -> Self {
73        self.providers.push((name.into(), Box::new(provider)));
74        self
75    }
76
77    /// Add a fallback to the default provider chain
78    #[cfg(any(feature = "default-https-client", feature = "rustls"))]
79    pub async fn or_default_provider(self) -> Self {
80        self.or_else(
81            "DefaultProviderChain",
82            crate::default_provider::credentials::default_provider().await,
83        )
84    }
85
86    /// Creates a credential provider chain that starts with the default provider
87    #[cfg(any(feature = "default-https-client", feature = "rustls"))]
88    pub async fn default_provider() -> Self {
89        Self::first_try(
90            "DefaultProviderChain",
91            crate::default_provider::credentials::default_provider().await,
92        )
93    }
94
95    async fn credentials(&self) -> provider::Result {
96        let mut attempts = Vec::with_capacity(self.providers.len());
97        for (name, provider) in &self.providers {
98            let span = tracing::debug_span!("credentials_provider_chain", provider = %name);
99            match provider.provide_credentials().instrument(span).await {
100                Ok(credentials) => {
101                    tracing::debug!(provider = %name, "loaded credentials");
102                    return Ok(credentials);
103                }
104                Err(err @ CredentialsError::CredentialsNotLoaded(_)) => {
105                    tracing::debug!(provider = %name, context = %DisplayErrorContext(&err), "provider in chain did not provide credentials");
106                    attempts.push(ProviderAttempt::new(name.clone(), err));
107                }
108                Err(err) => {
109                    tracing::warn!(provider = %name, error = %DisplayErrorContext(&err), "provider failed to provide credentials");
110                    return Err(err);
111                }
112            }
113        }
114        Err(CredentialsError::not_loaded(ProviderChainError::new(
115            attempts,
116        )))
117    }
118}
119
120impl ProvideCredentials for CredentialsProviderChain {
121    fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
122    where
123        Self: 'a,
124    {
125        future::ProvideCredentials::new(self.credentials())
126    }
127
128    fn fallback_on_interrupt(&self) -> Option<Credentials> {
129        for (_, provider) in &self.providers {
130            if let creds @ Some(_) = provider.fallback_on_interrupt() {
131                return creds;
132            }
133        }
134        None
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use std::time::Duration;
141
142    use aws_credential_types::{
143        credential_fn::provide_credentials_fn,
144        provider::{error::CredentialsError, future, ProvideCredentials},
145        Credentials,
146    };
147    use aws_smithy_async::future::timeout::Timeout;
148
149    use crate::meta::credentials::CredentialsProviderChain;
150
151    #[derive(Debug)]
152    struct FallbackCredentials(Credentials);
153
154    impl ProvideCredentials for FallbackCredentials {
155        fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
156        where
157            Self: 'a,
158        {
159            future::ProvideCredentials::new(async {
160                tokio::time::sleep(Duration::from_millis(200)).await;
161                Ok(self.0.clone())
162            })
163        }
164
165        fn fallback_on_interrupt(&self) -> Option<Credentials> {
166            Some(self.0.clone())
167        }
168    }
169
170    #[tokio::test]
171    async fn fallback_credentials_should_be_returned_from_provider2_on_timeout_while_provider2_was_providing_credentials(
172    ) {
173        let chain = CredentialsProviderChain::first_try(
174            "provider1",
175            provide_credentials_fn(|| async {
176                tokio::time::sleep(Duration::from_millis(200)).await;
177                Err(CredentialsError::not_loaded(
178                    "no providers in chain provided credentials",
179                ))
180            }),
181        )
182        .or_else("provider2", FallbackCredentials(Credentials::for_tests()));
183
184        // Let the first call to `provide_credentials` succeed.
185        let expected = chain.provide_credentials().await.unwrap();
186
187        // Let the second call fail with an external timeout.
188        let timeout = Timeout::new(
189            chain.provide_credentials(),
190            tokio::time::sleep(Duration::from_millis(300)),
191        );
192        match timeout.await {
193            Ok(_) => panic!("provide_credentials completed before timeout future"),
194            Err(_err) => match chain.fallback_on_interrupt() {
195                Some(actual) => assert_eq!(actual, expected),
196                None => panic!(
197                    "provide_credentials timed out and no credentials returned from fallback_on_interrupt"
198                ),
199            },
200        };
201    }
202
203    #[tokio::test]
204    async fn fallback_credentials_should_be_returned_from_provider2_on_timeout_while_provider1_was_providing_credentials(
205    ) {
206        let chain = CredentialsProviderChain::first_try(
207            "provider1",
208            provide_credentials_fn(|| async {
209                tokio::time::sleep(Duration::from_millis(200)).await;
210                Err(CredentialsError::not_loaded(
211                    "no providers in chain provided credentials",
212                ))
213            }),
214        )
215        .or_else("provider2", FallbackCredentials(Credentials::for_tests()));
216
217        // Let the first call to `provide_credentials` succeed.
218        let expected = chain.provide_credentials().await.unwrap();
219
220        // Let the second call fail with an external timeout.
221        let timeout = Timeout::new(
222            chain.provide_credentials(),
223            tokio::time::sleep(Duration::from_millis(100)),
224        );
225        match timeout.await {
226            Ok(_) => panic!("provide_credentials completed before timeout future"),
227            Err(_err) => match chain.fallback_on_interrupt() {
228                Some(actual) => assert_eq!(actual, expected),
229                None => panic!(
230                    "provide_credentials timed out and no credentials returned from fallback_on_interrupt"
231                ),
232            },
233        };
234    }
235
236    #[tokio::test]
237    async fn error_message_includes_per_provider_summary() {
238        let chain = CredentialsProviderChain::first_try(
239            "Environment",
240            provide_credentials_fn(|| async { Err(CredentialsError::not_loaded("not set")) }),
241        )
242        .or_else(
243            "Profile",
244            provide_credentials_fn(|| async {
245                Err(CredentialsError::not_loaded("profile 'deploy' not found"))
246            }),
247        )
248        .or_else(
249            "IMDS",
250            provide_credentials_fn(|| async {
251                Err(CredentialsError::not_loaded(
252                    "could not communicate with IMDS",
253                ))
254            }),
255        );
256
257        let err = chain.provide_credentials().await.expect_err("should fail");
258        assert!(matches!(err, CredentialsError::CredentialsNotLoaded(_)));
259        let source = std::error::Error::source(&err)
260            .expect("error should have a source")
261            .to_string();
262        assert!(
263            source.contains("no credentials found in chain. Attempted:"),
264            "missing header in: {source}"
265        );
266        assert!(
267            source.contains("Environment:"),
268            "missing Environment in: {source}"
269        );
270        assert!(source.contains("Profile:"), "missing Profile in: {source}");
271        assert!(source.contains("IMDS:"), "missing IMDS in: {source}");
272    }
273
274    #[tokio::test]
275    async fn hard_fail_stops_chain() {
276        let chain = CredentialsProviderChain::first_try(
277            "Failing",
278            provide_credentials_fn(|| async {
279                Err(CredentialsError::provider_error("503 Service Unavailable"))
280            }),
281        )
282        .or_else(
283            "Working",
284            provide_credentials_fn(|| async { Ok(Credentials::for_tests()) }),
285        );
286
287        let err = chain
288            .provide_credentials()
289            .await
290            .expect_err("should hard-fail, not fall through");
291        assert!(
292            matches!(err, CredentialsError::ProviderError(_)),
293            "expected ProviderError, got: {err:?}"
294        );
295    }
296
297    #[tokio::test]
298    async fn empty_chain_error_message() {
299        let chain = CredentialsProviderChain { providers: vec![] };
300        let err = chain.provide_credentials().await.expect_err("should fail");
301        assert!(matches!(err, CredentialsError::CredentialsNotLoaded(_)));
302        let source = std::error::Error::source(&err)
303            .expect("error should have a source")
304            .to_string();
305        assert!(
306            source.contains("no providers were configured in the chain"),
307            "unexpected message: {source}"
308        );
309    }
310
311    #[tokio::test]
312    async fn programmatic_access_via_downcast() {
313        use crate::meta::ProviderChainError;
314
315        let chain = CredentialsProviderChain::first_try(
316            "Environment",
317            provide_credentials_fn(|| async { Err(CredentialsError::not_loaded("not set")) }),
318        )
319        .or_else(
320            "Profile",
321            provide_credentials_fn(|| async {
322                Err(CredentialsError::not_loaded("no profile defined"))
323            }),
324        );
325
326        let err = chain.provide_credentials().await.expect_err("should fail");
327        let source = std::error::Error::source(&err).expect("should have source");
328        let chain_err = source
329            .downcast_ref::<ProviderChainError<CredentialsError>>()
330            .expect("should downcast to ProviderChainError");
331        assert_eq!(chain_err.attempts().len(), 2);
332        assert_eq!(chain_err.attempts()[0].name(), "Environment");
333        assert_eq!(chain_err.attempts()[1].name(), "Profile");
334    }
335}