aws_config/default_provider/
credentials.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use std::borrow::Cow;
7
8use aws_credential_types::provider::{self, future, ProvideCredentials};
9use aws_credential_types::Credentials;
10use tracing::Instrument;
11
12use crate::environment::credentials::EnvironmentVariableCredentialsProvider;
13use crate::meta::credentials::CredentialsProviderChain;
14use crate::meta::region::ProvideRegion;
15use crate::provider_config::ProviderConfig;
16
17#[cfg(any(feature = "default-https-client", feature = "rustls"))]
18/// Default Credentials Provider chain
19///
20/// The region from the default region provider will be used
21pub async fn default_provider() -> impl ProvideCredentials {
22    DefaultCredentialsChain::builder().build().await
23}
24
25/// Default AWS Credential Provider Chain
26///
27/// Resolution order:
28/// 1. Environment variables: [`EnvironmentVariableCredentialsProvider`]
29/// 2. Shared config (`~/.aws/config`, `~/.aws/credentials`): [`SharedConfigCredentialsProvider`](crate::profile::ProfileFileCredentialsProvider)
30/// 3. [Web Identity Tokens](crate::web_identity_token)
31/// 4. ECS (IAM Roles for Tasks) & General HTTP credentials: [`ecs`](crate::ecs)
32/// 5. [EC2 IMDSv2](crate::imds)
33///
34/// The outer provider is wrapped in a refreshing cache.
35///
36/// More providers are a work in progress.
37///
38/// # Examples
39/// Create a default chain with a custom region:
40/// ```no_run
41/// use aws_types::region::Region;
42/// use aws_config::default_provider::credentials::DefaultCredentialsChain;
43/// let credentials_provider = DefaultCredentialsChain::builder()
44///     .region(Region::new("us-west-1"))
45///     .build();
46/// ```
47///
48/// Create a default chain with no overrides:
49/// ```no_run
50/// use aws_config::default_provider::credentials::DefaultCredentialsChain;
51/// let credentials_provider = DefaultCredentialsChain::builder().build();
52/// ```
53///
54/// Create a default chain that uses a different profile:
55/// ```no_run
56/// use aws_config::default_provider::credentials::DefaultCredentialsChain;
57/// let credentials_provider = DefaultCredentialsChain::builder()
58///     .profile_name("otherprofile")
59///     .build();
60/// ```
61#[derive(Debug)]
62pub struct DefaultCredentialsChain {
63    provider_chain: CredentialsProviderChain,
64}
65
66impl DefaultCredentialsChain {
67    /// Builder for `DefaultCredentialsChain`
68    pub fn builder() -> Builder {
69        Builder::default()
70    }
71
72    async fn credentials(&self) -> provider::Result {
73        self.provider_chain
74            .provide_credentials()
75            .instrument(tracing::debug_span!("provide_credentials", provider = %"default_chain"))
76            .await
77    }
78}
79
80impl ProvideCredentials for DefaultCredentialsChain {
81    fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
82    where
83        Self: 'a,
84    {
85        future::ProvideCredentials::new(self.credentials())
86    }
87
88    fn fallback_on_interrupt(&self) -> Option<Credentials> {
89        self.provider_chain.fallback_on_interrupt()
90    }
91}
92
93/// Builder for [`DefaultCredentialsChain`].
94#[derive(Debug, Default)]
95pub struct Builder {
96    profile_file_builder: crate::profile::credentials::Builder,
97    web_identity_builder: crate::web_identity_token::Builder,
98    imds_builder: crate::imds::credentials::Builder,
99    ecs_builder: crate::ecs::Builder,
100    region_override: Option<Box<dyn ProvideRegion>>,
101    region_chain: crate::default_provider::region::Builder,
102    conf: Option<ProviderConfig>,
103}
104
105impl Builder {
106    /// Sets the region used when making requests to AWS services
107    ///
108    /// When unset, the default region resolver chain will be used.
109    pub fn region(mut self, region: impl ProvideRegion + 'static) -> Self {
110        self.set_region(Some(region));
111        self
112    }
113
114    /// Sets the region used when making requests to AWS services
115    ///
116    /// When unset, the default region resolver chain will be used.
117    pub fn set_region(&mut self, region: Option<impl ProvideRegion + 'static>) -> &mut Self {
118        self.region_override = region.map(|provider| Box::new(provider) as _);
119        self
120    }
121
122    /// Add an additional credential source for the ProfileProvider
123    ///
124    /// Assume role profiles may specify named credential sources:
125    /// ```ini
126    /// [default]
127    /// role_arn = arn:aws:iam::123456789:role/RoleA
128    /// credential_source = MyCustomProvider
129    /// ```
130    ///
131    /// Typically, these are built-in providers like `Environment`, however, custom sources may
132    /// also be used.
133    ///
134    /// See [`with_custom_provider`](crate::profile::credentials::Builder::with_custom_provider)
135    pub fn with_custom_credential_source(
136        mut self,
137        name: impl Into<Cow<'static, str>>,
138        provider: impl ProvideCredentials + 'static,
139    ) -> Self {
140        self.profile_file_builder = self
141            .profile_file_builder
142            .with_custom_provider(name, provider);
143        self
144    }
145
146    /// Override the profile name used by this provider
147    ///
148    /// When unset, the value of the `AWS_PROFILE` environment variable will be used.
149    pub fn profile_name(mut self, name: &str) -> Self {
150        self.profile_file_builder = self.profile_file_builder.profile_name(name);
151        self.region_chain = self.region_chain.profile_name(name);
152        self
153    }
154
155    /// Override the IMDS client used for this provider
156    ///
157    /// When unset, the default IMDS client will be used.
158    pub fn imds_client(mut self, client: crate::imds::Client) -> Self {
159        self.imds_builder = self.imds_builder.imds_client(client);
160        self
161    }
162
163    /// Override the configuration used for this provider
164    pub fn configure(mut self, config: ProviderConfig) -> Self {
165        self.region_chain = self.region_chain.configure(&config);
166        self.conf = Some(config);
167        self
168    }
169
170    /// Creates a `DefaultCredentialsChain`
171    ///
172    /// ## Panics
173    /// This function will panic if no connector has been set or the `default-https-client`
174    /// feature has been disabled.
175    pub async fn build(self) -> DefaultCredentialsChain {
176        let region = match self.region_override {
177            Some(provider) => provider.region().await,
178            None => self.region_chain.build().region().await,
179        };
180
181        let conf = self.conf.unwrap_or_default().with_region(region);
182
183        let env_provider = EnvironmentVariableCredentialsProvider::new_with_env(conf.env());
184        let profile_provider = self.profile_file_builder.configure(&conf).build();
185        let web_identity_token_provider = self.web_identity_builder.configure(&conf).build();
186        let imds_provider = self.imds_builder.configure(&conf).build();
187        let ecs_provider = self.ecs_builder.configure(&conf).build();
188
189        let provider_chain = CredentialsProviderChain::first_try("Environment", env_provider)
190            .or_else("Profile", profile_provider)
191            .or_else("WebIdentityToken", web_identity_token_provider)
192            .or_else("EcsContainer", ecs_provider)
193            .or_else("Ec2InstanceMetadata", imds_provider);
194
195        DefaultCredentialsChain { provider_chain }
196    }
197}
198
199#[cfg(test)]
200mod test {
201    use crate::default_provider::credentials::DefaultCredentialsChain;
202    use crate::test_case::{StaticTestProvider, TestEnvironment};
203    use aws_credential_types::provider::ProvideCredentials;
204    use aws_smithy_async::time::StaticTimeSource;
205    use std::time::UNIX_EPOCH;
206
207    /// Test generation macro
208    ///
209    /// # Examples
210    /// **Run the test case in `test-data/default-credential-provider-chain/test_name`
211    /// ```no_run
212    /// make_test!(test_name);
213    /// ```
214    ///
215    /// **Update (responses are replayed but new requests are recorded) the test case**:
216    /// ```no_run
217    /// make_test!(update: test_name)
218    /// ```
219    ///
220    /// **Run the test case against a real HTTPS connection:**
221    /// > Note: Be careful to remove sensitive information before committing. Always use a temporary
222    /// > AWS account when recording live traffic.
223    /// ```no_run
224    /// make_test!(live: test_name)
225    /// ```
226    macro_rules! make_test {
227        ($name:ident $(#[$m:meta])*) => {
228            make_test!($name, execute, $(#[$m])*);
229        };
230        (update: $name:ident) => {
231            make_test!($name, execute_and_update);
232        };
233        (live: $name:ident) => {
234            make_test!($name, execute_from_live_traffic);
235        };
236        ($name:ident, $func:ident, $(#[$m:meta])*) => {
237            make_test!($name, $func, std::convert::identity $(, #[$m])*);
238        };
239        ($name:ident, builder: $provider_config_builder:expr) => {
240            make_test!($name, execute, $provider_config_builder);
241        };
242        ($name:ident, $func:ident, $provider_config_builder:expr $(, #[$m:meta])*) => {
243            $(#[$m])*
244            #[tokio::test]
245            async fn $name() {
246                let _ = crate::test_case::TestEnvironment::from_dir(
247                    concat!(
248                        "./test-data/default-credential-provider-chain/",
249                        stringify!($name)
250                    ),
251                    crate::test_case::test_credentials_provider(|config| {
252                        async move {
253                            crate::default_provider::credentials::Builder::default()
254                                .configure(config)
255                                .build()
256                                .await
257                                .provide_credentials()
258                                .await
259                        }
260                    }),
261                )
262                .await
263                .unwrap()
264                .map_provider_config($provider_config_builder)
265                .$func()
266                .await;
267            }
268        };
269    }
270
271    make_test!(prefer_environment);
272    make_test!(profile_static_keys);
273    make_test!(profile_static_keys_case_insensitive);
274    make_test!(web_identity_token_env);
275    make_test!(web_identity_source_profile_no_env);
276    make_test!(web_identity_token_invalid_jwt);
277    make_test!(web_identity_token_source_profile);
278    make_test!(web_identity_token_profile);
279    make_test!(profile_name);
280    make_test!(profile_overrides_web_identity);
281    make_test!(environment_variables_blank);
282    make_test!(imds_token_fail);
283
284    make_test!(imds_no_iam_role);
285    make_test!(imds_default_chain_error);
286    make_test!(imds_default_chain_success, builder: |config| {
287        config.with_time_source(StaticTimeSource::new(UNIX_EPOCH))
288    });
289    make_test!(imds_assume_role);
290    make_test!(imds_config_with_no_creds, builder: |config| {
291        config.with_time_source(StaticTimeSource::new(UNIX_EPOCH))
292    });
293    make_test!(imds_disabled);
294    make_test!(imds_default_chain_retries, builder: |config| {
295        config.with_time_source(StaticTimeSource::new(UNIX_EPOCH))
296    });
297    make_test!(ecs_assume_role);
298    make_test!(ecs_credentials);
299    make_test!(ecs_credentials_invalid_profile);
300
301    make_test!(eks_pod_identity_credentials);
302    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is disabled on Windows because it uses Unix-style paths
303    #[cfg(not(windows))]
304    make_test!(eks_pod_identity_no_token_file);
305
306    #[cfg(not(feature = "sso"))]
307    make_test!(sso_assume_role #[should_panic(expected = "This behavior requires following cargo feature(s) enabled: sso")]);
308
309    #[cfg(feature = "sso")]
310    make_test!(sso_assume_role);
311
312    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is disabled on Windows because it uses Unix-style paths
313    #[cfg(not(any(feature = "sso", windows)))]
314    make_test!(sso_no_token_file #[should_panic(expected = "This behavior requires following cargo feature(s) enabled: sso")]);
315    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is disabled on Windows because it uses Unix-style paths
316    #[cfg(all(feature = "sso", not(windows)))]
317    make_test!(sso_no_token_file);
318
319    #[cfg(feature = "sso")]
320    make_test!(e2e_fips_and_dual_stack_sso);
321
322    #[tokio::test]
323    async fn profile_name_override() {
324        // Only use the TestEnvironment to create a ProviderConfig from the
325        // profile_static_keys test directory. We don't actually want to
326        // use the expected test output from that directory since we're
327        // overriding the profile name on the credentials chain in this test.
328        let provider_config = TestEnvironment::<crate::test_case::Credentials, ()>::from_dir(
329            "./test-data/default-credential-provider-chain/profile_static_keys",
330            StaticTestProvider::new(|_| unreachable!()),
331        )
332        .await
333        .unwrap()
334        .provider_config()
335        .clone();
336
337        let creds = DefaultCredentialsChain::builder()
338            .profile_name("secondary")
339            .configure(provider_config)
340            .build()
341            .await
342            .provide_credentials()
343            .await
344            .expect("creds should load");
345
346        assert_eq!(creds.access_key_id(), "correct_key_secondary");
347    }
348
349    #[tokio::test]
350    async fn no_providers_configured_err() {
351        use crate::provider_config::ProviderConfig;
352        use aws_credential_types::provider::error::CredentialsError;
353        use aws_smithy_async::rt::sleep::TokioSleep;
354        use aws_smithy_http_client::test_util::NeverTcpConnector;
355
356        tokio::time::pause();
357        let conf = ProviderConfig::no_configuration()
358            .with_http_client(NeverTcpConnector::new().into_client())
359            .with_time_source(StaticTimeSource::new(UNIX_EPOCH))
360            .with_sleep_impl(TokioSleep::new());
361        let provider = DefaultCredentialsChain::builder()
362            .configure(conf)
363            .build()
364            .await;
365        let creds = provider
366            .provide_credentials()
367            .await
368            .expect_err("no providers enabled");
369        assert!(
370            matches!(creds, CredentialsError::CredentialsNotLoaded { .. }),
371            "should be NotLoaded: {:?}",
372            creds
373        )
374    }
375}