Skip to main content

aws_config/profile/
credentials.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Profile File Based Credential Providers
7//!
8//! Profile file based providers combine two pieces:
9//!
10//! 1. Parsing and resolution of the assume role chain
11//! 2. A user-modifiable hashmap of provider name to provider.
12//!
13//! Profile file based providers first determine the chain of providers that will be used to load
14//! credentials. After determining and validating this chain, a `Vec` of providers will be created.
15//!
16//! Each subsequent provider will provide boostrap providers to the next provider in order to load
17//! the final credentials.
18//!
19//! This module contains two sub modules:
20//! - `repr` which contains an abstract representation of a provider chain and the logic to
21//!   build it from `~/.aws/credentials` and `~/.aws/config`.
22//! - `exec` which contains a chain representation of providers to implement passing bootstrapped credentials
23//!   through a series of providers.
24
25use crate::profile::cell::ErrorTakingOnceCell;
26#[allow(deprecated)]
27use crate::profile::profile_file::ProfileFiles;
28use crate::profile::Profile;
29use crate::profile::ProfileFileLoadError;
30use crate::provider_config::ProviderConfig;
31use aws_credential_types::credential_feature::AwsCredentialFeature;
32use aws_credential_types::{
33    provider::{self, error::CredentialsError, future, ProvideCredentials},
34    Credentials,
35};
36use aws_smithy_types::error::display::DisplayErrorContext;
37use std::borrow::Cow;
38use std::collections::HashMap;
39use std::error::Error;
40use std::fmt::{Display, Formatter};
41use std::sync::Arc;
42use tracing::Instrument;
43
44mod exec;
45pub(crate) mod repr;
46
47/// AWS Profile based credentials provider
48///
49/// This credentials provider will load credentials from `~/.aws/config` and `~/.aws/credentials`.
50/// The locations of these files are configurable via environment variables, see [below](#location-of-profile-files).
51///
52/// Generally, this will be constructed via the default provider chain, however, it can be manually
53/// constructed with the builder:
54/// ```rust,no_run
55/// use aws_config::profile::ProfileFileCredentialsProvider;
56/// let provider = ProfileFileCredentialsProvider::builder().build();
57/// ```
58///
59/// _Note: Profile providers, when called, will load and parse the profile from the file system
60/// only once. Parsed file contents will be cached indefinitely._
61///
62/// This provider supports several different credentials formats:
63/// ### Credentials defined explicitly within the file
64/// ```ini
65/// [default]
66/// aws_access_key_id = 123
67/// aws_secret_access_key = 456
68/// ```
69///
70/// ### Assume Role Credentials loaded from a credential source
71/// ```ini
72/// [default]
73/// role_arn = arn:aws:iam::123456789:role/RoleA
74/// credential_source = Environment
75/// ```
76///
77/// NOTE: Currently only the `Environment` credential source is supported although it is possible to
78/// provide custom sources:
79/// ```no_run
80/// use aws_credential_types::provider::{self, future, ProvideCredentials};
81/// use aws_config::profile::ProfileFileCredentialsProvider;
82/// #[derive(Debug)]
83/// struct MyCustomProvider;
84/// impl MyCustomProvider {
85///     async fn load_credentials(&self) -> provider::Result {
86///         todo!()
87///     }
88/// }
89///
90/// impl ProvideCredentials for MyCustomProvider {
91///   fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials where Self: 'a {
92///         future::ProvideCredentials::new(self.load_credentials())
93///     }
94/// }
95/// # if cfg!(feature = "default-https-client") {
96/// let provider = ProfileFileCredentialsProvider::builder()
97///     .with_custom_provider("Custom", MyCustomProvider)
98///     .build();
99/// }
100/// ```
101///
102/// ### Assume role credentials from a source profile
103/// ```ini
104/// [default]
105/// role_arn = arn:aws:iam::123456789:role/RoleA
106/// source_profile = base
107///
108/// [profile base]
109/// aws_access_key_id = 123
110/// aws_secret_access_key = 456
111/// ```
112///
113/// Other more complex configurations are possible, consult `test-data/assume-role-tests.json`.
114///
115/// ### Credentials loaded from an external process
116/// ```ini
117/// [default]
118/// credential_process = /opt/bin/awscreds-custom --username helen
119/// ```
120///
121/// An external process can be used to provide credentials.
122///
123/// ### Loading Credentials from SSO
124/// ```ini
125/// [default]
126/// sso_start_url = https://example.com/start
127/// sso_region = us-east-2
128/// sso_account_id = 123456789011
129/// sso_role_name = readOnly
130/// region = us-west-2
131/// ```
132///
133/// SSO can also be used as a source profile for assume role chains.
134///
135/// ### Credentials from a console session
136///
137/// An existing AWS Console session can be used to provide credentials.
138///
139/// ```ini
140/// [default]
141/// login_session = arn:aws:iam::0123456789012:user/Admin
142/// ```
143///
144#[doc = include_str!("location_of_profile_files.md")]
145#[derive(Debug)]
146pub struct ProfileFileCredentialsProvider {
147    config: Arc<Config>,
148    inner_provider: ErrorTakingOnceCell<ChainProvider, CredentialsError>,
149}
150
151#[derive(Debug)]
152struct Config {
153    factory: exec::named::NamedProviderFactory,
154    provider_config: ProviderConfig,
155}
156
157impl ProfileFileCredentialsProvider {
158    /// Builder for this credentials provider
159    pub fn builder() -> Builder {
160        Builder::default()
161    }
162
163    async fn load_credentials(&self) -> provider::Result {
164        // The inner provider needs to be cached across successive calls to load_credentials
165        // since the base providers can potentially have information cached in their instances.
166        // For example, the SsoCredentialsProvider maintains an in-memory expiring token cache.
167        let inner_provider = self
168            .inner_provider
169            .get_or_init(
170                {
171                    let config = self.config.clone();
172                    move || async move {
173                        match build_provider_chain(config.clone()).await {
174                            Ok(chain) => Ok(ChainProvider {
175                                config: config.clone(),
176                                chain: Some(Arc::new(chain)),
177                            }),
178                            Err(err) => match err {
179                                ProfileFileError::NoProfilesDefined
180                                | ProfileFileError::ProfileDidNotContainCredentials { .. } => {
181                                    Ok(ChainProvider {
182                                        config: config.clone(),
183                                        chain: None,
184                                    })
185                                }
186                                ProfileFileError::MissingProfile { .. }
187                                | ProfileFileError::TokenProviderConfig { .. } => {
188                                    Err(CredentialsError::not_loaded(format!("{}", &err)))
189                                }
190                                _ => Err(CredentialsError::invalid_configuration(format!(
191                                    "ProfileFile provider could not be built: {}",
192                                    &err
193                                ))),
194                            },
195                        }
196                    }
197                },
198                CredentialsError::unhandled(
199                    "profile file credentials provider initialization error already taken",
200                ),
201            )
202            .await?;
203        inner_provider.provide_credentials().await.map(|mut creds| {
204            creds
205                .get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
206                .push(AwsCredentialFeature::CredentialsProfile);
207            creds
208        })
209    }
210}
211
212impl ProvideCredentials for ProfileFileCredentialsProvider {
213    fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
214    where
215        Self: 'a,
216    {
217        future::ProvideCredentials::new(self.load_credentials())
218    }
219}
220
221/// An Error building a Credential source from an AWS Profile
222#[derive(Debug)]
223#[non_exhaustive]
224pub enum ProfileFileError {
225    /// The profile was not a valid AWS profile
226    #[non_exhaustive]
227    InvalidProfile(ProfileFileLoadError),
228
229    /// No profiles existed (the profile was empty)
230    #[non_exhaustive]
231    NoProfilesDefined,
232
233    /// The profile did not contain any credential information
234    #[non_exhaustive]
235    ProfileDidNotContainCredentials {
236        /// The name of the profile
237        profile: String,
238    },
239
240    /// The profile contained an infinite loop of `source_profile` references
241    #[non_exhaustive]
242    CredentialLoop {
243        /// Vec of profiles leading to the loop
244        profiles: Vec<String>,
245        /// The next profile that caused the loop
246        next: String,
247    },
248
249    /// The profile was missing a credential source
250    #[non_exhaustive]
251    MissingCredentialSource {
252        /// The name of the profile
253        profile: String,
254        /// Error message
255        message: Cow<'static, str>,
256    },
257    /// The profile contained an invalid credential source
258    #[non_exhaustive]
259    InvalidCredentialSource {
260        /// The name of the profile
261        profile: String,
262        /// Error message
263        message: Cow<'static, str>,
264    },
265    /// The profile referred to a another profile by name that was not defined
266    #[non_exhaustive]
267    MissingProfile {
268        /// The name of the profile
269        profile: String,
270        /// Error message
271        message: Cow<'static, str>,
272    },
273    /// The profile referred to `credential_source` that was not defined
274    #[non_exhaustive]
275    UnknownProvider {
276        /// The name of the provider
277        name: String,
278    },
279
280    /// Feature not enabled
281    #[non_exhaustive]
282    FeatureNotEnabled {
283        /// The feature or comma delimited list of features that must be enabled
284        feature: Cow<'static, str>,
285        /// Additional information about the missing feature
286        message: Option<Cow<'static, str>>,
287    },
288
289    /// Missing sso-session section in config
290    #[non_exhaustive]
291    MissingSsoSession {
292        /// The name of the profile that specified `sso_session`
293        profile: String,
294        /// SSO session name
295        sso_session: String,
296    },
297
298    /// Invalid SSO configuration
299    #[non_exhaustive]
300    InvalidSsoConfig {
301        /// The name of the profile that the error originates in
302        profile: String,
303        /// Error message
304        message: Cow<'static, str>,
305    },
306
307    /// Profile is intended to be used in the token provider chain rather
308    /// than in the credentials chain.
309    #[non_exhaustive]
310    TokenProviderConfig {},
311}
312
313impl ProfileFileError {
314    fn missing_field(profile: &Profile, field: &'static str) -> Self {
315        ProfileFileError::MissingProfile {
316            profile: profile.name().to_string(),
317            message: format!("`{field}` was missing").into(),
318        }
319    }
320}
321
322impl Error for ProfileFileError {
323    fn source(&self) -> Option<&(dyn Error + 'static)> {
324        match self {
325            ProfileFileError::InvalidProfile(err) => Some(err),
326            _ => None,
327        }
328    }
329}
330
331impl Display for ProfileFileError {
332    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
333        match self {
334            ProfileFileError::InvalidProfile(err) => {
335                write!(f, "invalid profile: {err}")
336            }
337            ProfileFileError::CredentialLoop { profiles, next } => write!(
338                f,
339                "profile formed an infinite loop. first we loaded {profiles:?}, \
340            then attempted to reload {next}",
341            ),
342            ProfileFileError::MissingCredentialSource { profile, message } => {
343                write!(f, "missing credential source in `{profile}`: {message}")
344            }
345            ProfileFileError::InvalidCredentialSource { profile, message } => {
346                write!(f, "invalid credential source in `{profile}`: {message}")
347            }
348            ProfileFileError::MissingProfile { profile, message } => {
349                write!(f, "profile `{profile}` was not defined: {message}")
350            }
351            ProfileFileError::UnknownProvider { name } => write!(
352                f,
353                "profile referenced `{name}` provider but that provider is not supported",
354            ),
355            ProfileFileError::NoProfilesDefined => write!(f, "No profiles were defined"),
356            ProfileFileError::ProfileDidNotContainCredentials { profile } => write!(
357                f,
358                "profile `{profile}` did not contain credential information"
359            ),
360            ProfileFileError::FeatureNotEnabled { feature, message } => {
361                let message = message.as_deref().unwrap_or_default();
362                write!(
363                    f,
364                    "This behavior requires following cargo feature(s) enabled: {feature}. {message}",
365                )
366            }
367            ProfileFileError::MissingSsoSession {
368                profile,
369                sso_session,
370            } => {
371                write!(f, "sso-session named `{sso_session}` (referenced by profile `{profile}`) was not found")
372            }
373            ProfileFileError::InvalidSsoConfig { profile, message } => {
374                write!(f, "profile `{profile}` has invalid SSO config: {message}")
375            }
376            ProfileFileError::TokenProviderConfig { .. } => {
377                write!(
378                    f,
379                    "selected profile will resolve an access token instead of credentials \
380                     since it doesn't have `sso_account_id` and `sso_role_name` set. Specify both \
381                     `sso_account_id` and `sso_role_name` to let this profile resolve credentials."
382                )
383            }
384        }
385    }
386}
387
388/// Builder for [`ProfileFileCredentialsProvider`]
389#[derive(Debug, Default)]
390pub struct Builder {
391    provider_config: Option<ProviderConfig>,
392    profile_override: Option<String>,
393    #[allow(deprecated)]
394    profile_files: Option<ProfileFiles>,
395    custom_providers: HashMap<Cow<'static, str>, Arc<dyn ProvideCredentials>>,
396}
397
398impl Builder {
399    /// Override the configuration for the [`ProfileFileCredentialsProvider`]
400    ///
401    /// # Examples
402    ///
403    /// ```no_run
404    /// # async fn test() {
405    /// use aws_config::profile::ProfileFileCredentialsProvider;
406    /// use aws_config::provider_config::ProviderConfig;
407    /// let provider = ProfileFileCredentialsProvider::builder()
408    ///     .configure(&ProviderConfig::with_default_region().await)
409    ///     .build();
410    /// # }
411    /// ```
412    pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
413        self.provider_config = Some(provider_config.clone());
414        self
415    }
416
417    /// Adds a custom credential source
418    ///
419    /// # Examples
420    ///
421    /// ```no_run
422    /// use aws_credential_types::provider::{self, future, ProvideCredentials};
423    /// use aws_config::profile::ProfileFileCredentialsProvider;
424    /// #[derive(Debug)]
425    /// struct MyCustomProvider;
426    /// impl MyCustomProvider {
427    ///     async fn load_credentials(&self) -> provider::Result {
428    ///         todo!()
429    ///     }
430    /// }
431    ///
432    /// impl ProvideCredentials for MyCustomProvider {
433    ///   fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials where Self: 'a {
434    ///         future::ProvideCredentials::new(self.load_credentials())
435    ///     }
436    /// }
437    ///
438    /// # if cfg!(feature = "rustls") {
439    /// let provider = ProfileFileCredentialsProvider::builder()
440    ///     .with_custom_provider("Custom", MyCustomProvider)
441    ///     .build();
442    /// # }
443    /// ```
444    pub fn with_custom_provider(
445        mut self,
446        name: impl Into<Cow<'static, str>>,
447        provider: impl ProvideCredentials + 'static,
448    ) -> Self {
449        self.custom_providers
450            .insert(name.into(), Arc::new(provider));
451        self
452    }
453
454    /// Override the profile name used by the [`ProfileFileCredentialsProvider`]
455    pub fn profile_name(mut self, profile_name: impl Into<String>) -> Self {
456        self.profile_override = Some(profile_name.into());
457        self
458    }
459
460    /// Set the profile file that should be used by the [`ProfileFileCredentialsProvider`]
461    #[allow(deprecated)]
462    pub fn profile_files(mut self, profile_files: ProfileFiles) -> Self {
463        self.profile_files = Some(profile_files);
464        self
465    }
466
467    /// Builds a [`ProfileFileCredentialsProvider`]
468    pub fn build(self) -> ProfileFileCredentialsProvider {
469        let build_span = tracing::debug_span!("build_profile_file_credentials_provider");
470        let _enter = build_span.enter();
471        let conf = self
472            .provider_config
473            .unwrap_or_default()
474            .with_profile_config(self.profile_files, self.profile_override);
475        let mut named_providers = self.custom_providers.clone();
476        named_providers
477            .entry("Environment".into())
478            .or_insert_with(|| {
479                Arc::new(crate::environment::credentials::EnvironmentVariableCredentialsProvider::new_with_env(
480                    conf.env(),
481                ))
482            });
483
484        named_providers
485            .entry("Ec2InstanceMetadata".into())
486            .or_insert_with(|| {
487                Arc::new(
488                    crate::imds::credentials::ImdsCredentialsProvider::builder()
489                        .configure(&conf)
490                        .build(),
491                )
492            });
493
494        named_providers
495            .entry("EcsContainer".into())
496            .or_insert_with(|| {
497                Arc::new(
498                    crate::ecs::EcsCredentialsProvider::builder()
499                        .configure(&conf)
500                        .build(),
501                )
502            });
503        let factory = exec::named::NamedProviderFactory::new(named_providers);
504
505        ProfileFileCredentialsProvider {
506            config: Arc::new(Config {
507                factory,
508                provider_config: conf,
509            }),
510            inner_provider: ErrorTakingOnceCell::new(),
511        }
512    }
513}
514
515async fn build_provider_chain(
516    config: Arc<Config>,
517) -> Result<exec::ProviderChain, ProfileFileError> {
518    let profile_set = config
519        .provider_config
520        .try_profile()
521        .await
522        .map_err(|parse_err| ProfileFileError::InvalidProfile(parse_err.clone()))?;
523    let repr = repr::resolve_chain(profile_set)?;
524    tracing::info!(chain = ?repr, "constructed abstract provider from config file");
525    exec::ProviderChain::from_repr(&config.provider_config, repr, &config.factory)
526}
527
528#[derive(Debug)]
529struct ChainProvider {
530    config: Arc<Config>,
531    chain: Option<Arc<exec::ProviderChain>>,
532}
533
534impl ChainProvider {
535    async fn provide_credentials(&self) -> Result<Credentials, CredentialsError> {
536        // Can't borrow `self` across an await point, or else we lose `Send` on the returned future
537        let config = self.config.clone();
538        let chain = self.chain.clone();
539
540        if let Some(chain) = chain {
541            let mut creds = match chain
542                .base()
543                .provide_credentials()
544                .instrument(tracing::debug_span!("load_base_credentials"))
545                .await
546            {
547                Ok(creds) => {
548                    tracing::info!(creds = ?creds, "loaded base credentials");
549                    creds
550                }
551                Err(e) => {
552                    tracing::warn!(error = %DisplayErrorContext(&e), "failed to load base credentials");
553                    return Err(CredentialsError::provider_error(e));
554                }
555            };
556
557            // we want to create `SdkConfig` _after_ we have resolved the profile or else
558            // we won't get things like `service_config()` set appropriately.
559            //
560            // Resolve FIPS/dual-stack from the profile if they weren't set explicitly on the
561            // `ProviderConfig` (e.g. when `ProfileFileCredentialsProvider` is constructed
562            // directly via its builder, bypassing `ConfigLoader::load`).
563            let mut sdk_config_builder = config.provider_config.client_config().into_builder();
564            if config.provider_config.use_fips().is_none() {
565                sdk_config_builder.set_use_fips(
566                    crate::default_provider::use_fips::use_fips_provider(&config.provider_config)
567                        .await,
568                );
569            }
570            if config.provider_config.use_dual_stack().is_none() {
571                sdk_config_builder.set_use_dual_stack(
572                    crate::default_provider::use_dual_stack::use_dual_stack_provider(
573                        &config.provider_config,
574                    )
575                    .await,
576                );
577            }
578            let sdk_config = sdk_config_builder.build();
579            for provider in chain.chain().iter() {
580                let next_creds = provider
581                    .credentials(creds, &sdk_config)
582                    .instrument(tracing::debug_span!("load_assume_role", provider = ?provider))
583                    .await;
584                match next_creds {
585                    Ok(next_creds) => {
586                        tracing::info!(creds = ?next_creds, "loaded assume role credentials");
587                        creds = next_creds
588                    }
589                    Err(e) => {
590                        tracing::warn!(provider = ?provider, "failed to load assume role credentials");
591                        return Err(CredentialsError::provider_error(e));
592                    }
593                }
594            }
595            Ok(creds)
596        } else {
597            Err(CredentialsError::not_loaded_no_source())
598        }
599    }
600}
601
602#[cfg(test)]
603mod test {
604    use crate::profile::credentials::Builder;
605    use aws_credential_types::provider::ProvideCredentials;
606
607    macro_rules! make_test {
608        ($name: ident) => {
609            #[tokio::test]
610            async fn $name() {
611                let _ = crate::test_case::TestEnvironment::from_dir(
612                    concat!("./test-data/profile-provider/", stringify!($name)),
613                    crate::test_case::test_credentials_provider(|config| async move {
614                        Builder::default()
615                            .configure(&config)
616                            .build()
617                            .provide_credentials()
618                            .await
619                    }),
620                )
621                .await
622                .unwrap()
623                .execute()
624                .await;
625            }
626        };
627    }
628
629    make_test!(e2e_assume_role);
630    make_test!(e2e_fips_and_dual_stack_sts);
631    make_test!(empty_config);
632    make_test!(retry_on_error);
633    make_test!(invalid_config);
634    make_test!(region_override);
635    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is disabled on Windows because it uses Unix-style paths
636    #[cfg(all(feature = "credentials-process", not(windows)))]
637    make_test!(credential_process);
638    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is disabled on Windows because it uses Unix-style paths
639    #[cfg(all(feature = "credentials-process", not(windows)))]
640    make_test!(credential_process_failure);
641    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is disabled on Windows because it uses Unix-style paths
642    #[cfg(all(feature = "credentials-process", not(windows)))]
643    make_test!(credential_process_account_id_fallback);
644    #[cfg(feature = "credentials-process")]
645    make_test!(credential_process_invalid);
646    #[cfg(feature = "sso")]
647    make_test!(sso_credentials);
648    #[cfg(feature = "sso")]
649    make_test!(invalid_sso_credentials_config);
650    #[cfg(feature = "sso")]
651    make_test!(sso_override_global_env_url);
652    #[cfg(feature = "sso")]
653    make_test!(sso_token);
654
655    make_test!(assume_role_override_global_env_url);
656    make_test!(assume_role_override_service_env_url);
657    make_test!(assume_role_override_global_profile_url);
658    make_test!(assume_role_override_service_profile_url);
659
660    /// Regression test for https://github.com/smithy-lang/smithy-rs/issues/4614:
661    /// when building `ProfileFileCredentialsProvider` directly via its builder (bypassing
662    /// `ConfigLoader::load`), the profile's `use_fips_endpoint` and `use_dualstack_endpoint`
663    /// settings must still propagate to the internal STS client used for assume-role.
664    #[tokio::test]
665    async fn profile_use_fips_endpoint_propagates_to_sts_client() {
666        #[allow(deprecated)]
667        use crate::profile::profile_file::{ProfileFileKind, ProfileFiles};
668        use crate::provider_config::ProviderConfig;
669        use aws_smithy_http_client::test_util::capture_request;
670        use aws_smithy_types::body::SdkBody;
671        use aws_types::os_shim_internal::Env;
672        use aws_types::region::Region;
673
674        let (http_client, rx) = capture_request(Some(
675            http::Response::builder()
676                .status(200)
677                .body(SdkBody::from(
678                    r#"<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
679                        <AssumeRoleResult>
680                          <Credentials>
681                            <AccessKeyId>ASIAFAKEKEY</AccessKeyId>
682                            <SecretAccessKey>fakesecret</SecretAccessKey>
683                            <SessionToken>fakesession</SessionToken>
684                            <Expiration>2099-01-01T00:00:00Z</Expiration>
685                          </Credentials>
686                        </AssumeRoleResult>
687                       </AssumeRoleResponse>"#,
688                ))
689                .unwrap(),
690        ));
691
692        let provider_config = ProviderConfig::empty()
693            .with_env(Env::from_slice(&[]))
694            .with_region(Some(Region::new("us-east-1")))
695            .with_http_client(http_client);
696
697        #[allow(deprecated)]
698        let profile_files = ProfileFiles::builder()
699            .with_contents(
700                ProfileFileKind::Config,
701                "[profile fips-test]\nuse_fips_endpoint = true\nuse_dualstack_endpoint = true\nregion = us-east-1\nrole_arn = arn:aws:iam::123456789012:role/FakeTestRole\nsource_profile = source\n\n[profile source]\naws_access_key_id = AKIAIOSFODNN7EXAMPLE\naws_secret_access_key = secret\n",
702            )
703            .build();
704
705        let provider = Builder::default()
706            .configure(&provider_config)
707            .profile_files(profile_files)
708            .profile_name("fips-test")
709            .build();
710
711        let _ = provider.provide_credentials().await;
712        let request = rx.expect_request();
713        let uri = request.uri().to_string();
714        // FIPS + dual-stack STS endpoint has the form `sts-fips.<region>.api.aws`.
715        assert!(
716            uri.contains("sts-fips.us-east-1.api.aws"),
717            "expected STS FIPS + dual-stack endpoint, got: {uri}"
718        );
719    }
720}
721
722#[cfg(all(test, feature = "sso"))]
723mod sso_tests {
724    use crate::{profile::credentials::Builder, provider_config::ProviderConfig};
725    use aws_credential_types::credential_feature::AwsCredentialFeature;
726    use aws_credential_types::provider::ProvideCredentials;
727    use aws_sdk_sso::config::RuntimeComponents;
728    use aws_smithy_runtime_api::client::{
729        http::{
730            HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings,
731            SharedHttpConnector,
732        },
733        orchestrator::{HttpRequest, HttpResponse},
734    };
735    use aws_smithy_types::body::SdkBody;
736    use aws_types::os_shim_internal::{Env, Fs};
737    use std::collections::HashMap;
738
739    #[derive(Debug)]
740    struct ClientInner {
741        expected_token: &'static str,
742    }
743    impl HttpConnector for ClientInner {
744        fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
745            assert_eq!(
746                self.expected_token,
747                request.headers().get("x-amz-sso_bearer_token").unwrap()
748            );
749            HttpConnectorFuture::ready(Ok(HttpResponse::new(
750                    200.try_into().unwrap(),
751                    SdkBody::from("{\"roleCredentials\":{\"accessKeyId\":\"ASIARTESTID\",\"secretAccessKey\":\"TESTSECRETKEY\",\"sessionToken\":\"TESTSESSIONTOKEN\",\"expiration\": 1651516560000}}"),
752                )))
753        }
754    }
755    #[derive(Debug)]
756    struct Client {
757        inner: SharedHttpConnector,
758    }
759    impl Client {
760        fn new(expected_token: &'static str) -> Self {
761            Self {
762                inner: SharedHttpConnector::new(ClientInner { expected_token }),
763            }
764        }
765    }
766    impl HttpClient for Client {
767        fn http_connector(
768            &self,
769            _settings: &HttpConnectorSettings,
770            _components: &RuntimeComponents,
771        ) -> SharedHttpConnector {
772            self.inner.clone()
773        }
774    }
775
776    fn create_test_fs() -> Fs {
777        Fs::from_map({
778            let mut map = HashMap::new();
779            map.insert(
780                "/home/.aws/config".to_string(),
781                br#"
782[profile default]
783sso_session = dev
784sso_account_id = 012345678901
785sso_role_name = SampleRole
786region = us-east-1
787
788[sso-session dev]
789sso_region = us-east-1
790sso_start_url = https://d-abc123.awsapps.com/start
791                "#
792                .to_vec(),
793            );
794            map.insert(
795                "/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json".to_string(),
796                br#"
797                {
798                    "accessToken": "secret-access-token",
799                    "expiresAt": "2199-11-14T04:05:45Z",
800                    "refreshToken": "secret-refresh-token",
801                    "clientId": "ABCDEFG323242423121312312312312312",
802                    "clientSecret": "ABCDE123",
803                    "registrationExpiresAt": "2199-03-06T19:53:17Z",
804                    "region": "us-east-1",
805                    "startUrl": "https://d-abc123.awsapps.com/start"
806                }
807                "#
808                .to_vec(),
809            );
810            map
811        })
812    }
813
814    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is ignored on Windows because it uses Unix-style paths
815    #[cfg_attr(windows, ignore)]
816    // In order to preserve the SSO token cache, the inner provider must only
817    // be created once, rather than once per credential resolution.
818    #[tokio::test]
819    async fn create_inner_provider_exactly_once() {
820        let fs = create_test_fs();
821
822        let provider_config = ProviderConfig::empty()
823            .with_fs(fs.clone())
824            .with_env(Env::from_slice(&[("HOME", "/home")]))
825            .with_http_client(Client::new("secret-access-token"));
826        let provider = Builder::default().configure(&provider_config).build();
827
828        let first_creds = provider.provide_credentials().await.unwrap();
829
830        // Write to the token cache with an access token that won't match the fake client's
831        // expected access token, and thus, won't return SSO credentials.
832        fs.write(
833            "/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json",
834            r#"
835            {
836                "accessToken": "NEW!!secret-access-token",
837                "expiresAt": "2199-11-14T04:05:45Z",
838                "refreshToken": "secret-refresh-token",
839                "clientId": "ABCDEFG323242423121312312312312312",
840                "clientSecret": "ABCDE123",
841                "registrationExpiresAt": "2199-03-06T19:53:17Z",
842                "region": "us-east-1",
843                "startUrl": "https://d-abc123.awsapps.com/start"
844            }
845            "#,
846        )
847        .await
848        .unwrap();
849
850        // Loading credentials will still work since the SSOTokenProvider should have only
851        // been created once, and thus, the correct token is still in an in-memory cache.
852        let second_creds = provider
853            .provide_credentials()
854            .await
855            .expect("used cached token instead of loading from the file system");
856        assert_eq!(first_creds, second_creds);
857
858        // Now create a new provider, which should use the new cached token value from the file system
859        // since it won't have the in-memory cache. We do this just to verify that the FS mutation above
860        // actually worked correctly.
861        let provider_config = ProviderConfig::empty()
862            .with_fs(fs.clone())
863            .with_env(Env::from_slice(&[("HOME", "/home")]))
864            .with_http_client(Client::new("NEW!!secret-access-token"));
865        let provider = Builder::default().configure(&provider_config).build();
866        let third_creds = provider.provide_credentials().await.unwrap();
867        assert_eq!(second_creds, third_creds);
868    }
869
870    #[cfg_attr(windows, ignore)]
871    #[tokio::test]
872    async fn credential_feature() {
873        let fs = create_test_fs();
874
875        let provider_config = ProviderConfig::empty()
876            .with_fs(fs.clone())
877            .with_env(Env::from_slice(&[("HOME", "/home")]))
878            .with_http_client(Client::new("secret-access-token"));
879        let provider = Builder::default().configure(&provider_config).build();
880
881        let creds = provider.provide_credentials().await.unwrap();
882
883        assert_eq!(
884            &vec![
885                AwsCredentialFeature::CredentialsSso,
886                AwsCredentialFeature::CredentialsProfile
887            ],
888            creds.get_property::<Vec<AwsCredentialFeature>>().unwrap()
889        )
890    }
891}
892
893#[cfg(all(test, feature = "credentials-login"))]
894mod login_tests {
895    use crate::provider_config::ProviderConfig;
896    use aws_credential_types::provider::error::CredentialsError;
897    use aws_credential_types::provider::ProvideCredentials;
898    use aws_sdk_signin::config::RuntimeComponents;
899    use aws_smithy_runtime_api::client::{
900        http::{
901            HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings,
902            SharedHttpConnector,
903        },
904        orchestrator::{HttpRequest, HttpResponse},
905    };
906    use aws_smithy_types::body::SdkBody;
907    use aws_types::os_shim_internal::{Env, Fs};
908    use std::collections::HashMap;
909    use std::sync::atomic::{AtomicUsize, Ordering};
910    use std::sync::Arc;
911
912    #[derive(Debug, Clone)]
913    struct TestClientInner {
914        call_count: Arc<AtomicUsize>,
915        response: Option<&'static str>,
916    }
917
918    impl HttpConnector for TestClientInner {
919        fn call(&self, _request: HttpRequest) -> HttpConnectorFuture {
920            self.call_count.fetch_add(1, Ordering::SeqCst);
921            if let Some(response) = self.response {
922                HttpConnectorFuture::ready(Ok(HttpResponse::new(
923                    200.try_into().unwrap(),
924                    SdkBody::from(response),
925                )))
926            } else {
927                HttpConnectorFuture::ready(Ok(HttpResponse::new(
928                    500.try_into().unwrap(),
929                    SdkBody::from("{\"error\":\"server_error\"}"),
930                )))
931            }
932        }
933    }
934
935    #[derive(Debug, Clone)]
936    struct TestClient {
937        inner: SharedHttpConnector,
938        call_count: Arc<AtomicUsize>,
939    }
940
941    impl TestClient {
942        fn new_success() -> Self {
943            let call_count = Arc::new(AtomicUsize::new(0));
944            let response = r#"{
945                "accessToken": {
946                    "accessKeyId": "ASIARTESTID",
947                    "secretAccessKey": "TESTSECRETKEY",
948                    "sessionToken": "TESTSESSIONTOKEN"
949                },
950                "expiresIn": 3600,
951                "refreshToken": "new-refresh-token"
952            }"#;
953            let inner = TestClientInner {
954                call_count: call_count.clone(),
955                response: Some(response),
956            };
957            Self {
958                inner: SharedHttpConnector::new(inner),
959                call_count,
960            }
961        }
962
963        fn new_error() -> Self {
964            let call_count = Arc::new(AtomicUsize::new(0));
965            let inner = TestClientInner {
966                call_count: call_count.clone(),
967                response: None,
968            };
969            Self {
970                inner: SharedHttpConnector::new(inner),
971                call_count,
972            }
973        }
974
975        fn call_count(&self) -> usize {
976            self.call_count.load(Ordering::SeqCst)
977        }
978    }
979
980    impl HttpClient for TestClient {
981        fn http_connector(
982            &self,
983            _settings: &HttpConnectorSettings,
984            _components: &RuntimeComponents,
985        ) -> SharedHttpConnector {
986            self.inner.clone()
987        }
988    }
989
990    fn create_test_fs_unexpired() -> Fs {
991        Fs::from_map({
992            let mut map = HashMap::new();
993            map.insert(
994                "/home/.aws/config".to_string(),
995                br#"
996[profile default]
997login_session = arn:aws:iam::0123456789012:user/Admin
998region = us-east-1
999                "#
1000                .to_vec(),
1001            );
1002            map.insert(
1003                "/home/.aws/login/cache/36db1d138ff460920374e4c3d8e01f53f9f73537e89c88d639f68393df0e2726.json".to_string(),
1004                br#"{
1005                    "accessToken": {
1006                        "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
1007                        "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
1008                        "sessionToken": "session-token",
1009                        "accountId": "012345678901",
1010                        "expiresAt": "2199-12-25T21:30:00Z"
1011                    },
1012                    "tokenType": "aws_sigv4",
1013                    "refreshToken": "refresh-token-value",
1014                    "identityToken": "identity-token-value",
1015                    "clientId": "aws:signin:::cli/same-device",
1016                    "dpopKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIFDZHUzOG1Pzq+6F0mjMlOSp1syN9LRPBuHMoCFXTcXhoAoGCCqGSM49\nAwEHoUQDQgAE9qhj+KtcdHj1kVgwxWWWw++tqoh7H7UHs7oXh8jBbgF47rrYGC+t\ndjiIaHK3dBvvdE7MGj5HsepzLm3Kj91bqA==\n-----END EC PRIVATE KEY-----\n"
1017                }"#
1018                .to_vec(),
1019            );
1020            map
1021        })
1022    }
1023
1024    fn create_test_fs_expired() -> Fs {
1025        Fs::from_map({
1026            let mut map = HashMap::new();
1027            map.insert(
1028                "/home/.aws/config".to_string(),
1029                br#"
1030[profile default]
1031login_session = arn:aws:iam::0123456789012:user/Admin
1032region = us-east-1
1033                "#
1034                .to_vec(),
1035            );
1036            map.insert(
1037                "/home/.aws/login/cache/36db1d138ff460920374e4c3d8e01f53f9f73537e89c88d639f68393df0e2726.json".to_string(),
1038                br#"{
1039                    "accessToken": {
1040                        "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
1041                        "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
1042                        "sessionToken": "session-token",
1043                        "accountId": "012345678901",
1044                        "expiresAt": "2020-01-01T00:00:00Z"
1045                    },
1046                    "tokenType": "aws_sigv4",
1047                    "refreshToken": "refresh-token-value",
1048                    "identityToken": "identity-token-value",
1049                    "clientId": "aws:signin:::cli/same-device",
1050                    "dpopKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIFDZHUzOG1Pzq+6F0mjMlOSp1syN9LRPBuHMoCFXTcXhoAoGCCqGSM49\nAwEHoUQDQgAE9qhj+KtcdHj1kVgwxWWWw++tqoh7H7UHs7oXh8jBbgF47rrYGC+t\ndjiIaHK3dBvvdE7MGj5HsepzLm3Kj91bqA==\n-----END EC PRIVATE KEY-----\n"
1051                }"#
1052                .to_vec(),
1053            );
1054            map
1055        })
1056    }
1057
1058    #[cfg_attr(windows, ignore)]
1059    #[tokio::test]
1060    async fn unexpired_credentials_no_refresh() {
1061        let client = TestClient::new_success();
1062
1063        let provider_config = ProviderConfig::empty()
1064            .with_fs(create_test_fs_unexpired())
1065            .with_env(Env::from_slice(&[("HOME", "/home")]))
1066            .with_http_client(client.clone())
1067            .with_region(Some(aws_types::region::Region::new("us-east-1")));
1068
1069        let provider = crate::profile::credentials::Builder::default()
1070            .configure(&provider_config)
1071            .build();
1072
1073        let creds = provider.provide_credentials().await.unwrap();
1074        assert_eq!("AKIAIOSFODNN7EXAMPLE", creds.access_key_id());
1075        assert_eq!(0, client.call_count());
1076    }
1077
1078    #[cfg_attr(windows, ignore)]
1079    #[tokio::test]
1080    async fn expired_credentials_trigger_refresh() {
1081        let client = TestClient::new_success();
1082
1083        let provider_config = ProviderConfig::empty()
1084            .with_fs(create_test_fs_expired())
1085            .with_env(Env::from_slice(&[("HOME", "/home")]))
1086            .with_http_client(client.clone())
1087            .with_region(Some(aws_types::region::Region::new("us-east-1")));
1088
1089        let provider = crate::profile::credentials::Builder::default()
1090            .configure(&provider_config)
1091            .build();
1092
1093        let creds = provider.provide_credentials().await.unwrap();
1094        assert_eq!("ASIARTESTID", creds.access_key_id());
1095        assert_eq!(1, client.call_count());
1096    }
1097
1098    #[cfg_attr(windows, ignore)]
1099    #[tokio::test]
1100    async fn refresh_error_propagates() {
1101        let client = TestClient::new_error();
1102
1103        let provider_config = ProviderConfig::empty()
1104            .with_fs(create_test_fs_expired())
1105            .with_env(Env::from_slice(&[("HOME", "/home")]))
1106            .with_http_client(client)
1107            .with_region(Some(aws_types::region::Region::new("us-east-1")));
1108
1109        let provider = crate::profile::credentials::Builder::default()
1110            .configure(&provider_config)
1111            .build();
1112
1113        let err = provider
1114            .provide_credentials()
1115            .await
1116            .expect_err("should fail on refresh error");
1117
1118        match &err {
1119            CredentialsError::ProviderError(_) => {}
1120            _ => panic!("wrong error type"),
1121        }
1122    }
1123}