Skip to main content

aws_config/
provider_config.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Configuration Options for Credential Providers
7
8use crate::env_service_config::EnvServiceConfig;
9use crate::profile;
10#[allow(deprecated)]
11use crate::profile::profile_file::ProfileFiles;
12use crate::profile::{ProfileFileLoadError, ProfileSet};
13use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep, SharedAsyncSleep};
14use aws_smithy_async::time::{SharedTimeSource, TimeSource};
15use aws_smithy_runtime_api::client::behavior_version::BehaviorVersion;
16use aws_smithy_runtime_api::client::http::HttpClient;
17use aws_smithy_runtime_api::shared::IntoShared;
18use aws_smithy_types::error::display::DisplayErrorContext;
19use aws_smithy_types::retry::RetryConfig;
20use aws_smithy_types::timeout::TimeoutConfig;
21use aws_types::os_shim_internal::{Env, Fs};
22use aws_types::region::Region;
23use aws_types::sdk_config::SharedHttpClient;
24use aws_types::SdkConfig;
25use std::borrow::Cow;
26use std::fmt::{Debug, Formatter};
27use std::sync::Arc;
28use tokio::sync::OnceCell;
29
30/// Configuration options for Credential Providers
31///
32/// Most credential providers builders offer a `configure` method which applies general provider configuration
33/// options.
34///
35/// To use a region from the default region provider chain use [`ProviderConfig::with_default_region`].
36/// Otherwise, use [`ProviderConfig::without_region`]. Note that some credentials providers require a region
37/// to be explicitly set.
38#[derive(Clone)]
39pub struct ProviderConfig {
40    env: Env,
41    fs: Fs,
42    time_source: SharedTimeSource,
43    http_client: Option<SharedHttpClient>,
44    retry_config: Option<RetryConfig>,
45    timeout_config: Option<TimeoutConfig>,
46    sleep_impl: Option<SharedAsyncSleep>,
47    region: Option<Region>,
48    use_fips: Option<bool>,
49    use_dual_stack: Option<bool>,
50    behavior_version: Option<BehaviorVersion>,
51    /// An AWS profile created from `ProfileFiles` and a `profile_name`
52    parsed_profile: Arc<OnceCell<Result<ProfileSet, ProfileFileLoadError>>>,
53    /// A list of [std::path::Path]s to profile files
54    #[allow(deprecated)]
55    profile_files: ProfileFiles,
56    /// An override to use when constructing a `ProfileSet`
57    profile_name_override: Option<Cow<'static, str>>,
58}
59
60impl Debug for ProviderConfig {
61    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
62        f.debug_struct("ProviderConfig")
63            .field("env", &self.env)
64            .field("fs", &self.fs)
65            .field("time_source", &self.time_source)
66            .field("http_client", &self.http_client)
67            .field("retry_config", &self.retry_config)
68            .field("timeout_config", &self.timeout_config)
69            .field("sleep_impl", &self.sleep_impl)
70            .field("region", &self.region)
71            .field("use_fips", &self.use_fips)
72            .field("use_dual_stack", &self.use_dual_stack)
73            .field("profile_name_override", &self.profile_name_override)
74            .finish()
75    }
76}
77
78impl Default for ProviderConfig {
79    fn default() -> Self {
80        Self {
81            env: Env::default(),
82            fs: Fs::default(),
83            time_source: SharedTimeSource::default(),
84            http_client: None,
85            retry_config: None,
86            timeout_config: None,
87            sleep_impl: default_async_sleep(),
88            region: None,
89            use_fips: None,
90            use_dual_stack: None,
91            behavior_version: None,
92            parsed_profile: Default::default(),
93            #[allow(deprecated)]
94            profile_files: ProfileFiles::default(),
95            profile_name_override: None,
96        }
97    }
98}
99
100#[cfg(test)]
101impl ProviderConfig {
102    /// ProviderConfig with all configuration removed
103    ///
104    /// Unlike [`ProviderConfig::empty`] where `env` and `fs` will use their non-mocked implementations,
105    /// this method will use an empty mock environment and an empty mock file system.
106    pub fn no_configuration() -> Self {
107        use aws_smithy_async::time::StaticTimeSource;
108        use std::collections::HashMap;
109        use std::time::UNIX_EPOCH;
110        let fs = Fs::from_raw_map(HashMap::new());
111        let env = Env::from_slice(&[]);
112        Self {
113            parsed_profile: Default::default(),
114            #[allow(deprecated)]
115            profile_files: ProfileFiles::default(),
116            env,
117            fs,
118            time_source: SharedTimeSource::new(StaticTimeSource::new(UNIX_EPOCH)),
119            http_client: None,
120            retry_config: None,
121            timeout_config: None,
122            sleep_impl: None,
123            region: None,
124            use_fips: None,
125            use_dual_stack: None,
126            behavior_version: None,
127            profile_name_override: None,
128        }
129    }
130}
131
132impl ProviderConfig {
133    /// Create a default provider config with the region unset.
134    ///
135    /// Using this option means that you may need to set a region manually.
136    ///
137    /// This constructor will use a default value for the HTTPS connector and Sleep implementation
138    /// when they are enabled as crate features which is usually the correct option. To construct
139    /// a `ProviderConfig` without these fields set, use [`ProviderConfig::empty`].
140    ///
141    ///
142    /// # Examples
143    /// ```no_run
144    /// # #[cfg(feature = "default-https-client")]
145    /// # fn example() {
146    /// use aws_config::provider_config::ProviderConfig;
147    /// use aws_sdk_sts::config::Region;
148    /// use aws_config::web_identity_token::WebIdentityTokenCredentialsProvider;
149    /// let conf = ProviderConfig::without_region().with_region(Some(Region::new("us-east-1")));
150    ///
151    /// let credential_provider = WebIdentityTokenCredentialsProvider::builder().configure(&conf).build();
152    /// # }
153    /// ```
154    pub fn without_region() -> Self {
155        Self::default()
156    }
157
158    /// Constructs a ProviderConfig with no fields set
159    pub fn empty() -> Self {
160        ProviderConfig {
161            env: Env::default(),
162            fs: Fs::default(),
163            time_source: SharedTimeSource::default(),
164            http_client: None,
165            retry_config: None,
166            timeout_config: None,
167            sleep_impl: None,
168            region: None,
169            use_fips: None,
170            use_dual_stack: None,
171            behavior_version: None,
172            parsed_profile: Default::default(),
173            #[allow(deprecated)]
174            profile_files: ProfileFiles::default(),
175            profile_name_override: None,
176        }
177    }
178
179    /// Initializer for ConfigBag to avoid possibly setting incorrect defaults.
180    pub(crate) fn init(
181        time_source: SharedTimeSource,
182        sleep_impl: Option<SharedAsyncSleep>,
183    ) -> Self {
184        Self {
185            parsed_profile: Default::default(),
186            #[allow(deprecated)]
187            profile_files: ProfileFiles::default(),
188            env: Env::default(),
189            fs: Fs::default(),
190            time_source,
191            http_client: None,
192            retry_config: None,
193            timeout_config: None,
194            sleep_impl,
195            region: None,
196            use_fips: None,
197            use_dual_stack: None,
198            behavior_version: None,
199            profile_name_override: None,
200        }
201    }
202
203    /// Create a default provider config with the region region automatically loaded from the default chain.
204    ///
205    /// # Examples
206    /// ```no_run
207    /// # async fn test() {
208    /// use aws_config::provider_config::ProviderConfig;
209    /// use aws_sdk_sts::config::Region;
210    /// use aws_config::web_identity_token::WebIdentityTokenCredentialsProvider;
211    /// let conf = ProviderConfig::with_default_region().await;
212    /// let credential_provider = WebIdentityTokenCredentialsProvider::builder().configure(&conf).build();
213    /// }
214    /// ```
215    pub async fn with_default_region() -> Self {
216        Self::without_region().load_default_region().await
217    }
218
219    /// Attempt to get a representation of `SdkConfig` from this `ProviderConfig`.
220    ///
221    ///
222    /// **WARN**: Some options (e.g. `service_config`) can only be set if the profile has been
223    /// parsed already (e.g. by calling [`ProviderConfig::profile()`]). This is an
224    /// imperfect mapping and should be used sparingly.
225    pub(crate) fn client_config(&self) -> SdkConfig {
226        let profiles = self.parsed_profile.get().and_then(|v| v.as_ref().ok());
227        let service_config = EnvServiceConfig {
228            env: self.env(),
229            env_config_sections: profiles.cloned().unwrap_or_default(),
230            ignore_configured_endpoint_urls: false,
231        };
232
233        let mut builder = SdkConfig::builder()
234            .retry_config(
235                self.retry_config
236                    .as_ref()
237                    .map_or(RetryConfig::standard(), |config| config.clone()),
238            )
239            .region(self.region())
240            .time_source(self.time_source())
241            .use_fips(self.use_fips().unwrap_or_default())
242            .use_dual_stack(self.use_dual_stack().unwrap_or_default())
243            .service_config(service_config)
244            .behavior_version(crate::BehaviorVersion::latest());
245        if let Some(timeout_config) = self.timeout_config.as_ref() {
246            builder.set_timeout_config(Some(timeout_config.clone()));
247        }
248        builder.set_http_client(self.http_client.clone());
249        builder.set_sleep_impl(self.sleep_impl.clone());
250        builder.build()
251    }
252
253    // When all crate features are disabled, these accessors are unused
254
255    #[allow(dead_code)]
256    pub(crate) fn env(&self) -> Env {
257        self.env.clone()
258    }
259
260    #[allow(dead_code)]
261    pub(crate) fn fs(&self) -> Fs {
262        self.fs.clone()
263    }
264
265    #[allow(dead_code)]
266    pub(crate) fn time_source(&self) -> SharedTimeSource {
267        self.time_source.clone()
268    }
269
270    #[allow(dead_code)]
271    pub(crate) fn http_client(&self) -> Option<SharedHttpClient> {
272        self.http_client.clone()
273    }
274
275    #[allow(dead_code)]
276    pub(crate) fn retry_config(&self) -> Option<RetryConfig> {
277        self.retry_config.clone()
278    }
279
280    #[allow(dead_code)]
281    pub(crate) fn sleep_impl(&self) -> Option<SharedAsyncSleep> {
282        self.sleep_impl.clone()
283    }
284
285    #[allow(dead_code)]
286    pub(crate) fn region(&self) -> Option<Region> {
287        self.region.clone()
288    }
289
290    #[allow(dead_code)]
291    pub(crate) fn use_fips(&self) -> Option<bool> {
292        self.use_fips
293    }
294
295    #[allow(dead_code)]
296    pub(crate) fn use_dual_stack(&self) -> Option<bool> {
297        self.use_dual_stack
298    }
299
300    pub(crate) async fn try_profile(&self) -> Result<&ProfileSet, &ProfileFileLoadError> {
301        let parsed_profile = self
302            .parsed_profile
303            .get_or_init(|| async {
304                let profile = profile::load(
305                    &self.fs,
306                    &self.env,
307                    &self.profile_files,
308                    self.profile_name_override.clone(),
309                )
310                .await;
311                if let Err(err) = profile.as_ref() {
312                    tracing::warn!(err = %DisplayErrorContext(&err), "failed to parse profile")
313                }
314                profile
315            })
316            .await;
317        parsed_profile.as_ref()
318    }
319
320    pub(crate) async fn profile(&self) -> Option<&ProfileSet> {
321        self.try_profile().await.ok()
322    }
323
324    /// Override the region for the configuration
325    pub fn with_region(mut self, region: Option<Region>) -> Self {
326        self.region = region;
327        self
328    }
329
330    /// Override the `use_fips` setting.
331    ///
332    /// When set to `Some(true)`, credential providers configured with this
333    /// `ProviderConfig` (e.g., [`DefaultCredentialsChain`], `AssumeRoleProvider`,
334    /// `WebIdentityTokenCredentialsProvider`) will use FIPS-compliant endpoints.
335    ///
336    /// This is the `ProviderConfig` equivalent of
337    /// [`ConfigLoader::use_fips`](crate::ConfigLoader::use_fips). It is needed
338    /// when constructing a `ProviderConfig` directly (e.g., via
339    /// [`ProviderConfig::empty()`]) rather than going through the `ConfigLoader`.
340    ///
341    /// [`DefaultCredentialsChain`]: crate::default_provider::credentials::DefaultCredentialsChain
342    pub fn with_use_fips(mut self, use_fips: Option<bool>) -> Self {
343        self.use_fips = use_fips;
344        self
345    }
346
347    /// Override the `use_dual_stack` setting.
348    ///
349    /// When set to `Some(true)`, credential providers configured with this
350    /// `ProviderConfig` will use dual-stack endpoints.
351    ///
352    /// This is the `ProviderConfig` equivalent of
353    /// [`ConfigLoader::use_dual_stack`](crate::ConfigLoader::use_dual_stack).
354    pub fn with_use_dual_stack(mut self, use_dual_stack: Option<bool>) -> Self {
355        self.use_dual_stack = use_dual_stack;
356        self
357    }
358
359    pub(crate) fn behavior_version(&self) -> Option<BehaviorVersion> {
360        self.behavior_version
361    }
362
363    /// Sets the behavior version for this provider config.
364    pub fn with_behavior_version(mut self, behavior_version: Option<BehaviorVersion>) -> Self {
365        self.behavior_version = behavior_version;
366        self
367    }
368
369    pub(crate) fn with_profile_name(self, profile_name: String) -> Self {
370        let profile_files = self.profile_files.clone();
371        self.with_profile_config(Some(profile_files), Some(profile_name))
372    }
373
374    /// Override the profile file paths (`~/.aws/config` by default) and name (`default` by default)
375    #[allow(deprecated)]
376    pub(crate) fn with_profile_config(
377        self,
378        profile_files: Option<ProfileFiles>,
379        profile_name_override: Option<String>,
380    ) -> Self {
381        // if there is no override, then don't clear out `parsed_profile`.
382        if profile_files.is_none() && profile_name_override.is_none() {
383            return self;
384        }
385        ProviderConfig {
386            // clear out the profile since we need to reparse it
387            parsed_profile: Default::default(),
388            profile_files: profile_files.unwrap_or(self.profile_files),
389            profile_name_override: profile_name_override
390                .map(Cow::Owned)
391                .or(self.profile_name_override),
392            ..self
393        }
394    }
395
396    /// Use the [default region chain](crate::default_provider::region) to set the
397    /// region for this configuration
398    ///
399    /// Note: the `env` and `fs` already set on this provider will be used when loading the default region.
400    pub async fn load_default_region(self) -> Self {
401        use crate::default_provider::region::DefaultRegionChain;
402        let provider_chain = DefaultRegionChain::builder().configure(&self).build();
403        self.with_region(provider_chain.region().await)
404    }
405
406    pub(crate) fn with_fs(self, fs: Fs) -> Self {
407        ProviderConfig {
408            parsed_profile: Default::default(),
409            fs,
410            ..self
411        }
412    }
413
414    pub(crate) fn with_env(self, env: Env) -> Self {
415        ProviderConfig {
416            parsed_profile: Default::default(),
417            env,
418            ..self
419        }
420    }
421
422    /// Override the time source for this configuration
423    pub fn with_time_source(self, time_source: impl TimeSource + 'static) -> Self {
424        ProviderConfig {
425            time_source: time_source.into_shared(),
426            ..self
427        }
428    }
429
430    /// Override the HTTP client for this configuration
431    pub fn with_http_client(self, http_client: impl HttpClient + 'static) -> Self {
432        ProviderConfig {
433            http_client: Some(http_client.into_shared()),
434            ..self
435        }
436    }
437
438    /// Override the sleep implementation for this configuration
439    pub fn with_sleep_impl(self, sleep_impl: impl AsyncSleep + 'static) -> Self {
440        ProviderConfig {
441            sleep_impl: Some(sleep_impl.into_shared()),
442            ..self
443        }
444    }
445
446    /// Override the retry config for this configuration
447    ///
448    /// This is honored by the inner clients (e.g. STS, SSO) used by credential providers in the
449    /// default chain.
450    ///
451    /// Note: this value is consumed while building the provider and is **not** reflected in the
452    /// outer client's configuration. When such a provider is passed to the config loader's
453    /// `credentials_provider(...)`, the identity cache derives its `load_timeout` from the outer
454    /// client's `RetryConfig`, so it cannot see the retry count set here. If this provider retries
455    /// more than the outer client, set the identity cache's `load_timeout` explicitly to avoid
456    /// cutting credential resolution short.
457    pub fn with_retry_config(self, retry_config: RetryConfig) -> Self {
458        ProviderConfig {
459            retry_config: Some(retry_config),
460            ..self
461        }
462    }
463
464    /// Override the timeout config for this configuration
465    ///
466    /// This is honored by the inner clients (e.g. STS, SSO) used by credential providers in the
467    /// default chain, allowing a caller to explicitly control credential-resolution timeouts
468    /// independently of the outer service client.
469    ///
470    /// Note: like [`with_retry_config`](Self::with_retry_config), this value is consumed while
471    /// building the provider and is **not** reflected in the outer client's configuration. When
472    /// such a provider is passed to the config loader's `credentials_provider(...)`, the identity
473    /// cache derives its `load_timeout` from the outer client's `TimeoutConfig` (the connect and
474    /// operation-attempt timeouts), so it cannot see the timeouts set here. If this provider uses
475    /// longer timeouts than the outer client, set the identity cache's `load_timeout` explicitly
476    /// to avoid cutting credential resolution short.
477    pub fn with_timeout_config(self, timeout_config: TimeoutConfig) -> Self {
478        ProviderConfig {
479            timeout_config: Some(timeout_config),
480            ..self
481        }
482    }
483}
484
485#[cfg(test)]
486mod test {
487    use super::ProviderConfig;
488    use aws_smithy_types::retry::RetryConfig;
489    use aws_smithy_types::timeout::TimeoutConfig;
490    use std::time::Duration;
491
492    // The inner clients (STS, SSO, ...) used by the default chain are built from
493    // `client_config()`, so this test checks that BOTH retry and timeout set on a
494    // `ProviderConfig` are threaded into the `SdkConfig` those inner clients consume.
495    #[test]
496    fn client_config_threads_retry_and_timeout() {
497        let timeout = TimeoutConfig::builder()
498            .operation_timeout(Duration::from_secs(3))
499            .connect_timeout(Duration::from_secs(1))
500            .build();
501        let conf = ProviderConfig::empty()
502            .with_retry_config(RetryConfig::standard().with_max_attempts(7))
503            .with_timeout_config(timeout.clone());
504
505        let sdk_config = conf.client_config();
506
507        assert_eq!(
508            7,
509            sdk_config
510                .retry_config()
511                .expect("retry config threaded to inner client config")
512                .max_attempts()
513        );
514        assert_eq!(
515            Some(&timeout),
516            sdk_config.timeout_config(),
517            "timeout config threaded to inner client config"
518        );
519    }
520
521    // When no timeout is configured, `client_config()` should leave timeout unset (inner clients
522    // fall back to their own defaults) while retry still defaults to standard.
523    #[test]
524    fn client_config_without_timeout_leaves_it_unset() {
525        let sdk_config = ProviderConfig::empty().client_config();
526        assert!(sdk_config.timeout_config().is_none());
527        assert_eq!(3, sdk_config.retry_config().unwrap().max_attempts());
528    }
529}