use std::borrow::Cow;
use std::collections::HashMap;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::sync::Arc;
use aws_types::credentials::{self, future, CredentialsError, ProvideCredentials};
use aws_types::os_shim_internal::{Env, Fs};
use aws_types::region::Region;
use tracing::Instrument;
use crate::connector::expect_connector;
use crate::meta::region::ProvideRegion;
use crate::profile::credentials::exec::named::NamedProviderFactory;
use crate::profile::credentials::exec::{ClientConfiguration, ProviderChain};
use crate::profile::parser::ProfileParseError;
use crate::provider_config::ProviderConfig;
use aws_smithy_client::erase::DynConnector;
mod exec;
mod repr;
impl ProvideCredentials for ProfileFileCredentialsProvider {
fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
where
Self: 'a,
{
future::ProvideCredentials::new(self.load_credentials().instrument(tracing::info_span!(
"load_credentials",
provider = "Profile"
)))
}
}
#[derive(Debug)]
pub struct ProfileFileCredentialsProvider {
factory: NamedProviderFactory,
client_config: ClientConfiguration,
fs: Fs,
env: Env,
region: Option<Region>,
connector: DynConnector,
profile_override: Option<String>,
}
impl ProfileFileCredentialsProvider {
pub fn builder() -> Builder {
Builder::default()
}
async fn load_credentials(&self) -> credentials::Result {
let profile = build_provider_chain(
&self.fs,
&self.env,
&self.region,
&self.connector,
&self.factory,
self.profile_override.as_deref(),
)
.await;
let inner_provider = profile.map_err(|err| match err {
ProfileFileError::NoProfilesDefined
| ProfileFileError::ProfileDidNotContainCredentials { .. } => {
CredentialsError::not_loaded(err)
}
_ => CredentialsError::invalid_configuration(format!(
"ProfileFile provider could not be built: {}",
&err
)),
})?;
let mut creds = match inner_provider
.base()
.provide_credentials()
.instrument(tracing::info_span!("load_base_credentials"))
.await
{
Ok(creds) => {
tracing::info!(creds = ?creds, "loaded base credentials");
creds
}
Err(e) => {
tracing::warn!(error = %e, "failed to load base credentials");
return Err(CredentialsError::provider_error(e));
}
};
for provider in inner_provider.chain().iter() {
let next_creds = provider
.credentials(creds, &self.client_config)
.instrument(tracing::info_span!("load_assume_role", provider = ?provider))
.await;
match next_creds {
Ok(next_creds) => {
tracing::info!(creds = ?next_creds, "loaded assume role credentials");
creds = next_creds
}
Err(e) => {
tracing::warn!(provider = ?provider, "failed to load assume role credentials");
return Err(CredentialsError::provider_error(e));
}
}
}
Ok(creds)
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ProfileFileError {
#[non_exhaustive]
CouldNotParseProfile(ProfileParseError),
#[non_exhaustive]
NoProfilesDefined,
#[non_exhaustive]
ProfileDidNotContainCredentials {
profile: String,
},
#[non_exhaustive]
CredentialLoop {
profiles: Vec<String>,
next: String,
},
#[non_exhaustive]
MissingCredentialSource {
profile: String,
message: Cow<'static, str>,
},
#[non_exhaustive]
InvalidCredentialSource {
profile: String,
message: Cow<'static, str>,
},
#[non_exhaustive]
MissingProfile {
profile: String,
message: Cow<'static, str>,
},
#[non_exhaustive]
UnknownProvider {
name: String,
},
}
impl Display for ProfileFileError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ProfileFileError::CouldNotParseProfile(err) => {
write!(f, "could not parse profile file: {}", err)
}
ProfileFileError::CredentialLoop { profiles, next } => write!(
f,
"profile formed an infinite loop. first we loaded {:?}, \
then attempted to reload {}",
profiles, next
),
ProfileFileError::MissingCredentialSource { profile, message } => {
write!(f, "missing credential source in `{}`: {}", profile, message)
}
ProfileFileError::InvalidCredentialSource { profile, message } => {
write!(f, "invalid credential source in `{}`: {}", profile, message)
}
ProfileFileError::MissingProfile { profile, message } => {
write!(f, "profile `{}` was not defined: {}", profile, message)
}
ProfileFileError::UnknownProvider { name } => write!(
f,
"profile referenced `{}` provider but that provider is not supported",
name
),
ProfileFileError::NoProfilesDefined => write!(f, "No profiles were defined"),
ProfileFileError::ProfileDidNotContainCredentials { profile } => write!(
f,
"profile `{}` did not contain credential information",
profile
),
}
}
}
impl Error for ProfileFileError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ProfileFileError::CouldNotParseProfile(err) => Some(err),
_ => None,
}
}
}
#[derive(Default)]
pub struct Builder {
provider_config: Option<ProviderConfig>,
profile_override: Option<String>,
custom_providers: HashMap<Cow<'static, str>, Arc<dyn ProvideCredentials>>,
}
impl Builder {
pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
self.provider_config = Some(provider_config.clone());
self
}
pub fn with_custom_provider(
mut self,
name: impl Into<Cow<'static, str>>,
provider: impl ProvideCredentials + 'static,
) -> Self {
self.custom_providers
.insert(name.into(), Arc::new(provider));
self
}
pub fn profile_name(mut self, profile_name: impl Into<String>) -> Self {
self.profile_override = Some(profile_name.into());
self
}
pub fn build(self) -> ProfileFileCredentialsProvider {
let build_span = tracing::info_span!("build_profile_provider");
let _enter = build_span.enter();
let conf = self.provider_config.unwrap_or_default();
let mut named_providers = self.custom_providers.clone();
named_providers
.entry("Environment".into())
.or_insert_with(|| {
Arc::new(crate::environment::credentials::EnvironmentVariableCredentialsProvider::new_with_env(
conf.env(),
))
});
named_providers
.entry("Ec2InstanceMetadata".into())
.or_insert_with(|| {
Arc::new(
crate::imds::credentials::ImdsCredentialsProvider::builder()
.configure(&conf)
.build(),
)
});
named_providers
.entry("EcsContainer".into())
.or_insert_with(|| {
Arc::new(
crate::ecs::EcsCredentialsProvider::builder()
.configure(&conf)
.build(),
)
});
let factory = exec::named::NamedProviderFactory::new(named_providers);
let connector = expect_connector(conf.default_connector());
let core_client = aws_hyper::Client::new(connector.clone());
ProfileFileCredentialsProvider {
factory,
client_config: ClientConfiguration {
core_client,
region: conf.region(),
},
fs: conf.fs(),
env: conf.env(),
region: conf.region(),
connector,
profile_override: self.profile_override,
}
}
}
async fn build_provider_chain(
fs: &Fs,
env: &Env,
region: &dyn ProvideRegion,
connector: &DynConnector,
factory: &NamedProviderFactory,
profile_override: Option<&str>,
) -> Result<ProviderChain, ProfileFileError> {
let profile_set = super::parser::load(fs, env).await.map_err(|err| {
tracing::warn!(err = %err, "failed to parse profile");
ProfileFileError::CouldNotParseProfile(err)
})?;
let repr = repr::resolve_chain(&profile_set, profile_override)?;
tracing::info!(chain = ?repr, "constructed abstract provider from config file");
exec::ProviderChain::from_repr(fs.clone(), connector, region.region().await, repr, factory)
}
#[cfg(test)]
mod test {
use tracing_test::traced_test;
use crate::profile::credentials::Builder;
use crate::test_case::TestEnvironment;
macro_rules! make_test {
($name: ident) => {
#[traced_test]
#[tokio::test]
async fn $name() {
TestEnvironment::from_dir(concat!(
"./test-data/profile-provider/",
stringify!($name)
))
.unwrap()
.execute(|conf| async move { Builder::default().configure(&conf).build() })
.await
}
};
}
make_test!(e2e_assume_role);
make_test!(empty_config);
make_test!(retry_on_error);
make_test!(invalid_config);
make_test!(region_override);
}