use std::borrow::Cow;
use std::time::Duration;
use aws_types::credentials::{self, future, ProvideCredentials};
use tracing::Instrument;
use crate::environment::credentials::EnvironmentVariableCredentialsProvider;
use crate::meta::credentials::{CredentialsProviderChain, LazyCachingCredentialsProvider};
use crate::meta::region::ProvideRegion;
use crate::provider_config::ProviderConfig;
#[cfg(any(feature = "rustls", feature = "native-tls"))]
pub async fn default_provider() -> impl ProvideCredentials {
DefaultCredentialsChain::builder().build().await
}
#[derive(Debug)]
pub struct DefaultCredentialsChain(LazyCachingCredentialsProvider);
impl DefaultCredentialsChain {
pub fn builder() -> Builder {
Builder::default()
}
async fn credentials(&self) -> credentials::Result {
self.0
.provide_credentials()
.instrument(tracing::info_span!("provide_credentials", provider = %"default_chain"))
.await
}
}
impl ProvideCredentials for DefaultCredentialsChain {
fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
where
Self: 'a,
{
future::ProvideCredentials::new(self.credentials())
}
}
#[derive(Debug, Default)]
pub struct Builder {
profile_file_builder: crate::profile::credentials::Builder,
web_identity_builder: crate::web_identity_token::Builder,
imds_builder: crate::imds::credentials::Builder,
ecs_builder: crate::ecs::Builder,
credential_cache: crate::meta::credentials::lazy_caching::Builder,
region_override: Option<Box<dyn ProvideRegion>>,
region_chain: crate::default_provider::region::Builder,
conf: Option<ProviderConfig>,
}
impl Builder {
pub fn region(mut self, region: impl ProvideRegion + 'static) -> Self {
self.set_region(Some(region));
self
}
pub fn set_region(&mut self, region: Option<impl ProvideRegion + 'static>) -> &mut Self {
self.region_override = region.map(|provider| Box::new(provider) as _);
self
}
pub fn load_timeout(mut self, timeout: Duration) -> Self {
self.set_load_timeout(Some(timeout));
self
}
pub fn set_load_timeout(&mut self, timeout: Option<Duration>) -> &mut Self {
self.credential_cache.set_load_timeout(timeout);
self
}
pub fn buffer_time(mut self, buffer_time: Duration) -> Self {
self.set_buffer_time(Some(buffer_time));
self
}
pub fn set_buffer_time(&mut self, buffer_time: Option<Duration>) -> &mut Self {
self.credential_cache.set_buffer_time(buffer_time);
self
}
pub fn default_credential_expiration(mut self, duration: Duration) -> Self {
self.set_default_credential_expiration(Some(duration));
self
}
pub fn set_default_credential_expiration(&mut self, duration: Option<Duration>) -> &mut Self {
self.credential_cache
.set_default_credential_expiration(duration);
self
}
pub fn with_custom_credential_source(
mut self,
name: impl Into<Cow<'static, str>>,
provider: impl ProvideCredentials + 'static,
) -> Self {
self.profile_file_builder = self
.profile_file_builder
.with_custom_provider(name, provider);
self
}
pub fn profile_name(mut self, name: &str) -> Self {
self.profile_file_builder = self.profile_file_builder.profile_name(name);
self.region_chain = self.region_chain.profile_name(name);
self
}
pub fn configure(mut self, config: ProviderConfig) -> Self {
self.region_chain = self.region_chain.configure(&config);
self.conf = Some(config);
self
}
pub async fn build(self) -> DefaultCredentialsChain {
let region = match self.region_override {
Some(provider) => provider.region().await,
None => self.region_chain.build().region().await,
};
let conf = self.conf.unwrap_or_default().with_region(region);
let env_provider = EnvironmentVariableCredentialsProvider::new_with_env(conf.env());
let profile_provider = self.profile_file_builder.configure(&conf).build();
let web_identity_token_provider = self.web_identity_builder.configure(&conf).build();
let imds_provider = self.imds_builder.configure(&conf).build();
let ecs_provider = self.ecs_builder.configure(&conf).build();
let provider_chain = CredentialsProviderChain::first_try("Environment", env_provider)
.or_else("Profile", profile_provider)
.or_else("WebIdentityToken", web_identity_token_provider)
.or_else("EcsContainer", ecs_provider)
.or_else("Ec2InstanceMetadata", imds_provider);
let cached_provider = self.credential_cache.configure(&conf).load(provider_chain);
DefaultCredentialsChain(cached_provider.build())
}
}
#[cfg(test)]
mod test {
use tracing_test::traced_test;
use aws_smithy_types::retry::{RetryConfig, RetryMode};
use aws_types::credentials::ProvideCredentials;
use aws_types::os_shim_internal::{Env, Fs};
use crate::default_provider::credentials::DefaultCredentialsChain;
use crate::default_provider::retry_config;
use crate::provider_config::ProviderConfig;
use crate::test_case::TestEnvironment;
macro_rules! make_test {
($name: ident) => {
make_test!($name, execute);
};
(update: $name:ident) => {
make_test!($name, execute_and_update);
};
(live: $name:ident) => {
make_test!($name, execute_from_live_traffic);
};
($name: ident, $func: ident) => {
#[traced_test]
#[tokio::test]
async fn $name() {
crate::test_case::TestEnvironment::from_dir(concat!(
"./test-data/default-provider-chain/",
stringify!($name)
))
.unwrap()
.$func(|conf| async {
crate::default_provider::credentials::Builder::default()
.configure(conf)
.build()
.await
})
.await
}
};
}
make_test!(prefer_environment);
make_test!(profile_static_keys);
make_test!(web_identity_token_env);
make_test!(web_identity_source_profile_no_env);
make_test!(web_identity_token_invalid_jwt);
make_test!(web_identity_token_source_profile);
make_test!(web_identity_token_profile);
make_test!(profile_name);
make_test!(profile_overrides_web_identity);
make_test!(environment_variables_blank);
make_test!(imds_token_fail);
make_test!(imds_no_iam_role);
make_test!(imds_default_chain_error);
make_test!(imds_default_chain_success);
make_test!(imds_assume_role);
make_test!(imds_config_with_no_creds);
make_test!(imds_disabled);
make_test!(imds_default_chain_retries);
make_test!(ecs_assume_role);
make_test!(ecs_credentials);
make_test!(ecs_credentials_invalid_profile);
make_test!(sso_assume_role);
make_test!(sso_no_token_file);
#[tokio::test]
async fn profile_name_override() {
let (_, conf) =
TestEnvironment::from_dir("./test-data/default-provider-chain/profile_static_keys")
.unwrap()
.provider_config()
.await;
let provider = DefaultCredentialsChain::builder()
.profile_name("secondary")
.configure(conf)
.build()
.await;
let creds = provider
.provide_credentials()
.await
.expect("creds should load");
assert_eq!(creds.access_key_id(), "correct_key_secondary");
}
#[tokio::test]
#[traced_test]
#[cfg(feature = "client-hyper")]
async fn no_providers_configured_err() {
use aws_smithy_async::rt::sleep::TokioSleep;
use aws_smithy_client::erase::boxclone::BoxCloneService;
use aws_smithy_client::never::NeverConnected;
use aws_types::credentials::CredentialsError;
use aws_types::os_shim_internal::TimeSource;
tokio::time::pause();
let conf = ProviderConfig::no_configuration()
.with_tcp_connector(BoxCloneService::new(NeverConnected::new()))
.with_time_source(TimeSource::real())
.with_sleep(TokioSleep::new());
let provider = DefaultCredentialsChain::builder()
.configure(conf)
.build()
.await;
let creds = provider
.provide_credentials()
.await
.expect_err("no providers enabled");
assert!(
matches!(creds, CredentialsError::CredentialsNotLoaded { .. }),
"should be NotLoaded: {:?}",
creds
)
}
#[tokio::test]
async fn test_returns_default_retry_config_from_empty_profile() {
let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]);
let fs = Fs::from_slice(&[("config", "[default]\n")]);
let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
let actual_retry_config = retry_config::default_provider()
.configure(&provider_config)
.retry_config()
.await;
let expected_retry_config = RetryConfig::standard();
assert_eq!(actual_retry_config, expected_retry_config);
assert_eq!(actual_retry_config.max_attempts(), 3);
assert_eq!(actual_retry_config.mode(), RetryMode::Standard);
}
#[tokio::test]
async fn test_no_retry_config_in_empty_profile() {
let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]);
let fs = Fs::from_slice(&[("config", "[default]\n")]);
let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
let actual_retry_config = retry_config::default_provider()
.configure(&provider_config)
.retry_config()
.await;
let expected_retry_config = RetryConfig::standard();
assert_eq!(actual_retry_config, expected_retry_config)
}
#[tokio::test]
async fn test_creation_of_retry_config_from_profile() {
let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]);
let fs = Fs::from_slice(&[(
"config",
r#"[default]
max_attempts = 1
retry_mode = standard
"#,
)]);
let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
let actual_retry_config = retry_config::default_provider()
.configure(&provider_config)
.retry_config()
.await;
let expected_retry_config = RetryConfig::standard().with_max_attempts(1);
assert_eq!(actual_retry_config, expected_retry_config)
}
#[tokio::test]
async fn test_env_retry_config_takes_precedence_over_profile_retry_config() {
let env = Env::from_slice(&[
("AWS_CONFIG_FILE", "config"),
("AWS_MAX_ATTEMPTS", "42"),
("AWS_RETRY_MODE", "standard"),
]);
let fs = Fs::from_slice(&[(
"config",
r#"[default]
max_attempts = 88
retry_mode = standard
"#,
)]);
let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
let actual_retry_config = retry_config::default_provider()
.configure(&provider_config)
.retry_config()
.await;
let expected_retry_config = RetryConfig::standard().with_max_attempts(42);
assert_eq!(actual_retry_config, expected_retry_config)
}
#[tokio::test]
#[should_panic = "failed to parse max attempts set by aws profile: invalid digit found in string"]
async fn test_invalid_profile_retry_config_panics() {
let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]);
let fs = Fs::from_slice(&[(
"config",
r#"[default]
max_attempts = potato
"#,
)]);
let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
let _ = retry_config::default_provider()
.configure(&provider_config)
.retry_config()
.await;
}
}