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!("`{field}` was missing").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 {profiles:?}, \
327 then attempted to reload {next}",
328 ),
329 ProfileFileError::MissingCredentialSource { profile, message } => {
330 write!(f, "missing credential source in `{profile}`: {message}")
331 }
332 ProfileFileError::InvalidCredentialSource { profile, message } => {
333 write!(f, "invalid credential source in `{profile}`: {message}")
334 }
335 ProfileFileError::MissingProfile { profile, message } => {
336 write!(f, "profile `{profile}` was not defined: {message}")
337 }
338 ProfileFileError::UnknownProvider { name } => write!(
339 f,
340 "profile referenced `{name}` provider but that provider is not supported",
341 ),
342 ProfileFileError::NoProfilesDefined => write!(f, "No profiles were defined"),
343 ProfileFileError::ProfileDidNotContainCredentials { profile } => write!(
344 f,
345 "profile `{profile}` did not contain credential information"
346 ),
347 ProfileFileError::FeatureNotEnabled { feature, message } => {
348 let message = message.as_deref().unwrap_or_default();
349 write!(
350 f,
351 "This behavior requires following cargo feature(s) enabled: {feature}. {message}",
352 )
353 }
354 ProfileFileError::MissingSsoSession {
355 profile,
356 sso_session,
357 } => {
358 write!(f, "sso-session named `{sso_session}` (referenced by profile `{profile}`) was not found")
359 }
360 ProfileFileError::InvalidSsoConfig { profile, message } => {
361 write!(f, "profile `{profile}` has invalid SSO config: {message}")
362 }
363 ProfileFileError::TokenProviderConfig { .. } => {
364 write!(
365 f,
366 "selected profile will resolve an access token instead of credentials \
367 since it doesn't have `sso_account_id` and `sso_role_name` set. Specify both \
368 `sso_account_id` and `sso_role_name` to let this profile resolve credentials."
369 )
370 }
371 }
372 }
373}
374
375#[derive(Debug, Default)]
377pub struct Builder {
378 provider_config: Option<ProviderConfig>,
379 profile_override: Option<String>,
380 #[allow(deprecated)]
381 profile_files: Option<ProfileFiles>,
382 custom_providers: HashMap<Cow<'static, str>, Arc<dyn ProvideCredentials>>,
383}
384
385impl Builder {
386 pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
400 self.provider_config = Some(provider_config.clone());
401 self
402 }
403
404 pub fn with_custom_provider(
432 mut self,
433 name: impl Into<Cow<'static, str>>,
434 provider: impl ProvideCredentials + 'static,
435 ) -> Self {
436 self.custom_providers
437 .insert(name.into(), Arc::new(provider));
438 self
439 }
440
441 pub fn profile_name(mut self, profile_name: impl Into<String>) -> Self {
443 self.profile_override = Some(profile_name.into());
444 self
445 }
446
447 #[allow(deprecated)]
449 pub fn profile_files(mut self, profile_files: ProfileFiles) -> Self {
450 self.profile_files = Some(profile_files);
451 self
452 }
453
454 pub fn build(self) -> ProfileFileCredentialsProvider {
456 let build_span = tracing::debug_span!("build_profile_file_credentials_provider");
457 let _enter = build_span.enter();
458 let conf = self
459 .provider_config
460 .unwrap_or_default()
461 .with_profile_config(self.profile_files, self.profile_override);
462 let mut named_providers = self.custom_providers.clone();
463 named_providers
464 .entry("Environment".into())
465 .or_insert_with(|| {
466 Arc::new(crate::environment::credentials::EnvironmentVariableCredentialsProvider::new_with_env(
467 conf.env(),
468 ))
469 });
470
471 named_providers
472 .entry("Ec2InstanceMetadata".into())
473 .or_insert_with(|| {
474 Arc::new(
475 crate::imds::credentials::ImdsCredentialsProvider::builder()
476 .configure(&conf)
477 .build(),
478 )
479 });
480
481 named_providers
482 .entry("EcsContainer".into())
483 .or_insert_with(|| {
484 Arc::new(
485 crate::ecs::EcsCredentialsProvider::builder()
486 .configure(&conf)
487 .build(),
488 )
489 });
490 let factory = exec::named::NamedProviderFactory::new(named_providers);
491
492 ProfileFileCredentialsProvider {
493 config: Arc::new(Config {
494 factory,
495 provider_config: conf,
496 }),
497 inner_provider: ErrorTakingOnceCell::new(),
498 }
499 }
500}
501
502async fn build_provider_chain(
503 config: Arc<Config>,
504) -> Result<exec::ProviderChain, ProfileFileError> {
505 let profile_set = config
506 .provider_config
507 .try_profile()
508 .await
509 .map_err(|parse_err| ProfileFileError::InvalidProfile(parse_err.clone()))?;
510 let repr = repr::resolve_chain(profile_set)?;
511 tracing::info!(chain = ?repr, "constructed abstract provider from config file");
512 exec::ProviderChain::from_repr(&config.provider_config, repr, &config.factory)
513}
514
515#[derive(Debug)]
516struct ChainProvider {
517 config: Arc<Config>,
518 chain: Option<Arc<exec::ProviderChain>>,
519}
520
521impl ChainProvider {
522 async fn provide_credentials(&self) -> Result<Credentials, CredentialsError> {
523 let config = self.config.clone();
525 let chain = self.chain.clone();
526
527 if let Some(chain) = chain {
528 let mut creds = match chain
529 .base()
530 .provide_credentials()
531 .instrument(tracing::debug_span!("load_base_credentials"))
532 .await
533 {
534 Ok(creds) => {
535 tracing::info!(creds = ?creds, "loaded base credentials");
536 creds
537 }
538 Err(e) => {
539 tracing::warn!(error = %DisplayErrorContext(&e), "failed to load base credentials");
540 return Err(CredentialsError::provider_error(e));
541 }
542 };
543
544 let sdk_config = config.provider_config.client_config();
547 for provider in chain.chain().iter() {
548 let next_creds = provider
549 .credentials(creds, &sdk_config)
550 .instrument(tracing::debug_span!("load_assume_role", provider = ?provider))
551 .await;
552 match next_creds {
553 Ok(next_creds) => {
554 tracing::info!(creds = ?next_creds, "loaded assume role credentials");
555 creds = next_creds
556 }
557 Err(e) => {
558 tracing::warn!(provider = ?provider, "failed to load assume role credentials");
559 return Err(CredentialsError::provider_error(e));
560 }
561 }
562 }
563 Ok(creds)
564 } else {
565 Err(CredentialsError::not_loaded_no_source())
566 }
567 }
568}
569
570#[cfg(test)]
571mod test {
572 use crate::profile::credentials::Builder;
573 use aws_credential_types::provider::ProvideCredentials;
574
575 macro_rules! make_test {
576 ($name: ident) => {
577 #[tokio::test]
578 async fn $name() {
579 let _ = crate::test_case::TestEnvironment::from_dir(
580 concat!("./test-data/profile-provider/", stringify!($name)),
581 crate::test_case::test_credentials_provider(|config| async move {
582 Builder::default()
583 .configure(&config)
584 .build()
585 .provide_credentials()
586 .await
587 }),
588 )
589 .await
590 .unwrap()
591 .execute()
592 .await;
593 }
594 };
595 }
596
597 make_test!(e2e_assume_role);
598 make_test!(e2e_fips_and_dual_stack_sts);
599 make_test!(empty_config);
600 make_test!(retry_on_error);
601 make_test!(invalid_config);
602 make_test!(region_override);
603 #[cfg(all(feature = "credentials-process", not(windows)))]
605 make_test!(credential_process);
606 #[cfg(all(feature = "credentials-process", not(windows)))]
608 make_test!(credential_process_failure);
609 #[cfg(all(feature = "credentials-process", not(windows)))]
611 make_test!(credential_process_account_id_fallback);
612 #[cfg(feature = "credentials-process")]
613 make_test!(credential_process_invalid);
614 #[cfg(feature = "sso")]
615 make_test!(sso_credentials);
616 #[cfg(feature = "sso")]
617 make_test!(invalid_sso_credentials_config);
618 #[cfg(feature = "sso")]
619 make_test!(sso_override_global_env_url);
620 #[cfg(feature = "sso")]
621 make_test!(sso_token);
622
623 make_test!(assume_role_override_global_env_url);
624 make_test!(assume_role_override_service_env_url);
625 make_test!(assume_role_override_global_profile_url);
626 make_test!(assume_role_override_service_profile_url);
627}
628
629#[cfg(all(test, feature = "sso"))]
630mod sso_tests {
631 use crate::{profile::credentials::Builder, provider_config::ProviderConfig};
632 use aws_credential_types::credential_feature::AwsCredentialFeature;
633 use aws_credential_types::provider::ProvideCredentials;
634 use aws_sdk_sso::config::RuntimeComponents;
635 use aws_smithy_runtime_api::client::{
636 http::{
637 HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings,
638 SharedHttpConnector,
639 },
640 orchestrator::{HttpRequest, HttpResponse},
641 };
642 use aws_smithy_types::body::SdkBody;
643 use aws_types::os_shim_internal::{Env, Fs};
644 use std::collections::HashMap;
645
646 #[derive(Debug)]
647 struct ClientInner {
648 expected_token: &'static str,
649 }
650 impl HttpConnector for ClientInner {
651 fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
652 assert_eq!(
653 self.expected_token,
654 request.headers().get("x-amz-sso_bearer_token").unwrap()
655 );
656 HttpConnectorFuture::ready(Ok(HttpResponse::new(
657 200.try_into().unwrap(),
658 SdkBody::from("{\"roleCredentials\":{\"accessKeyId\":\"ASIARTESTID\",\"secretAccessKey\":\"TESTSECRETKEY\",\"sessionToken\":\"TESTSESSIONTOKEN\",\"expiration\": 1651516560000}}"),
659 )))
660 }
661 }
662 #[derive(Debug)]
663 struct Client {
664 inner: SharedHttpConnector,
665 }
666 impl Client {
667 fn new(expected_token: &'static str) -> Self {
668 Self {
669 inner: SharedHttpConnector::new(ClientInner { expected_token }),
670 }
671 }
672 }
673 impl HttpClient for Client {
674 fn http_connector(
675 &self,
676 _settings: &HttpConnectorSettings,
677 _components: &RuntimeComponents,
678 ) -> SharedHttpConnector {
679 self.inner.clone()
680 }
681 }
682
683 fn create_test_fs() -> Fs {
684 Fs::from_map({
685 let mut map = HashMap::new();
686 map.insert(
687 "/home/.aws/config".to_string(),
688 br#"
689[profile default]
690sso_session = dev
691sso_account_id = 012345678901
692sso_role_name = SampleRole
693region = us-east-1
694
695[sso-session dev]
696sso_region = us-east-1
697sso_start_url = https://d-abc123.awsapps.com/start
698 "#
699 .to_vec(),
700 );
701 map.insert(
702 "/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json".to_string(),
703 br#"
704 {
705 "accessToken": "secret-access-token",
706 "expiresAt": "2199-11-14T04:05:45Z",
707 "refreshToken": "secret-refresh-token",
708 "clientId": "ABCDEFG323242423121312312312312312",
709 "clientSecret": "ABCDE123",
710 "registrationExpiresAt": "2199-03-06T19:53:17Z",
711 "region": "us-east-1",
712 "startUrl": "https://d-abc123.awsapps.com/start"
713 }
714 "#
715 .to_vec(),
716 );
717 map
718 })
719 }
720
721 #[cfg_attr(windows, ignore)]
723 #[tokio::test]
726 async fn create_inner_provider_exactly_once() {
727 let fs = create_test_fs();
728
729 let provider_config = ProviderConfig::empty()
730 .with_fs(fs.clone())
731 .with_env(Env::from_slice(&[("HOME", "/home")]))
732 .with_http_client(Client::new("secret-access-token"));
733 let provider = Builder::default().configure(&provider_config).build();
734
735 let first_creds = provider.provide_credentials().await.unwrap();
736
737 fs.write(
740 "/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json",
741 r#"
742 {
743 "accessToken": "NEW!!secret-access-token",
744 "expiresAt": "2199-11-14T04:05:45Z",
745 "refreshToken": "secret-refresh-token",
746 "clientId": "ABCDEFG323242423121312312312312312",
747 "clientSecret": "ABCDE123",
748 "registrationExpiresAt": "2199-03-06T19:53:17Z",
749 "region": "us-east-1",
750 "startUrl": "https://d-abc123.awsapps.com/start"
751 }
752 "#,
753 )
754 .await
755 .unwrap();
756
757 let second_creds = provider
760 .provide_credentials()
761 .await
762 .expect("used cached token instead of loading from the file system");
763 assert_eq!(first_creds, second_creds);
764
765 let provider_config = ProviderConfig::empty()
769 .with_fs(fs.clone())
770 .with_env(Env::from_slice(&[("HOME", "/home")]))
771 .with_http_client(Client::new("NEW!!secret-access-token"));
772 let provider = Builder::default().configure(&provider_config).build();
773 let third_creds = provider.provide_credentials().await.unwrap();
774 assert_eq!(second_creds, third_creds);
775 }
776
777 #[cfg_attr(windows, ignore)]
778 #[tokio::test]
779 async fn credential_feature() {
780 let fs = create_test_fs();
781
782 let provider_config = ProviderConfig::empty()
783 .with_fs(fs.clone())
784 .with_env(Env::from_slice(&[("HOME", "/home")]))
785 .with_http_client(Client::new("secret-access-token"));
786 let provider = Builder::default().configure(&provider_config).build();
787
788 let creds = provider.provide_credentials().await.unwrap();
789
790 assert_eq!(
791 &vec![
792 AwsCredentialFeature::CredentialsSso,
793 AwsCredentialFeature::CredentialsProfile
794 ],
795 creds.get_property::<Vec<AwsCredentialFeature>>().unwrap()
796 )
797 }
798}