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