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 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 #[cfg(all(feature = "credentials-process", not(windows)))]
614 make_test!(credential_process);
615 #[cfg(all(feature = "credentials-process", not(windows)))]
617 make_test!(credential_process_failure);
618 #[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 #[cfg_attr(windows, ignore)]
732 #[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 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 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 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}