1use 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#[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 pub fn builder() -> Builder {
160 Builder::default()
161 }
162
163 async fn load_credentials(&self) -> provider::Result {
164 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#[derive(Debug)]
219#[non_exhaustive]
220pub enum ProfileFileError {
221 #[non_exhaustive]
223 InvalidProfile(ProfileFileLoadError),
224
225 #[non_exhaustive]
227 NoProfilesDefined,
228
229 #[non_exhaustive]
231 ProfileDidNotContainCredentials {
232 profile: String,
234 },
235
236 #[non_exhaustive]
238 CredentialLoop {
239 profiles: Vec<String>,
241 next: String,
243 },
244
245 #[non_exhaustive]
247 MissingCredentialSource {
248 profile: String,
250 message: Cow<'static, str>,
252 },
253 #[non_exhaustive]
255 InvalidCredentialSource {
256 profile: String,
258 message: Cow<'static, str>,
260 },
261 #[non_exhaustive]
263 MissingProfile {
264 profile: String,
266 message: Cow<'static, str>,
268 },
269 #[non_exhaustive]
271 UnknownProvider {
272 name: String,
274 },
275
276 #[non_exhaustive]
278 FeatureNotEnabled {
279 feature: Cow<'static, str>,
281 message: Option<Cow<'static, str>>,
283 },
284
285 #[non_exhaustive]
287 MissingSsoSession {
288 profile: String,
290 sso_session: String,
292 },
293
294 #[non_exhaustive]
296 InvalidSsoConfig {
297 profile: String,
299 message: Cow<'static, str>,
301 },
302
303 #[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#[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 pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
409 self.provider_config = Some(provider_config.clone());
410 self
411 }
412
413 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 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 #[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 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 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 let mut sdk_config_builder = config.provider_config.client_config().into_builder();
560 if config.provider_config.use_fips().is_none() {
561 sdk_config_builder.set_use_fips(
562 crate::default_provider::use_fips::use_fips_provider(&config.provider_config)
563 .await,
564 );
565 }
566 if config.provider_config.use_dual_stack().is_none() {
567 sdk_config_builder.set_use_dual_stack(
568 crate::default_provider::use_dual_stack::use_dual_stack_provider(
569 &config.provider_config,
570 )
571 .await,
572 );
573 }
574 let sdk_config = sdk_config_builder.build();
575 for provider in chain.chain().iter() {
576 let next_creds = provider
577 .credentials(creds, &sdk_config)
578 .instrument(tracing::debug_span!("load_assume_role", provider = ?provider))
579 .await;
580 match next_creds {
581 Ok(next_creds) => {
582 tracing::info!(creds = ?next_creds, "loaded assume role credentials");
583 creds = next_creds
584 }
585 Err(e) => {
586 tracing::warn!(provider = ?provider, "failed to load assume role credentials");
587 return Err(CredentialsError::provider_error(e));
588 }
589 }
590 }
591 Ok(creds)
592 } else {
593 Err(CredentialsError::not_loaded_no_source())
594 }
595 }
596}
597
598#[cfg(test)]
599mod test {
600 use crate::profile::credentials::Builder;
601 use aws_credential_types::provider::ProvideCredentials;
602
603 macro_rules! make_test {
604 ($name: ident) => {
605 #[tokio::test]
606 async fn $name() {
607 let _ = crate::test_case::TestEnvironment::from_dir(
608 concat!("./test-data/profile-provider/", stringify!($name)),
609 crate::test_case::test_credentials_provider(|config| async move {
610 Builder::default()
611 .configure(&config)
612 .build()
613 .provide_credentials()
614 .await
615 }),
616 )
617 .await
618 .unwrap()
619 .execute()
620 .await;
621 }
622 };
623 }
624
625 make_test!(e2e_assume_role);
626 make_test!(e2e_fips_and_dual_stack_sts);
627 make_test!(empty_config);
628 make_test!(retry_on_error);
629 make_test!(invalid_config);
630 make_test!(region_override);
631 #[cfg(all(feature = "credentials-process", not(windows)))]
633 make_test!(credential_process);
634 #[cfg(all(feature = "credentials-process", not(windows)))]
636 make_test!(credential_process_failure);
637 #[cfg(all(feature = "credentials-process", not(windows)))]
639 make_test!(credential_process_account_id_fallback);
640 #[cfg(feature = "credentials-process")]
641 make_test!(credential_process_invalid);
642 #[cfg(feature = "sso")]
643 make_test!(sso_credentials);
644 #[cfg(feature = "sso")]
645 make_test!(invalid_sso_credentials_config);
646 #[cfg(feature = "sso")]
647 make_test!(sso_override_global_env_url);
648 #[cfg(feature = "sso")]
649 make_test!(sso_token);
650
651 make_test!(assume_role_override_global_env_url);
652 make_test!(assume_role_override_service_env_url);
653 make_test!(assume_role_override_global_profile_url);
654 make_test!(assume_role_override_service_profile_url);
655
656 #[tokio::test]
661 async fn profile_use_fips_endpoint_propagates_to_sts_client() {
662 #[allow(deprecated)]
663 use crate::profile::profile_file::{ProfileFileKind, ProfileFiles};
664 use crate::provider_config::ProviderConfig;
665 use aws_smithy_http_client::test_util::capture_request;
666 use aws_smithy_types::body::SdkBody;
667 use aws_types::os_shim_internal::Env;
668 use aws_types::region::Region;
669
670 let (http_client, rx) = capture_request(Some(
671 http::Response::builder()
672 .status(200)
673 .body(SdkBody::from(
674 r#"<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
675 <AssumeRoleResult>
676 <Credentials>
677 <AccessKeyId>ASIAFAKEKEY</AccessKeyId>
678 <SecretAccessKey>fakesecret</SecretAccessKey>
679 <SessionToken>fakesession</SessionToken>
680 <Expiration>2099-01-01T00:00:00Z</Expiration>
681 </Credentials>
682 </AssumeRoleResult>
683 </AssumeRoleResponse>"#,
684 ))
685 .unwrap(),
686 ));
687
688 let provider_config = ProviderConfig::empty()
689 .with_env(Env::from_slice(&[]))
690 .with_region(Some(Region::new("us-east-1")))
691 .with_http_client(http_client);
692
693 #[allow(deprecated)]
694 let profile_files = ProfileFiles::builder()
695 .with_contents(
696 ProfileFileKind::Config,
697 "[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",
698 )
699 .build();
700
701 let provider = Builder::default()
702 .configure(&provider_config)
703 .profile_files(profile_files)
704 .profile_name("fips-test")
705 .build();
706
707 let _ = provider.provide_credentials().await;
708 let request = rx.expect_request();
709 let uri = request.uri().to_string();
710 assert!(
712 uri.contains("sts-fips.us-east-1.api.aws"),
713 "expected STS FIPS + dual-stack endpoint, got: {uri}"
714 );
715 }
716}
717
718#[cfg(all(test, feature = "sso"))]
719mod sso_tests {
720 use crate::{profile::credentials::Builder, provider_config::ProviderConfig};
721 use aws_credential_types::credential_feature::AwsCredentialFeature;
722 use aws_credential_types::provider::ProvideCredentials;
723 use aws_sdk_sso::config::RuntimeComponents;
724 use aws_smithy_runtime_api::client::{
725 http::{
726 HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings,
727 SharedHttpConnector,
728 },
729 orchestrator::{HttpRequest, HttpResponse},
730 };
731 use aws_smithy_types::body::SdkBody;
732 use aws_types::os_shim_internal::{Env, Fs};
733 use std::collections::HashMap;
734
735 #[derive(Debug)]
736 struct ClientInner {
737 expected_token: &'static str,
738 }
739 impl HttpConnector for ClientInner {
740 fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
741 assert_eq!(
742 self.expected_token,
743 request.headers().get("x-amz-sso_bearer_token").unwrap()
744 );
745 HttpConnectorFuture::ready(Ok(HttpResponse::new(
746 200.try_into().unwrap(),
747 SdkBody::from("{\"roleCredentials\":{\"accessKeyId\":\"ASIARTESTID\",\"secretAccessKey\":\"TESTSECRETKEY\",\"sessionToken\":\"TESTSESSIONTOKEN\",\"expiration\": 1651516560000}}"),
748 )))
749 }
750 }
751 #[derive(Debug)]
752 struct Client {
753 inner: SharedHttpConnector,
754 }
755 impl Client {
756 fn new(expected_token: &'static str) -> Self {
757 Self {
758 inner: SharedHttpConnector::new(ClientInner { expected_token }),
759 }
760 }
761 }
762 impl HttpClient for Client {
763 fn http_connector(
764 &self,
765 _settings: &HttpConnectorSettings,
766 _components: &RuntimeComponents,
767 ) -> SharedHttpConnector {
768 self.inner.clone()
769 }
770 }
771
772 fn create_test_fs() -> Fs {
773 Fs::from_map({
774 let mut map = HashMap::new();
775 map.insert(
776 "/home/.aws/config".to_string(),
777 br#"
778[profile default]
779sso_session = dev
780sso_account_id = 012345678901
781sso_role_name = SampleRole
782region = us-east-1
783
784[sso-session dev]
785sso_region = us-east-1
786sso_start_url = https://d-abc123.awsapps.com/start
787 "#
788 .to_vec(),
789 );
790 map.insert(
791 "/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json".to_string(),
792 br#"
793 {
794 "accessToken": "secret-access-token",
795 "expiresAt": "2199-11-14T04:05:45Z",
796 "refreshToken": "secret-refresh-token",
797 "clientId": "ABCDEFG323242423121312312312312312",
798 "clientSecret": "ABCDE123",
799 "registrationExpiresAt": "2199-03-06T19:53:17Z",
800 "region": "us-east-1",
801 "startUrl": "https://d-abc123.awsapps.com/start"
802 }
803 "#
804 .to_vec(),
805 );
806 map
807 })
808 }
809
810 #[cfg_attr(windows, ignore)]
812 #[tokio::test]
815 async fn create_inner_provider_exactly_once() {
816 let fs = create_test_fs();
817
818 let provider_config = ProviderConfig::empty()
819 .with_fs(fs.clone())
820 .with_env(Env::from_slice(&[("HOME", "/home")]))
821 .with_http_client(Client::new("secret-access-token"));
822 let provider = Builder::default().configure(&provider_config).build();
823
824 let first_creds = provider.provide_credentials().await.unwrap();
825
826 fs.write(
829 "/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json",
830 r#"
831 {
832 "accessToken": "NEW!!secret-access-token",
833 "expiresAt": "2199-11-14T04:05:45Z",
834 "refreshToken": "secret-refresh-token",
835 "clientId": "ABCDEFG323242423121312312312312312",
836 "clientSecret": "ABCDE123",
837 "registrationExpiresAt": "2199-03-06T19:53:17Z",
838 "region": "us-east-1",
839 "startUrl": "https://d-abc123.awsapps.com/start"
840 }
841 "#,
842 )
843 .await
844 .unwrap();
845
846 let second_creds = provider
849 .provide_credentials()
850 .await
851 .expect("used cached token instead of loading from the file system");
852 assert_eq!(first_creds, second_creds);
853
854 let provider_config = ProviderConfig::empty()
858 .with_fs(fs.clone())
859 .with_env(Env::from_slice(&[("HOME", "/home")]))
860 .with_http_client(Client::new("NEW!!secret-access-token"));
861 let provider = Builder::default().configure(&provider_config).build();
862 let third_creds = provider.provide_credentials().await.unwrap();
863 assert_eq!(second_creds, third_creds);
864 }
865
866 #[cfg_attr(windows, ignore)]
867 #[tokio::test]
868 async fn credential_feature() {
869 let fs = create_test_fs();
870
871 let provider_config = ProviderConfig::empty()
872 .with_fs(fs.clone())
873 .with_env(Env::from_slice(&[("HOME", "/home")]))
874 .with_http_client(Client::new("secret-access-token"));
875 let provider = Builder::default().configure(&provider_config).build();
876
877 let creds = provider.provide_credentials().await.unwrap();
878
879 assert_eq!(
880 &vec![
881 AwsCredentialFeature::CredentialsSso,
882 AwsCredentialFeature::CredentialsProfile
883 ],
884 creds.get_property::<Vec<AwsCredentialFeature>>().unwrap()
885 )
886 }
887}
888
889#[cfg(all(test, feature = "credentials-login"))]
890mod login_tests {
891 use crate::provider_config::ProviderConfig;
892 use aws_credential_types::provider::error::CredentialsError;
893 use aws_credential_types::provider::ProvideCredentials;
894 use aws_sdk_signin::config::RuntimeComponents;
895 use aws_smithy_runtime_api::client::{
896 http::{
897 HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings,
898 SharedHttpConnector,
899 },
900 orchestrator::{HttpRequest, HttpResponse},
901 };
902 use aws_smithy_types::body::SdkBody;
903 use aws_types::os_shim_internal::{Env, Fs};
904 use std::collections::HashMap;
905 use std::sync::atomic::{AtomicUsize, Ordering};
906 use std::sync::Arc;
907
908 #[derive(Debug, Clone)]
909 struct TestClientInner {
910 call_count: Arc<AtomicUsize>,
911 response: Option<&'static str>,
912 }
913
914 impl HttpConnector for TestClientInner {
915 fn call(&self, _request: HttpRequest) -> HttpConnectorFuture {
916 self.call_count.fetch_add(1, Ordering::SeqCst);
917 if let Some(response) = self.response {
918 HttpConnectorFuture::ready(Ok(HttpResponse::new(
919 200.try_into().unwrap(),
920 SdkBody::from(response),
921 )))
922 } else {
923 HttpConnectorFuture::ready(Ok(HttpResponse::new(
924 500.try_into().unwrap(),
925 SdkBody::from("{\"error\":\"server_error\"}"),
926 )))
927 }
928 }
929 }
930
931 #[derive(Debug, Clone)]
932 struct TestClient {
933 inner: SharedHttpConnector,
934 call_count: Arc<AtomicUsize>,
935 }
936
937 impl TestClient {
938 fn new_success() -> Self {
939 let call_count = Arc::new(AtomicUsize::new(0));
940 let response = r#"{
941 "accessToken": {
942 "accessKeyId": "ASIARTESTID",
943 "secretAccessKey": "TESTSECRETKEY",
944 "sessionToken": "TESTSESSIONTOKEN"
945 },
946 "expiresIn": 3600,
947 "refreshToken": "new-refresh-token"
948 }"#;
949 let inner = TestClientInner {
950 call_count: call_count.clone(),
951 response: Some(response),
952 };
953 Self {
954 inner: SharedHttpConnector::new(inner),
955 call_count,
956 }
957 }
958
959 fn new_error() -> Self {
960 let call_count = Arc::new(AtomicUsize::new(0));
961 let inner = TestClientInner {
962 call_count: call_count.clone(),
963 response: None,
964 };
965 Self {
966 inner: SharedHttpConnector::new(inner),
967 call_count,
968 }
969 }
970
971 fn call_count(&self) -> usize {
972 self.call_count.load(Ordering::SeqCst)
973 }
974 }
975
976 impl HttpClient for TestClient {
977 fn http_connector(
978 &self,
979 _settings: &HttpConnectorSettings,
980 _components: &RuntimeComponents,
981 ) -> SharedHttpConnector {
982 self.inner.clone()
983 }
984 }
985
986 fn create_test_fs_unexpired() -> Fs {
987 Fs::from_map({
988 let mut map = HashMap::new();
989 map.insert(
990 "/home/.aws/config".to_string(),
991 br#"
992[profile default]
993login_session = arn:aws:iam::0123456789012:user/Admin
994region = us-east-1
995 "#
996 .to_vec(),
997 );
998 map.insert(
999 "/home/.aws/login/cache/36db1d138ff460920374e4c3d8e01f53f9f73537e89c88d639f68393df0e2726.json".to_string(),
1000 br#"{
1001 "accessToken": {
1002 "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
1003 "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
1004 "sessionToken": "session-token",
1005 "accountId": "012345678901",
1006 "expiresAt": "2199-12-25T21:30:00Z"
1007 },
1008 "tokenType": "aws_sigv4",
1009 "refreshToken": "refresh-token-value",
1010 "identityToken": "identity-token-value",
1011 "clientId": "aws:signin:::cli/same-device",
1012 "dpopKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIFDZHUzOG1Pzq+6F0mjMlOSp1syN9LRPBuHMoCFXTcXhoAoGCCqGSM49\nAwEHoUQDQgAE9qhj+KtcdHj1kVgwxWWWw++tqoh7H7UHs7oXh8jBbgF47rrYGC+t\ndjiIaHK3dBvvdE7MGj5HsepzLm3Kj91bqA==\n-----END EC PRIVATE KEY-----\n"
1013 }"#
1014 .to_vec(),
1015 );
1016 map
1017 })
1018 }
1019
1020 fn create_test_fs_expired() -> Fs {
1021 Fs::from_map({
1022 let mut map = HashMap::new();
1023 map.insert(
1024 "/home/.aws/config".to_string(),
1025 br#"
1026[profile default]
1027login_session = arn:aws:iam::0123456789012:user/Admin
1028region = us-east-1
1029 "#
1030 .to_vec(),
1031 );
1032 map.insert(
1033 "/home/.aws/login/cache/36db1d138ff460920374e4c3d8e01f53f9f73537e89c88d639f68393df0e2726.json".to_string(),
1034 br#"{
1035 "accessToken": {
1036 "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
1037 "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
1038 "sessionToken": "session-token",
1039 "accountId": "012345678901",
1040 "expiresAt": "2020-01-01T00:00:00Z"
1041 },
1042 "tokenType": "aws_sigv4",
1043 "refreshToken": "refresh-token-value",
1044 "identityToken": "identity-token-value",
1045 "clientId": "aws:signin:::cli/same-device",
1046 "dpopKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIFDZHUzOG1Pzq+6F0mjMlOSp1syN9LRPBuHMoCFXTcXhoAoGCCqGSM49\nAwEHoUQDQgAE9qhj+KtcdHj1kVgwxWWWw++tqoh7H7UHs7oXh8jBbgF47rrYGC+t\ndjiIaHK3dBvvdE7MGj5HsepzLm3Kj91bqA==\n-----END EC PRIVATE KEY-----\n"
1047 }"#
1048 .to_vec(),
1049 );
1050 map
1051 })
1052 }
1053
1054 #[cfg_attr(windows, ignore)]
1055 #[tokio::test]
1056 async fn unexpired_credentials_no_refresh() {
1057 let client = TestClient::new_success();
1058
1059 let provider_config = ProviderConfig::empty()
1060 .with_fs(create_test_fs_unexpired())
1061 .with_env(Env::from_slice(&[("HOME", "/home")]))
1062 .with_http_client(client.clone())
1063 .with_region(Some(aws_types::region::Region::new("us-east-1")));
1064
1065 let provider = crate::profile::credentials::Builder::default()
1066 .configure(&provider_config)
1067 .build();
1068
1069 let creds = provider.provide_credentials().await.unwrap();
1070 assert_eq!("AKIAIOSFODNN7EXAMPLE", creds.access_key_id());
1071 assert_eq!(0, client.call_count());
1072 }
1073
1074 #[cfg_attr(windows, ignore)]
1075 #[tokio::test]
1076 async fn expired_credentials_trigger_refresh() {
1077 let client = TestClient::new_success();
1078
1079 let provider_config = ProviderConfig::empty()
1080 .with_fs(create_test_fs_expired())
1081 .with_env(Env::from_slice(&[("HOME", "/home")]))
1082 .with_http_client(client.clone())
1083 .with_region(Some(aws_types::region::Region::new("us-east-1")));
1084
1085 let provider = crate::profile::credentials::Builder::default()
1086 .configure(&provider_config)
1087 .build();
1088
1089 let creds = provider.provide_credentials().await.unwrap();
1090 assert_eq!("ASIARTESTID", creds.access_key_id());
1091 assert_eq!(1, client.call_count());
1092 }
1093
1094 #[cfg_attr(windows, ignore)]
1095 #[tokio::test]
1096 async fn refresh_error_propagates() {
1097 let client = TestClient::new_error();
1098
1099 let provider_config = ProviderConfig::empty()
1100 .with_fs(create_test_fs_expired())
1101 .with_env(Env::from_slice(&[("HOME", "/home")]))
1102 .with_http_client(client)
1103 .with_region(Some(aws_types::region::Region::new("us-east-1")));
1104
1105 let provider = crate::profile::credentials::Builder::default()
1106 .configure(&provider_config)
1107 .build();
1108
1109 let err = provider
1110 .provide_credentials()
1111 .await
1112 .expect_err("should fail on refresh error");
1113
1114 match &err {
1115 CredentialsError::ProviderError(_) => {}
1116 _ => panic!("wrong error type"),
1117 }
1118 }
1119}