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")]
136#[derive(Debug)]
137pub struct ProfileFileCredentialsProvider {
138 config: Arc<Config>,
139 inner_provider: ErrorTakingOnceCell<ChainProvider, CredentialsError>,
140}
141
142#[derive(Debug)]
143struct Config {
144 factory: exec::named::NamedProviderFactory,
145 provider_config: ProviderConfig,
146}
147
148impl ProfileFileCredentialsProvider {
149 pub fn builder() -> Builder {
151 Builder::default()
152 }
153
154 async fn load_credentials(&self) -> provider::Result {
155 let inner_provider = self
159 .inner_provider
160 .get_or_init(
161 {
162 let config = self.config.clone();
163 move || async move {
164 match build_provider_chain(config.clone()).await {
165 Ok(chain) => Ok(ChainProvider {
166 config: config.clone(),
167 chain: Some(Arc::new(chain)),
168 }),
169 Err(err) => match err {
170 ProfileFileError::NoProfilesDefined
171 | ProfileFileError::ProfileDidNotContainCredentials { .. } => {
172 Ok(ChainProvider {
173 config: config.clone(),
174 chain: None,
175 })
176 }
177 _ => Err(CredentialsError::invalid_configuration(format!(
178 "ProfileFile provider could not be built: {}",
179 &err
180 ))),
181 },
182 }
183 }
184 },
185 CredentialsError::unhandled(
186 "profile file credentials provider initialization error already taken",
187 ),
188 )
189 .await?;
190 inner_provider.provide_credentials().await.map(|mut creds| {
191 creds
192 .get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
193 .push(AwsCredentialFeature::CredentialsProfile);
194 creds
195 })
196 }
197}
198
199impl ProvideCredentials for ProfileFileCredentialsProvider {
200 fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
201 where
202 Self: 'a,
203 {
204 future::ProvideCredentials::new(self.load_credentials())
205 }
206}
207
208#[derive(Debug)]
210#[non_exhaustive]
211pub enum ProfileFileError {
212 #[non_exhaustive]
214 InvalidProfile(ProfileFileLoadError),
215
216 #[non_exhaustive]
218 NoProfilesDefined,
219
220 #[non_exhaustive]
222 ProfileDidNotContainCredentials {
223 profile: String,
225 },
226
227 #[non_exhaustive]
229 CredentialLoop {
230 profiles: Vec<String>,
232 next: String,
234 },
235
236 #[non_exhaustive]
238 MissingCredentialSource {
239 profile: String,
241 message: Cow<'static, str>,
243 },
244 #[non_exhaustive]
246 InvalidCredentialSource {
247 profile: String,
249 message: Cow<'static, str>,
251 },
252 #[non_exhaustive]
254 MissingProfile {
255 profile: String,
257 message: Cow<'static, str>,
259 },
260 #[non_exhaustive]
262 UnknownProvider {
263 name: String,
265 },
266
267 #[non_exhaustive]
269 FeatureNotEnabled {
270 feature: Cow<'static, str>,
272 message: Option<Cow<'static, str>>,
274 },
275
276 #[non_exhaustive]
278 MissingSsoSession {
279 profile: String,
281 sso_session: String,
283 },
284
285 #[non_exhaustive]
287 InvalidSsoConfig {
288 profile: String,
290 message: Cow<'static, str>,
292 },
293
294 #[non_exhaustive]
297 TokenProviderConfig {},
298}
299
300impl ProfileFileError {
301 fn missing_field(profile: &Profile, field: &'static str) -> Self {
302 ProfileFileError::MissingProfile {
303 profile: profile.name().to_string(),
304 message: format!("`{}` was missing", field).into(),
305 }
306 }
307}
308
309impl Error for ProfileFileError {
310 fn source(&self) -> Option<&(dyn Error + 'static)> {
311 match self {
312 ProfileFileError::InvalidProfile(err) => Some(err),
313 _ => None,
314 }
315 }
316}
317
318impl Display for ProfileFileError {
319 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
320 match self {
321 ProfileFileError::InvalidProfile(err) => {
322 write!(f, "invalid profile: {}", err)
323 }
324 ProfileFileError::CredentialLoop { profiles, next } => write!(
325 f,
326 "profile formed an infinite loop. first we loaded {:?}, \
327 then attempted to reload {}",
328 profiles, next
329 ),
330 ProfileFileError::MissingCredentialSource { profile, message } => {
331 write!(f, "missing credential source in `{}`: {}", profile, message)
332 }
333 ProfileFileError::InvalidCredentialSource { profile, message } => {
334 write!(f, "invalid credential source in `{}`: {}", profile, message)
335 }
336 ProfileFileError::MissingProfile { profile, message } => {
337 write!(f, "profile `{}` was not defined: {}", profile, message)
338 }
339 ProfileFileError::UnknownProvider { name } => write!(
340 f,
341 "profile referenced `{}` provider but that provider is not supported",
342 name
343 ),
344 ProfileFileError::NoProfilesDefined => write!(f, "No profiles were defined"),
345 ProfileFileError::ProfileDidNotContainCredentials { profile } => write!(
346 f,
347 "profile `{}` did not contain credential information",
348 profile
349 ),
350 ProfileFileError::FeatureNotEnabled { feature, message } => {
351 let message = message.as_deref().unwrap_or_default();
352 write!(
353 f,
354 "This behavior requires following cargo feature(s) enabled: {feature}. {message}",
355 )
356 }
357 ProfileFileError::MissingSsoSession {
358 profile,
359 sso_session,
360 } => {
361 write!(f, "sso-session named `{sso_session}` (referenced by profile `{profile}`) was not found")
362 }
363 ProfileFileError::InvalidSsoConfig { profile, message } => {
364 write!(f, "profile `{profile}` has invalid SSO config: {message}")
365 }
366 ProfileFileError::TokenProviderConfig { .. } => {
367 write!(
368 f,
369 "selected profile will resolve an access token instead of credentials \
370 since it doesn't have `sso_account_id` and `sso_role_name` set. Specify both \
371 `sso_account_id` and `sso_role_name` to let this profile resolve credentials."
372 )
373 }
374 }
375 }
376}
377
378#[derive(Debug, Default)]
380pub struct Builder {
381 provider_config: Option<ProviderConfig>,
382 profile_override: Option<String>,
383 #[allow(deprecated)]
384 profile_files: Option<ProfileFiles>,
385 custom_providers: HashMap<Cow<'static, str>, Arc<dyn ProvideCredentials>>,
386}
387
388impl Builder {
389 pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
403 self.provider_config = Some(provider_config.clone());
404 self
405 }
406
407 pub fn with_custom_provider(
435 mut self,
436 name: impl Into<Cow<'static, str>>,
437 provider: impl ProvideCredentials + 'static,
438 ) -> Self {
439 self.custom_providers
440 .insert(name.into(), Arc::new(provider));
441 self
442 }
443
444 pub fn profile_name(mut self, profile_name: impl Into<String>) -> Self {
446 self.profile_override = Some(profile_name.into());
447 self
448 }
449
450 #[allow(deprecated)]
452 pub fn profile_files(mut self, profile_files: ProfileFiles) -> Self {
453 self.profile_files = Some(profile_files);
454 self
455 }
456
457 pub fn build(self) -> ProfileFileCredentialsProvider {
459 let build_span = tracing::debug_span!("build_profile_file_credentials_provider");
460 let _enter = build_span.enter();
461 let conf = self
462 .provider_config
463 .unwrap_or_default()
464 .with_profile_config(self.profile_files, self.profile_override);
465 let mut named_providers = self.custom_providers.clone();
466 named_providers
467 .entry("Environment".into())
468 .or_insert_with(|| {
469 Arc::new(crate::environment::credentials::EnvironmentVariableCredentialsProvider::new_with_env(
470 conf.env(),
471 ))
472 });
473
474 named_providers
475 .entry("Ec2InstanceMetadata".into())
476 .or_insert_with(|| {
477 Arc::new(
478 crate::imds::credentials::ImdsCredentialsProvider::builder()
479 .configure(&conf)
480 .build(),
481 )
482 });
483
484 named_providers
485 .entry("EcsContainer".into())
486 .or_insert_with(|| {
487 Arc::new(
488 crate::ecs::EcsCredentialsProvider::builder()
489 .configure(&conf)
490 .build(),
491 )
492 });
493 let factory = exec::named::NamedProviderFactory::new(named_providers);
494
495 ProfileFileCredentialsProvider {
496 config: Arc::new(Config {
497 factory,
498 provider_config: conf,
499 }),
500 inner_provider: ErrorTakingOnceCell::new(),
501 }
502 }
503}
504
505async fn build_provider_chain(
506 config: Arc<Config>,
507) -> Result<exec::ProviderChain, ProfileFileError> {
508 let profile_set = config
509 .provider_config
510 .try_profile()
511 .await
512 .map_err(|parse_err| ProfileFileError::InvalidProfile(parse_err.clone()))?;
513 let repr = repr::resolve_chain(profile_set)?;
514 tracing::info!(chain = ?repr, "constructed abstract provider from config file");
515 exec::ProviderChain::from_repr(&config.provider_config, repr, &config.factory)
516}
517
518#[derive(Debug)]
519struct ChainProvider {
520 config: Arc<Config>,
521 chain: Option<Arc<exec::ProviderChain>>,
522}
523
524impl ChainProvider {
525 async fn provide_credentials(&self) -> Result<Credentials, CredentialsError> {
526 let config = self.config.clone();
528 let chain = self.chain.clone();
529
530 if let Some(chain) = chain {
531 let mut creds = match chain
532 .base()
533 .provide_credentials()
534 .instrument(tracing::debug_span!("load_base_credentials"))
535 .await
536 {
537 Ok(creds) => {
538 tracing::info!(creds = ?creds, "loaded base credentials");
539 creds
540 }
541 Err(e) => {
542 tracing::warn!(error = %DisplayErrorContext(&e), "failed to load base credentials");
543 return Err(CredentialsError::provider_error(e));
544 }
545 };
546
547 let sdk_config = config.provider_config.client_config();
550 for provider in chain.chain().iter() {
551 let next_creds = provider
552 .credentials(creds, &sdk_config)
553 .instrument(tracing::debug_span!("load_assume_role", provider = ?provider))
554 .await;
555 match next_creds {
556 Ok(next_creds) => {
557 tracing::info!(creds = ?next_creds, "loaded assume role credentials");
558 creds = next_creds
559 }
560 Err(e) => {
561 tracing::warn!(provider = ?provider, "failed to load assume role credentials");
562 return Err(CredentialsError::provider_error(e));
563 }
564 }
565 }
566 Ok(creds)
567 } else {
568 Err(CredentialsError::not_loaded_no_source())
569 }
570 }
571}
572
573#[cfg(test)]
574mod test {
575 use crate::profile::credentials::Builder;
576 use aws_credential_types::provider::ProvideCredentials;
577
578 macro_rules! make_test {
579 ($name: ident) => {
580 #[tokio::test]
581 async fn $name() {
582 let _ = crate::test_case::TestEnvironment::from_dir(
583 concat!("./test-data/profile-provider/", stringify!($name)),
584 crate::test_case::test_credentials_provider(|config| async move {
585 Builder::default()
586 .configure(&config)
587 .build()
588 .provide_credentials()
589 .await
590 }),
591 )
592 .await
593 .unwrap()
594 .execute()
595 .await;
596 }
597 };
598 }
599
600 make_test!(e2e_assume_role);
601 make_test!(e2e_fips_and_dual_stack_sts);
602 make_test!(empty_config);
603 make_test!(retry_on_error);
604 make_test!(invalid_config);
605 make_test!(region_override);
606 #[cfg(all(feature = "credentials-process", not(windows)))]
608 make_test!(credential_process);
609 #[cfg(all(feature = "credentials-process", not(windows)))]
611 make_test!(credential_process_failure);
612 #[cfg(all(feature = "credentials-process", not(windows)))]
614 make_test!(credential_process_account_id_fallback);
615 #[cfg(feature = "credentials-process")]
616 make_test!(credential_process_invalid);
617 #[cfg(feature = "sso")]
618 make_test!(sso_credentials);
619 #[cfg(feature = "sso")]
620 make_test!(invalid_sso_credentials_config);
621 #[cfg(feature = "sso")]
622 make_test!(sso_override_global_env_url);
623 #[cfg(feature = "sso")]
624 make_test!(sso_token);
625
626 make_test!(assume_role_override_global_env_url);
627 make_test!(assume_role_override_service_env_url);
628 make_test!(assume_role_override_global_profile_url);
629 make_test!(assume_role_override_service_profile_url);
630}
631
632#[cfg(all(test, feature = "sso"))]
633mod sso_tests {
634 use crate::{profile::credentials::Builder, provider_config::ProviderConfig};
635 use aws_credential_types::credential_feature::AwsCredentialFeature;
636 use aws_credential_types::provider::ProvideCredentials;
637 use aws_sdk_sso::config::RuntimeComponents;
638 use aws_smithy_runtime_api::client::{
639 http::{
640 HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings,
641 SharedHttpConnector,
642 },
643 orchestrator::{HttpRequest, HttpResponse},
644 };
645 use aws_smithy_types::body::SdkBody;
646 use aws_types::os_shim_internal::{Env, Fs};
647 use std::collections::HashMap;
648
649 #[derive(Debug)]
650 struct ClientInner {
651 expected_token: &'static str,
652 }
653 impl HttpConnector for ClientInner {
654 fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
655 assert_eq!(
656 self.expected_token,
657 request.headers().get("x-amz-sso_bearer_token").unwrap()
658 );
659 HttpConnectorFuture::ready(Ok(HttpResponse::new(
660 200.try_into().unwrap(),
661 SdkBody::from("{\"roleCredentials\":{\"accessKeyId\":\"ASIARTESTID\",\"secretAccessKey\":\"TESTSECRETKEY\",\"sessionToken\":\"TESTSESSIONTOKEN\",\"expiration\": 1651516560000}}"),
662 )))
663 }
664 }
665 #[derive(Debug)]
666 struct Client {
667 inner: SharedHttpConnector,
668 }
669 impl Client {
670 fn new(expected_token: &'static str) -> Self {
671 Self {
672 inner: SharedHttpConnector::new(ClientInner { expected_token }),
673 }
674 }
675 }
676 impl HttpClient for Client {
677 fn http_connector(
678 &self,
679 _settings: &HttpConnectorSettings,
680 _components: &RuntimeComponents,
681 ) -> SharedHttpConnector {
682 self.inner.clone()
683 }
684 }
685
686 fn create_test_fs() -> Fs {
687 Fs::from_map({
688 let mut map = HashMap::new();
689 map.insert(
690 "/home/.aws/config".to_string(),
691 br#"
692[profile default]
693sso_session = dev
694sso_account_id = 012345678901
695sso_role_name = SampleRole
696region = us-east-1
697
698[sso-session dev]
699sso_region = us-east-1
700sso_start_url = https://d-abc123.awsapps.com/start
701 "#
702 .to_vec(),
703 );
704 map.insert(
705 "/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json".to_string(),
706 br#"
707 {
708 "accessToken": "secret-access-token",
709 "expiresAt": "2199-11-14T04:05:45Z",
710 "refreshToken": "secret-refresh-token",
711 "clientId": "ABCDEFG323242423121312312312312312",
712 "clientSecret": "ABCDE123",
713 "registrationExpiresAt": "2199-03-06T19:53:17Z",
714 "region": "us-east-1",
715 "startUrl": "https://d-abc123.awsapps.com/start"
716 }
717 "#
718 .to_vec(),
719 );
720 map
721 })
722 }
723
724 #[cfg_attr(windows, ignore)]
726 #[tokio::test]
729 async fn create_inner_provider_exactly_once() {
730 let fs = create_test_fs();
731
732 let provider_config = ProviderConfig::empty()
733 .with_fs(fs.clone())
734 .with_env(Env::from_slice(&[("HOME", "/home")]))
735 .with_http_client(Client::new("secret-access-token"));
736 let provider = Builder::default().configure(&provider_config).build();
737
738 let first_creds = provider.provide_credentials().await.unwrap();
739
740 fs.write(
743 "/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json",
744 r#"
745 {
746 "accessToken": "NEW!!secret-access-token",
747 "expiresAt": "2199-11-14T04:05:45Z",
748 "refreshToken": "secret-refresh-token",
749 "clientId": "ABCDEFG323242423121312312312312312",
750 "clientSecret": "ABCDE123",
751 "registrationExpiresAt": "2199-03-06T19:53:17Z",
752 "region": "us-east-1",
753 "startUrl": "https://d-abc123.awsapps.com/start"
754 }
755 "#,
756 )
757 .await
758 .unwrap();
759
760 let second_creds = provider
763 .provide_credentials()
764 .await
765 .expect("used cached token instead of loading from the file system");
766 assert_eq!(first_creds, second_creds);
767
768 let provider_config = ProviderConfig::empty()
772 .with_fs(fs.clone())
773 .with_env(Env::from_slice(&[("HOME", "/home")]))
774 .with_http_client(Client::new("NEW!!secret-access-token"));
775 let provider = Builder::default().configure(&provider_config).build();
776 let third_creds = provider.provide_credentials().await.unwrap();
777 assert_eq!(second_creds, third_creds);
778 }
779
780 #[cfg_attr(windows, ignore)]
781 #[tokio::test]
782 async fn credential_feature() {
783 let fs = create_test_fs();
784
785 let provider_config = ProviderConfig::empty()
786 .with_fs(fs.clone())
787 .with_env(Env::from_slice(&[("HOME", "/home")]))
788 .with_http_client(Client::new("secret-access-token"));
789 let provider = Builder::default().configure(&provider_config).build();
790
791 let creds = provider.provide_credentials().await.unwrap();
792
793 assert_eq!(
794 &vec![
795 AwsCredentialFeature::CredentialsSso,
796 AwsCredentialFeature::CredentialsProfile
797 ],
798 creds.get_property::<Vec<AwsCredentialFeature>>().unwrap()
799 )
800 }
801}