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 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#[derive(Debug)]
223#[non_exhaustive]
224pub enum ProfileFileError {
225 #[non_exhaustive]
227 InvalidProfile(ProfileFileLoadError),
228
229 #[non_exhaustive]
231 NoProfilesDefined,
232
233 #[non_exhaustive]
235 ProfileDidNotContainCredentials {
236 profile: String,
238 },
239
240 #[non_exhaustive]
242 CredentialLoop {
243 profiles: Vec<String>,
245 next: String,
247 },
248
249 #[non_exhaustive]
251 MissingCredentialSource {
252 profile: String,
254 message: Cow<'static, str>,
256 },
257 #[non_exhaustive]
259 InvalidCredentialSource {
260 profile: String,
262 message: Cow<'static, str>,
264 },
265 #[non_exhaustive]
267 MissingProfile {
268 profile: String,
270 message: Cow<'static, str>,
272 },
273 #[non_exhaustive]
275 UnknownProvider {
276 name: String,
278 },
279
280 #[non_exhaustive]
282 FeatureNotEnabled {
283 feature: Cow<'static, str>,
285 message: Option<Cow<'static, str>>,
287 },
288
289 #[non_exhaustive]
291 MissingSsoSession {
292 profile: String,
294 sso_session: String,
296 },
297
298 #[non_exhaustive]
300 InvalidSsoConfig {
301 profile: String,
303 message: Cow<'static, str>,
305 },
306
307 #[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#[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 pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
413 self.provider_config = Some(provider_config.clone());
414 self
415 }
416
417 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 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 #[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 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 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 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 #[cfg(all(feature = "credentials-process", not(windows)))]
637 make_test!(credential_process);
638 #[cfg(all(feature = "credentials-process", not(windows)))]
640 make_test!(credential_process_failure);
641 #[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 #[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 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 #[cfg_attr(windows, ignore)]
816 #[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 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 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 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}