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                                _ => Err(CredentialsError::invalid_configuration(format!(
187                                    "ProfileFile provider could not be built: {}",
188                                    &err
189                                ))),
190                            },
191                        }
192                    }
193                },
194                CredentialsError::unhandled(
195                    "profile file credentials provider initialization error already taken",
196                ),
197            )
198            .await?;
199        inner_provider.provide_credentials().await.map(|mut creds| {
200            creds
201                .get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
202                .push(AwsCredentialFeature::CredentialsProfile);
203            creds
204        })
205    }
206}
207
208impl ProvideCredentials for ProfileFileCredentialsProvider {
209    fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
210    where
211        Self: 'a,
212    {
213        future::ProvideCredentials::new(self.load_credentials())
214    }
215}
216
217/// An Error building a Credential source from an AWS Profile
218#[derive(Debug)]
219#[non_exhaustive]
220pub enum ProfileFileError {
221    /// The profile was not a valid AWS profile
222    #[non_exhaustive]
223    InvalidProfile(ProfileFileLoadError),
224
225    /// No profiles existed (the profile was empty)
226    #[non_exhaustive]
227    NoProfilesDefined,
228
229    /// The profile did not contain any credential information
230    #[non_exhaustive]
231    ProfileDidNotContainCredentials {
232        /// The name of the profile
233        profile: String,
234    },
235
236    /// The profile contained an infinite loop of `source_profile` references
237    #[non_exhaustive]
238    CredentialLoop {
239        /// Vec of profiles leading to the loop
240        profiles: Vec<String>,
241        /// The next profile that caused the loop
242        next: String,
243    },
244
245    /// The profile was missing a credential source
246    #[non_exhaustive]
247    MissingCredentialSource {
248        /// The name of the profile
249        profile: String,
250        /// Error message
251        message: Cow<'static, str>,
252    },
253    /// The profile contained an invalid credential source
254    #[non_exhaustive]
255    InvalidCredentialSource {
256        /// The name of the profile
257        profile: String,
258        /// Error message
259        message: Cow<'static, str>,
260    },
261    /// The profile referred to a another profile by name that was not defined
262    #[non_exhaustive]
263    MissingProfile {
264        /// The name of the profile
265        profile: String,
266        /// Error message
267        message: Cow<'static, str>,
268    },
269    /// The profile referred to `credential_source` that was not defined
270    #[non_exhaustive]
271    UnknownProvider {
272        /// The name of the provider
273        name: String,
274    },
275
276    /// Feature not enabled
277    #[non_exhaustive]
278    FeatureNotEnabled {
279        /// The feature or comma delimited list of features that must be enabled
280        feature: Cow<'static, str>,
281        /// Additional information about the missing feature
282        message: Option<Cow<'static, str>>,
283    },
284
285    /// Missing sso-session section in config
286    #[non_exhaustive]
287    MissingSsoSession {
288        /// The name of the profile that specified `sso_session`
289        profile: String,
290        /// SSO session name
291        sso_session: String,
292    },
293
294    /// Invalid SSO configuration
295    #[non_exhaustive]
296    InvalidSsoConfig {
297        /// The name of the profile that the error originates in
298        profile: String,
299        /// Error message
300        message: Cow<'static, str>,
301    },
302
303    /// Profile is intended to be used in the token provider chain rather
304    /// than in the credentials chain.
305    #[non_exhaustive]
306    TokenProviderConfig {},
307}
308
309impl ProfileFileError {
310    fn missing_field(profile: &Profile, field: &'static str) -> Self {
311        ProfileFileError::MissingProfile {
312            profile: profile.name().to_string(),
313            message: format!("`{field}` was missing").into(),
314        }
315    }
316}
317
318impl Error for ProfileFileError {
319    fn source(&self) -> Option<&(dyn Error + 'static)> {
320        match self {
321            ProfileFileError::InvalidProfile(err) => Some(err),
322            _ => None,
323        }
324    }
325}
326
327impl Display for ProfileFileError {
328    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
329        match self {
330            ProfileFileError::InvalidProfile(err) => {
331                write!(f, "invalid profile: {err}")
332            }
333            ProfileFileError::CredentialLoop { profiles, next } => write!(
334                f,
335                "profile formed an infinite loop. first we loaded {profiles:?}, \
336            then attempted to reload {next}",
337            ),
338            ProfileFileError::MissingCredentialSource { profile, message } => {
339                write!(f, "missing credential source in `{profile}`: {message}")
340            }
341            ProfileFileError::InvalidCredentialSource { profile, message } => {
342                write!(f, "invalid credential source in `{profile}`: {message}")
343            }
344            ProfileFileError::MissingProfile { profile, message } => {
345                write!(f, "profile `{profile}` was not defined: {message}")
346            }
347            ProfileFileError::UnknownProvider { name } => write!(
348                f,
349                "profile referenced `{name}` provider but that provider is not supported",
350            ),
351            ProfileFileError::NoProfilesDefined => write!(f, "No profiles were defined"),
352            ProfileFileError::ProfileDidNotContainCredentials { profile } => write!(
353                f,
354                "profile `{profile}` did not contain credential information"
355            ),
356            ProfileFileError::FeatureNotEnabled { feature, message } => {
357                let message = message.as_deref().unwrap_or_default();
358                write!(
359                    f,
360                    "This behavior requires following cargo feature(s) enabled: {feature}. {message}",
361                )
362            }
363            ProfileFileError::MissingSsoSession {
364                profile,
365                sso_session,
366            } => {
367                write!(f, "sso-session named `{sso_session}` (referenced by profile `{profile}`) was not found")
368            }
369            ProfileFileError::InvalidSsoConfig { profile, message } => {
370                write!(f, "profile `{profile}` has invalid SSO config: {message}")
371            }
372            ProfileFileError::TokenProviderConfig { .. } => {
373                write!(
374                    f,
375                    "selected profile will resolve an access token instead of credentials \
376                     since it doesn't have `sso_account_id` and `sso_role_name` set. Specify both \
377                     `sso_account_id` and `sso_role_name` to let this profile resolve credentials."
378                )
379            }
380        }
381    }
382}
383
384/// Builder for [`ProfileFileCredentialsProvider`]
385#[derive(Debug, Default)]
386pub struct Builder {
387    provider_config: Option<ProviderConfig>,
388    profile_override: Option<String>,
389    #[allow(deprecated)]
390    profile_files: Option<ProfileFiles>,
391    custom_providers: HashMap<Cow<'static, str>, Arc<dyn ProvideCredentials>>,
392}
393
394impl Builder {
395    /// Override the configuration for the [`ProfileFileCredentialsProvider`]
396    ///
397    /// # Examples
398    ///
399    /// ```no_run
400    /// # async fn test() {
401    /// use aws_config::profile::ProfileFileCredentialsProvider;
402    /// use aws_config::provider_config::ProviderConfig;
403    /// let provider = ProfileFileCredentialsProvider::builder()
404    ///     .configure(&ProviderConfig::with_default_region().await)
405    ///     .build();
406    /// # }
407    /// ```
408    pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
409        self.provider_config = Some(provider_config.clone());
410        self
411    }
412
413    /// Adds a custom credential source
414    ///
415    /// # Examples
416    ///
417    /// ```no_run
418    /// use aws_credential_types::provider::{self, future, ProvideCredentials};
419    /// use aws_config::profile::ProfileFileCredentialsProvider;
420    /// #[derive(Debug)]
421    /// struct MyCustomProvider;
422    /// impl MyCustomProvider {
423    ///     async fn load_credentials(&self) -> provider::Result {
424    ///         todo!()
425    ///     }
426    /// }
427    ///
428    /// impl ProvideCredentials for MyCustomProvider {
429    ///   fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials where Self: 'a {
430    ///         future::ProvideCredentials::new(self.load_credentials())
431    ///     }
432    /// }
433    ///
434    /// # if cfg!(feature = "rustls") {
435    /// let provider = ProfileFileCredentialsProvider::builder()
436    ///     .with_custom_provider("Custom", MyCustomProvider)
437    ///     .build();
438    /// # }
439    /// ```
440    pub fn with_custom_provider(
441        mut self,
442        name: impl Into<Cow<'static, str>>,
443        provider: impl ProvideCredentials + 'static,
444    ) -> Self {
445        self.custom_providers
446            .insert(name.into(), Arc::new(provider));
447        self
448    }
449
450    /// Override the profile name used by the [`ProfileFileCredentialsProvider`]
451    pub fn profile_name(mut self, profile_name: impl Into<String>) -> Self {
452        self.profile_override = Some(profile_name.into());
453        self
454    }
455
456    /// Set the profile file that should be used by the [`ProfileFileCredentialsProvider`]
457    #[allow(deprecated)]
458    pub fn profile_files(mut self, profile_files: ProfileFiles) -> Self {
459        self.profile_files = Some(profile_files);
460        self
461    }
462
463    /// Builds a [`ProfileFileCredentialsProvider`]
464    pub fn build(self) -> ProfileFileCredentialsProvider {
465        let build_span = tracing::debug_span!("build_profile_file_credentials_provider");
466        let _enter = build_span.enter();
467        let conf = self
468            .provider_config
469            .unwrap_or_default()
470            .with_profile_config(self.profile_files, self.profile_override);
471        let mut named_providers = self.custom_providers.clone();
472        named_providers
473            .entry("Environment".into())
474            .or_insert_with(|| {
475                Arc::new(crate::environment::credentials::EnvironmentVariableCredentialsProvider::new_with_env(
476                    conf.env(),
477                ))
478            });
479
480        named_providers
481            .entry("Ec2InstanceMetadata".into())
482            .or_insert_with(|| {
483                Arc::new(
484                    crate::imds::credentials::ImdsCredentialsProvider::builder()
485                        .configure(&conf)
486                        .build(),
487                )
488            });
489
490        named_providers
491            .entry("EcsContainer".into())
492            .or_insert_with(|| {
493                Arc::new(
494                    crate::ecs::EcsCredentialsProvider::builder()
495                        .configure(&conf)
496                        .build(),
497                )
498            });
499        let factory = exec::named::NamedProviderFactory::new(named_providers);
500
501        ProfileFileCredentialsProvider {
502            config: Arc::new(Config {
503                factory,
504                provider_config: conf,
505            }),
506            inner_provider: ErrorTakingOnceCell::new(),
507        }
508    }
509}
510
511async fn build_provider_chain(
512    config: Arc<Config>,
513) -> Result<exec::ProviderChain, ProfileFileError> {
514    let profile_set = config
515        .provider_config
516        .try_profile()
517        .await
518        .map_err(|parse_err| ProfileFileError::InvalidProfile(parse_err.clone()))?;
519    let repr = repr::resolve_chain(profile_set)?;
520    tracing::info!(chain = ?repr, "constructed abstract provider from config file");
521    exec::ProviderChain::from_repr(&config.provider_config, repr, &config.factory)
522}
523
524#[derive(Debug)]
525struct ChainProvider {
526    config: Arc<Config>,
527    chain: Option<Arc<exec::ProviderChain>>,
528}
529
530impl ChainProvider {
531    async fn provide_credentials(&self) -> Result<Credentials, CredentialsError> {
532        // Can't borrow `self` across an await point, or else we lose `Send` on the returned future
533        let config = self.config.clone();
534        let chain = self.chain.clone();
535
536        if let Some(chain) = chain {
537            let mut creds = match chain
538                .base()
539                .provide_credentials()
540                .instrument(tracing::debug_span!("load_base_credentials"))
541                .await
542            {
543                Ok(creds) => {
544                    tracing::info!(creds = ?creds, "loaded base credentials");
545                    creds
546                }
547                Err(e) => {
548                    tracing::warn!(error = %DisplayErrorContext(&e), "failed to load base credentials");
549                    return Err(CredentialsError::provider_error(e));
550                }
551            };
552
553            // we want to create `SdkConfig` _after_ we have resolved the profile or else
554            // we won't get things like `service_config()` set appropriately.
555            let sdk_config = config.provider_config.client_config();
556            for provider in chain.chain().iter() {
557                let next_creds = provider
558                    .credentials(creds, &sdk_config)
559                    .instrument(tracing::debug_span!("load_assume_role", provider = ?provider))
560                    .await;
561                match next_creds {
562                    Ok(next_creds) => {
563                        tracing::info!(creds = ?next_creds, "loaded assume role credentials");
564                        creds = next_creds
565                    }
566                    Err(e) => {
567                        tracing::warn!(provider = ?provider, "failed to load assume role credentials");
568                        return Err(CredentialsError::provider_error(e));
569                    }
570                }
571            }
572            Ok(creds)
573        } else {
574            Err(CredentialsError::not_loaded_no_source())
575        }
576    }
577}
578
579#[cfg(test)]
580mod test {
581    use crate::profile::credentials::Builder;
582    use aws_credential_types::provider::ProvideCredentials;
583
584    macro_rules! make_test {
585        ($name: ident) => {
586            #[tokio::test]
587            async fn $name() {
588                let _ = crate::test_case::TestEnvironment::from_dir(
589                    concat!("./test-data/profile-provider/", stringify!($name)),
590                    crate::test_case::test_credentials_provider(|config| async move {
591                        Builder::default()
592                            .configure(&config)
593                            .build()
594                            .provide_credentials()
595                            .await
596                    }),
597                )
598                .await
599                .unwrap()
600                .execute()
601                .await;
602            }
603        };
604    }
605
606    make_test!(e2e_assume_role);
607    make_test!(e2e_fips_and_dual_stack_sts);
608    make_test!(empty_config);
609    make_test!(retry_on_error);
610    make_test!(invalid_config);
611    make_test!(region_override);
612    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is disabled on Windows because it uses Unix-style paths
613    #[cfg(all(feature = "credentials-process", not(windows)))]
614    make_test!(credential_process);
615    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is disabled on Windows because it uses Unix-style paths
616    #[cfg(all(feature = "credentials-process", not(windows)))]
617    make_test!(credential_process_failure);
618    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is disabled on Windows because it uses Unix-style paths
619    #[cfg(all(feature = "credentials-process", not(windows)))]
620    make_test!(credential_process_account_id_fallback);
621    #[cfg(feature = "credentials-process")]
622    make_test!(credential_process_invalid);
623    #[cfg(feature = "sso")]
624    make_test!(sso_credentials);
625    #[cfg(feature = "sso")]
626    make_test!(invalid_sso_credentials_config);
627    #[cfg(feature = "sso")]
628    make_test!(sso_override_global_env_url);
629    #[cfg(feature = "sso")]
630    make_test!(sso_token);
631
632    make_test!(assume_role_override_global_env_url);
633    make_test!(assume_role_override_service_env_url);
634    make_test!(assume_role_override_global_profile_url);
635    make_test!(assume_role_override_service_profile_url);
636}
637
638#[cfg(all(test, feature = "sso"))]
639mod sso_tests {
640    use crate::{profile::credentials::Builder, provider_config::ProviderConfig};
641    use aws_credential_types::credential_feature::AwsCredentialFeature;
642    use aws_credential_types::provider::ProvideCredentials;
643    use aws_sdk_sso::config::RuntimeComponents;
644    use aws_smithy_runtime_api::client::{
645        http::{
646            HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings,
647            SharedHttpConnector,
648        },
649        orchestrator::{HttpRequest, HttpResponse},
650    };
651    use aws_smithy_types::body::SdkBody;
652    use aws_types::os_shim_internal::{Env, Fs};
653    use std::collections::HashMap;
654
655    #[derive(Debug)]
656    struct ClientInner {
657        expected_token: &'static str,
658    }
659    impl HttpConnector for ClientInner {
660        fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
661            assert_eq!(
662                self.expected_token,
663                request.headers().get("x-amz-sso_bearer_token").unwrap()
664            );
665            HttpConnectorFuture::ready(Ok(HttpResponse::new(
666                    200.try_into().unwrap(),
667                    SdkBody::from("{\"roleCredentials\":{\"accessKeyId\":\"ASIARTESTID\",\"secretAccessKey\":\"TESTSECRETKEY\",\"sessionToken\":\"TESTSESSIONTOKEN\",\"expiration\": 1651516560000}}"),
668                )))
669        }
670    }
671    #[derive(Debug)]
672    struct Client {
673        inner: SharedHttpConnector,
674    }
675    impl Client {
676        fn new(expected_token: &'static str) -> Self {
677            Self {
678                inner: SharedHttpConnector::new(ClientInner { expected_token }),
679            }
680        }
681    }
682    impl HttpClient for Client {
683        fn http_connector(
684            &self,
685            _settings: &HttpConnectorSettings,
686            _components: &RuntimeComponents,
687        ) -> SharedHttpConnector {
688            self.inner.clone()
689        }
690    }
691
692    fn create_test_fs() -> Fs {
693        Fs::from_map({
694            let mut map = HashMap::new();
695            map.insert(
696                "/home/.aws/config".to_string(),
697                br#"
698[profile default]
699sso_session = dev
700sso_account_id = 012345678901
701sso_role_name = SampleRole
702region = us-east-1
703
704[sso-session dev]
705sso_region = us-east-1
706sso_start_url = https://d-abc123.awsapps.com/start
707                "#
708                .to_vec(),
709            );
710            map.insert(
711                "/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json".to_string(),
712                br#"
713                {
714                    "accessToken": "secret-access-token",
715                    "expiresAt": "2199-11-14T04:05:45Z",
716                    "refreshToken": "secret-refresh-token",
717                    "clientId": "ABCDEFG323242423121312312312312312",
718                    "clientSecret": "ABCDE123",
719                    "registrationExpiresAt": "2199-03-06T19:53:17Z",
720                    "region": "us-east-1",
721                    "startUrl": "https://d-abc123.awsapps.com/start"
722                }
723                "#
724                .to_vec(),
725            );
726            map
727        })
728    }
729
730    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is ignored on Windows because it uses Unix-style paths
731    #[cfg_attr(windows, ignore)]
732    // In order to preserve the SSO token cache, the inner provider must only
733    // be created once, rather than once per credential resolution.
734    #[tokio::test]
735    async fn create_inner_provider_exactly_once() {
736        let fs = create_test_fs();
737
738        let provider_config = ProviderConfig::empty()
739            .with_fs(fs.clone())
740            .with_env(Env::from_slice(&[("HOME", "/home")]))
741            .with_http_client(Client::new("secret-access-token"));
742        let provider = Builder::default().configure(&provider_config).build();
743
744        let first_creds = provider.provide_credentials().await.unwrap();
745
746        // Write to the token cache with an access token that won't match the fake client's
747        // expected access token, and thus, won't return SSO credentials.
748        fs.write(
749            "/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json",
750            r#"
751            {
752                "accessToken": "NEW!!secret-access-token",
753                "expiresAt": "2199-11-14T04:05:45Z",
754                "refreshToken": "secret-refresh-token",
755                "clientId": "ABCDEFG323242423121312312312312312",
756                "clientSecret": "ABCDE123",
757                "registrationExpiresAt": "2199-03-06T19:53:17Z",
758                "region": "us-east-1",
759                "startUrl": "https://d-abc123.awsapps.com/start"
760            }
761            "#,
762        )
763        .await
764        .unwrap();
765
766        // Loading credentials will still work since the SSOTokenProvider should have only
767        // been created once, and thus, the correct token is still in an in-memory cache.
768        let second_creds = provider
769            .provide_credentials()
770            .await
771            .expect("used cached token instead of loading from the file system");
772        assert_eq!(first_creds, second_creds);
773
774        // Now create a new provider, which should use the new cached token value from the file system
775        // since it won't have the in-memory cache. We do this just to verify that the FS mutation above
776        // actually worked correctly.
777        let provider_config = ProviderConfig::empty()
778            .with_fs(fs.clone())
779            .with_env(Env::from_slice(&[("HOME", "/home")]))
780            .with_http_client(Client::new("NEW!!secret-access-token"));
781        let provider = Builder::default().configure(&provider_config).build();
782        let third_creds = provider.provide_credentials().await.unwrap();
783        assert_eq!(second_creds, third_creds);
784    }
785
786    #[cfg_attr(windows, ignore)]
787    #[tokio::test]
788    async fn credential_feature() {
789        let fs = create_test_fs();
790
791        let provider_config = ProviderConfig::empty()
792            .with_fs(fs.clone())
793            .with_env(Env::from_slice(&[("HOME", "/home")]))
794            .with_http_client(Client::new("secret-access-token"));
795        let provider = Builder::default().configure(&provider_config).build();
796
797        let creds = provider.provide_credentials().await.unwrap();
798
799        assert_eq!(
800            &vec![
801                AwsCredentialFeature::CredentialsSso,
802                AwsCredentialFeature::CredentialsProfile
803            ],
804            creds.get_property::<Vec<AwsCredentialFeature>>().unwrap()
805        )
806    }
807}
808
809#[cfg(all(test, feature = "credentials-login"))]
810mod login_tests {
811    use crate::provider_config::ProviderConfig;
812    use aws_credential_types::provider::error::CredentialsError;
813    use aws_credential_types::provider::ProvideCredentials;
814    use aws_sdk_signin::config::RuntimeComponents;
815    use aws_smithy_runtime_api::client::{
816        http::{
817            HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings,
818            SharedHttpConnector,
819        },
820        orchestrator::{HttpRequest, HttpResponse},
821    };
822    use aws_smithy_types::body::SdkBody;
823    use aws_types::os_shim_internal::{Env, Fs};
824    use std::collections::HashMap;
825    use std::sync::atomic::{AtomicUsize, Ordering};
826    use std::sync::Arc;
827
828    #[derive(Debug, Clone)]
829    struct TestClientInner {
830        call_count: Arc<AtomicUsize>,
831        response: Option<&'static str>,
832    }
833
834    impl HttpConnector for TestClientInner {
835        fn call(&self, _request: HttpRequest) -> HttpConnectorFuture {
836            self.call_count.fetch_add(1, Ordering::SeqCst);
837            if let Some(response) = self.response {
838                HttpConnectorFuture::ready(Ok(HttpResponse::new(
839                    200.try_into().unwrap(),
840                    SdkBody::from(response),
841                )))
842            } else {
843                HttpConnectorFuture::ready(Ok(HttpResponse::new(
844                    500.try_into().unwrap(),
845                    SdkBody::from("{\"error\":\"server_error\"}"),
846                )))
847            }
848        }
849    }
850
851    #[derive(Debug, Clone)]
852    struct TestClient {
853        inner: SharedHttpConnector,
854        call_count: Arc<AtomicUsize>,
855    }
856
857    impl TestClient {
858        fn new_success() -> Self {
859            let call_count = Arc::new(AtomicUsize::new(0));
860            let response = r#"{
861                "accessToken": {
862                    "accessKeyId": "ASIARTESTID",
863                    "secretAccessKey": "TESTSECRETKEY",
864                    "sessionToken": "TESTSESSIONTOKEN"
865                },
866                "expiresIn": 3600,
867                "refreshToken": "new-refresh-token"
868            }"#;
869            let inner = TestClientInner {
870                call_count: call_count.clone(),
871                response: Some(response),
872            };
873            Self {
874                inner: SharedHttpConnector::new(inner),
875                call_count,
876            }
877        }
878
879        fn new_error() -> Self {
880            let call_count = Arc::new(AtomicUsize::new(0));
881            let inner = TestClientInner {
882                call_count: call_count.clone(),
883                response: None,
884            };
885            Self {
886                inner: SharedHttpConnector::new(inner),
887                call_count,
888            }
889        }
890
891        fn call_count(&self) -> usize {
892            self.call_count.load(Ordering::SeqCst)
893        }
894    }
895
896    impl HttpClient for TestClient {
897        fn http_connector(
898            &self,
899            _settings: &HttpConnectorSettings,
900            _components: &RuntimeComponents,
901        ) -> SharedHttpConnector {
902            self.inner.clone()
903        }
904    }
905
906    fn create_test_fs_unexpired() -> Fs {
907        Fs::from_map({
908            let mut map = HashMap::new();
909            map.insert(
910                "/home/.aws/config".to_string(),
911                br#"
912[profile default]
913login_session = arn:aws:iam::0123456789012:user/Admin
914region = us-east-1
915                "#
916                .to_vec(),
917            );
918            map.insert(
919                "/home/.aws/login/cache/36db1d138ff460920374e4c3d8e01f53f9f73537e89c88d639f68393df0e2726.json".to_string(),
920                br#"{
921                    "accessToken": {
922                        "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
923                        "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
924                        "sessionToken": "session-token",
925                        "accountId": "012345678901",
926                        "expiresAt": "2199-12-25T21:30:00Z"
927                    },
928                    "tokenType": "aws_sigv4",
929                    "refreshToken": "refresh-token-value",
930                    "identityToken": "identity-token-value",
931                    "clientId": "aws:signin:::cli/same-device",
932                    "dpopKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIFDZHUzOG1Pzq+6F0mjMlOSp1syN9LRPBuHMoCFXTcXhoAoGCCqGSM49\nAwEHoUQDQgAE9qhj+KtcdHj1kVgwxWWWw++tqoh7H7UHs7oXh8jBbgF47rrYGC+t\ndjiIaHK3dBvvdE7MGj5HsepzLm3Kj91bqA==\n-----END EC PRIVATE KEY-----\n"
933                }"#
934                .to_vec(),
935            );
936            map
937        })
938    }
939
940    fn create_test_fs_expired() -> Fs {
941        Fs::from_map({
942            let mut map = HashMap::new();
943            map.insert(
944                "/home/.aws/config".to_string(),
945                br#"
946[profile default]
947login_session = arn:aws:iam::0123456789012:user/Admin
948region = us-east-1
949                "#
950                .to_vec(),
951            );
952            map.insert(
953                "/home/.aws/login/cache/36db1d138ff460920374e4c3d8e01f53f9f73537e89c88d639f68393df0e2726.json".to_string(),
954                br#"{
955                    "accessToken": {
956                        "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
957                        "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
958                        "sessionToken": "session-token",
959                        "accountId": "012345678901",
960                        "expiresAt": "2020-01-01T00:00:00Z"
961                    },
962                    "tokenType": "aws_sigv4",
963                    "refreshToken": "refresh-token-value",
964                    "identityToken": "identity-token-value",
965                    "clientId": "aws:signin:::cli/same-device",
966                    "dpopKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIFDZHUzOG1Pzq+6F0mjMlOSp1syN9LRPBuHMoCFXTcXhoAoGCCqGSM49\nAwEHoUQDQgAE9qhj+KtcdHj1kVgwxWWWw++tqoh7H7UHs7oXh8jBbgF47rrYGC+t\ndjiIaHK3dBvvdE7MGj5HsepzLm3Kj91bqA==\n-----END EC PRIVATE KEY-----\n"
967                }"#
968                .to_vec(),
969            );
970            map
971        })
972    }
973
974    #[cfg_attr(windows, ignore)]
975    #[tokio::test]
976    async fn unexpired_credentials_no_refresh() {
977        let client = TestClient::new_success();
978
979        let provider_config = ProviderConfig::empty()
980            .with_fs(create_test_fs_unexpired())
981            .with_env(Env::from_slice(&[("HOME", "/home")]))
982            .with_http_client(client.clone())
983            .with_region(Some(aws_types::region::Region::new("us-east-1")));
984
985        let provider = crate::profile::credentials::Builder::default()
986            .configure(&provider_config)
987            .build();
988
989        let creds = provider.provide_credentials().await.unwrap();
990        assert_eq!("AKIAIOSFODNN7EXAMPLE", creds.access_key_id());
991        assert_eq!(0, client.call_count());
992    }
993
994    #[cfg_attr(windows, ignore)]
995    #[tokio::test]
996    async fn expired_credentials_trigger_refresh() {
997        let client = TestClient::new_success();
998
999        let provider_config = ProviderConfig::empty()
1000            .with_fs(create_test_fs_expired())
1001            .with_env(Env::from_slice(&[("HOME", "/home")]))
1002            .with_http_client(client.clone())
1003            .with_region(Some(aws_types::region::Region::new("us-east-1")));
1004
1005        let provider = crate::profile::credentials::Builder::default()
1006            .configure(&provider_config)
1007            .build();
1008
1009        let creds = provider.provide_credentials().await.unwrap();
1010        assert_eq!("ASIARTESTID", creds.access_key_id());
1011        assert_eq!(1, client.call_count());
1012    }
1013
1014    #[cfg_attr(windows, ignore)]
1015    #[tokio::test]
1016    async fn refresh_error_propagates() {
1017        let client = TestClient::new_error();
1018
1019        let provider_config = ProviderConfig::empty()
1020            .with_fs(create_test_fs_expired())
1021            .with_env(Env::from_slice(&[("HOME", "/home")]))
1022            .with_http_client(client)
1023            .with_region(Some(aws_types::region::Region::new("us-east-1")));
1024
1025        let provider = crate::profile::credentials::Builder::default()
1026            .configure(&provider_config)
1027            .build();
1028
1029        let err = provider
1030            .provide_credentials()
1031            .await
1032            .expect_err("should fail on refresh error");
1033
1034        match &err {
1035            CredentialsError::ProviderError(_) => {}
1036            _ => panic!("wrong error type"),
1037        }
1038    }
1039}