aws_config/meta/credentials/
chain.rs1use 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
17pub 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 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 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 #[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 #[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 expected = chain.provide_credentials().await.unwrap();
186
187 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 expected = chain.provide_credentials().await.unwrap();
219
220 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}