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