use std::str::FromStr;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use tokio::runtime::Handle;
use tracing::info;
use super::AmazonConfig;
use crate::aws::credential::{
AssumeRoleProvider, InstanceCredentialProvider, TaskCredentialProvider, WebIdentityProvider,
};
use crate::aws::{AwsCredential, AwsCredentialProvider};
use crate::config::ConfigValue;
use crate::service::make_service;
use crate::{
ClientConfigKey, ClientOptions, Result, RetryConfig, StaticCredentialProvider,
TokenCredentialProvider,
};
static DEFAULT_METADATA_ENDPOINT: &str = "http://169.254.169.254";
#[derive(Debug, thiserror::Error)]
enum Error {
#[error("Missing AccessKeyId")]
MissingAccessKeyId,
#[error("Missing SecretAccessKey")]
MissingSecretAccessKey,
#[error("Configuration key: '{}' is not known.", key)]
UnknownConfigurationKey { key: String },
}
impl From<Error> for crate::Error {
fn from(source: Error) -> Self {
match source {
Error::UnknownConfigurationKey { key } => Self::UnknownConfigurationKey { key },
_ => Self::Generic {
source: Box::new(source),
},
}
}
}
#[derive(Debug, Default, Clone)]
pub struct AmazonBuilder {
access_key_id: Option<String>,
secret_access_key: Option<String>,
region: Option<String>,
token: Option<String>,
retry_config: RetryConfig,
imdsv1_fallback: ConfigValue<bool>,
metadata_endpoint: Option<String>,
container_credentials_relative_uri: Option<String>,
client_options: ClientOptions,
credentials: Option<AwsCredentialProvider>,
skip_signature: ConfigValue<bool>,
role_arn: Option<String>,
role_session_name: Option<String>,
sts_endpoint: Option<String>,
}
#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Serialize, Deserialize)]
#[non_exhaustive]
pub enum AmazonS3ConfigKey {
AccessKeyId,
SecretAccessKey,
Region,
DefaultRegion,
Token,
ImdsV1Fallback,
MetadataEndpoint,
ContainerCredentialsRelativeUri,
SkipSignature,
RoleArn,
RoleSessionName,
StsEndpoint,
Client(ClientConfigKey),
}
impl AsRef<str> for AmazonS3ConfigKey {
fn as_ref(&self) -> &str {
match self {
Self::AccessKeyId => "aws_access_key_id",
Self::SecretAccessKey => "aws_secret_access_key",
Self::Region => "aws_region",
Self::Token => "aws_session_token",
Self::ImdsV1Fallback => "aws_imdsv1_fallback",
Self::DefaultRegion => "aws_default_region",
Self::MetadataEndpoint => "aws_metadata_endpoint",
Self::ContainerCredentialsRelativeUri => "aws_container_credentials_relative_uri",
Self::SkipSignature => "aws_skip_signature",
Self::RoleArn => "aws_role_arn",
Self::RoleSessionName => "aws_role_session_name",
Self::StsEndpoint => "aws_sts_endpoint",
Self::Client(opt) => opt.as_ref(),
}
}
}
impl FromStr for AmazonS3ConfigKey {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"aws_access_key_id" | "access_key_id" => Ok(Self::AccessKeyId),
"aws_secret_access_key" | "secret_access_key" => Ok(Self::SecretAccessKey),
"aws_default_region" | "default_region" => Ok(Self::DefaultRegion),
"aws_region" | "region" => Ok(Self::Region),
"aws_session_token" | "aws_token" | "session_token" | "token" => Ok(Self::Token),
"aws_imdsv1_fallback" | "imdsv1_fallback" => Ok(Self::ImdsV1Fallback),
"aws_metadata_endpoint" | "metadata_endpoint" => Ok(Self::MetadataEndpoint),
"aws_container_credentials_relative_uri" => Ok(Self::ContainerCredentialsRelativeUri),
"aws_skip_signature" | "skip_signature" => Ok(Self::SkipSignature),
"aws_role_arn" | "role_arn" => Ok(Self::RoleArn),
"aws_role_session_name" | "role_session_name" => Ok(Self::RoleSessionName),
"aws_sts_endpoint" | "sts_endpoint" => Ok(Self::StsEndpoint),
"aws_allow_http" => Ok(Self::Client(ClientConfigKey::AllowHttp)),
_ => match s.strip_prefix("aws_").unwrap_or(s).parse() {
Ok(key) => Ok(Self::Client(key)),
Err(_) => Err(Error::UnknownConfigurationKey { key: s.into() }.into()),
},
}
}
}
impl AmazonBuilder {
pub fn new() -> Self {
Default::default()
}
pub fn from_env() -> Self {
let mut builder: Self = Default::default();
for (os_key, os_value) in std::env::vars_os() {
if let (Some(key), Some(value)) = (os_key.to_str(), os_value.to_str()) {
if key.starts_with("AWS_") {
if let Ok(config_key) = key.to_ascii_lowercase().parse() {
builder = builder.with_config(config_key, value);
}
}
}
}
builder
}
pub fn with_config(mut self, key: AmazonS3ConfigKey, value: impl Into<String>) -> Self {
match key {
AmazonS3ConfigKey::AccessKeyId => self.access_key_id = Some(value.into()),
AmazonS3ConfigKey::SecretAccessKey => self.secret_access_key = Some(value.into()),
AmazonS3ConfigKey::Region => self.region = Some(value.into()),
AmazonS3ConfigKey::Token => self.token = Some(value.into()),
AmazonS3ConfigKey::ImdsV1Fallback => self.imdsv1_fallback.parse(value),
AmazonS3ConfigKey::DefaultRegion => {
self.region = self.region.or_else(|| Some(value.into()))
}
AmazonS3ConfigKey::MetadataEndpoint => self.metadata_endpoint = Some(value.into()),
AmazonS3ConfigKey::ContainerCredentialsRelativeUri => {
self.container_credentials_relative_uri = Some(value.into())
}
AmazonS3ConfigKey::Client(key) => {
self.client_options = self.client_options.with_config(key, value)
}
AmazonS3ConfigKey::SkipSignature => self.skip_signature.parse(value),
AmazonS3ConfigKey::RoleArn => self.role_arn = Some(value.into()),
AmazonS3ConfigKey::RoleSessionName => self.role_session_name = Some(value.into()),
AmazonS3ConfigKey::StsEndpoint => self.sts_endpoint = Some(value.into()),
};
self
}
pub fn get_config_value(&self, key: &AmazonS3ConfigKey) -> Option<String> {
match key {
AmazonS3ConfigKey::AccessKeyId => self.access_key_id.clone(),
AmazonS3ConfigKey::SecretAccessKey => self.secret_access_key.clone(),
AmazonS3ConfigKey::Region | AmazonS3ConfigKey::DefaultRegion => self.region.clone(),
AmazonS3ConfigKey::Token => self.token.clone(),
AmazonS3ConfigKey::ImdsV1Fallback => Some(self.imdsv1_fallback.to_string()),
AmazonS3ConfigKey::MetadataEndpoint => self.metadata_endpoint.clone(),
AmazonS3ConfigKey::Client(key) => self.client_options.get_config_value(key),
AmazonS3ConfigKey::ContainerCredentialsRelativeUri => {
self.container_credentials_relative_uri.clone()
}
AmazonS3ConfigKey::SkipSignature => Some(self.skip_signature.to_string()),
AmazonS3ConfigKey::RoleArn => self.role_arn.clone(),
AmazonS3ConfigKey::RoleSessionName => self.role_session_name.clone(),
AmazonS3ConfigKey::StsEndpoint => self.sts_endpoint.clone(),
}
}
pub fn with_access_key_id(mut self, access_key_id: impl Into<String>) -> Self {
self.access_key_id = Some(access_key_id.into());
self
}
pub fn with_secret_access_key(mut self, secret_access_key: impl Into<String>) -> Self {
self.secret_access_key = Some(secret_access_key.into());
self
}
pub fn with_token(mut self, token: impl Into<String>) -> Self {
self.token = Some(token.into());
self
}
pub fn with_region(mut self, region: impl Into<String>) -> Self {
self.region = Some(region.into());
self
}
pub fn with_credentials(mut self, credentials: AwsCredentialProvider) -> Self {
self.credentials = Some(credentials);
self
}
pub fn with_allow_http(mut self, allow_http: bool) -> Self {
self.client_options = self.client_options.with_allow_http(allow_http);
self
}
pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
self.retry_config = retry_config;
self
}
pub fn with_imdsv1_fallback(mut self) -> Self {
self.imdsv1_fallback = true.into();
self
}
pub fn with_skip_signature(mut self, skip_signature: bool) -> Self {
self.skip_signature = skip_signature.into();
self
}
pub fn with_metadata_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.metadata_endpoint = Some(endpoint.into());
self
}
pub fn with_role_arn(mut self, role_arn: impl Into<String>) -> Self {
self.role_arn = Some(role_arn.into());
self
}
pub fn with_role_session_name(mut self, session_name: impl Into<String>) -> Self {
self.role_session_name = Some(session_name.into());
self
}
pub fn with_sts_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.sts_endpoint = Some(endpoint.into());
self
}
pub fn with_proxy_url(mut self, proxy_url: impl Into<String>) -> Self {
self.client_options = self.client_options.with_proxy_url(proxy_url);
self
}
pub fn with_proxy_ca_certificate(mut self, proxy_ca_certificate: impl Into<String>) -> Self {
self.client_options = self
.client_options
.with_proxy_ca_certificate(proxy_ca_certificate);
self
}
pub fn with_proxy_excludes(mut self, proxy_excludes: impl Into<String>) -> Self {
self.client_options = self.client_options.with_proxy_excludes(proxy_excludes);
self
}
pub fn with_client_options(mut self, options: ClientOptions) -> Self {
self.client_options = options;
self
}
pub fn build(self, runtime: Option<&Handle>) -> Result<AmazonConfig> {
let region = self.region.unwrap_or_else(|| "us-east-1".to_string());
let credentials = if let Some(credentials) = self.credentials {
credentials
} else if self.access_key_id.is_some() || self.secret_access_key.is_some() {
match (self.access_key_id, self.secret_access_key, self.token) {
(Some(key_id), Some(secret_key), token) => {
info!("Using Static credential provider");
let credential = AwsCredential {
key_id,
secret_key,
token,
};
Arc::new(StaticCredentialProvider::new(credential)) as _
}
(None, Some(_), _) => return Err(Error::MissingAccessKeyId.into()),
(Some(_), None, _) => return Err(Error::MissingSecretAccessKey.into()),
(None, None, _) => unreachable!(),
}
} else if let (Ok(token_path), Ok(role_arn)) = (
std::env::var("AWS_WEB_IDENTITY_TOKEN_FILE"),
std::env::var("AWS_ROLE_ARN"),
) {
info!("Using WebIdentity credential provider");
let session_name = std::env::var("AWS_ROLE_SESSION_NAME")
.unwrap_or_else(|_| "WebIdentitySession".to_string());
let endpoint = format!("https://sts.{region}.amazonaws.com");
let client = self
.client_options
.clone()
.with_allow_http(false)
.client()?;
let token = WebIdentityProvider {
token_path,
session_name,
role_arn,
endpoint,
};
let service = make_service(client.clone(), runtime);
Arc::new(TokenCredentialProvider::new(
token,
client,
service,
self.retry_config.clone(),
)) as _
} else if let Ok(full_uri) = std::env::var("AWS_CONTAINER_CREDENTIALS_FULL_URI") {
info!("Using Task credential provider (full URI)");
let auth_token_file = std::env::var("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE").ok();
let client = self.client_options.clone().with_allow_http(true).client()?;
let service = make_service(client.clone(), runtime);
Arc::new(TaskCredentialProvider {
url: full_uri,
auth_token_file,
retry: self.retry_config.clone(),
client,
service,
cache: Default::default(),
}) as _
} else if let Some(uri) = self.container_credentials_relative_uri {
info!("Using Task credential provider (relative URI)");
let auth_token_file = std::env::var("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE").ok();
let client = self.client_options.clone().with_allow_http(true).client()?;
let service = make_service(client.clone(), runtime);
Arc::new(TaskCredentialProvider {
url: format!("http://169.254.170.2{uri}"),
auth_token_file,
retry: self.retry_config.clone(),
client,
service,
cache: Default::default(),
}) as _
} else {
info!("Using Instance credential provider");
let token = InstanceCredentialProvider {
imdsv1_fallback: self.imdsv1_fallback.get()?,
metadata_endpoint: self
.metadata_endpoint
.unwrap_or_else(|| DEFAULT_METADATA_ENDPOINT.into()),
};
let client = self.client_options.metadata_client()?;
let service = make_service(client.clone(), runtime);
Arc::new(TokenCredentialProvider::new(
token,
client,
service,
self.retry_config.clone(),
)) as _
};
let credentials = if let Some(role_arn) = self.role_arn {
info!("Wrapping credentials with AssumeRole provider");
let session_name = self
.role_session_name
.unwrap_or_else(|| "AssumeRoleSession".to_string());
let endpoint = self
.sts_endpoint
.unwrap_or_else(|| format!("https://sts.{region}.amazonaws.com"));
let client = self
.client_options
.clone()
.with_allow_http(false)
.client()?;
let service = make_service(client.clone(), runtime);
Arc::new(TokenCredentialProvider::new(
AssumeRoleProvider {
role_arn,
session_name,
endpoint,
base_credentials: credentials,
region: region.clone(),
policy: None,
},
client,
service,
self.retry_config.clone(),
)) as _
} else {
credentials
};
Ok(AmazonConfig {
region,
credentials,
retry_config: self.retry_config,
client_options: self.client_options,
skip_signature: self.skip_signature.get()?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn s3_test_config_from_map() {
let aws_access_key_id = "object_store:fake_access_key_id".to_string();
let aws_secret_access_key = "object_store:fake_secret_key".to_string();
let aws_default_region = "object_store:fake_default_region".to_string();
let aws_session_token = "object_store:fake_session_token".to_string();
let options = HashMap::from([
("aws_access_key_id", aws_access_key_id.clone()),
("aws_secret_access_key", aws_secret_access_key),
("aws_default_region", aws_default_region.clone()),
("aws_session_token", aws_session_token.clone()),
]);
let builder = options
.into_iter()
.fold(AmazonBuilder::new(), |builder, (key, value)| {
builder.with_config(key.parse().unwrap(), value)
})
.with_config(AmazonS3ConfigKey::SecretAccessKey, "new-secret-key");
assert_eq!(builder.access_key_id.unwrap(), aws_access_key_id.as_str());
assert_eq!(builder.secret_access_key.unwrap(), "new-secret-key");
assert_eq!(builder.region.unwrap(), aws_default_region);
assert_eq!(builder.token.unwrap(), aws_session_token);
}
#[test]
fn s3_test_config_get_value() {
let aws_access_key_id = "object_store:fake_access_key_id".to_string();
let aws_secret_access_key = "object_store:fake_secret_key".to_string();
let aws_default_region = "object_store:fake_default_region".to_string();
let aws_session_token = "object_store:fake_session_token".to_string();
let builder = AmazonBuilder::new()
.with_config(AmazonS3ConfigKey::AccessKeyId, &aws_access_key_id)
.with_config(AmazonS3ConfigKey::SecretAccessKey, &aws_secret_access_key)
.with_config(AmazonS3ConfigKey::DefaultRegion, &aws_default_region)
.with_config(AmazonS3ConfigKey::Token, &aws_session_token);
assert_eq!(
builder
.get_config_value(&AmazonS3ConfigKey::AccessKeyId)
.unwrap(),
aws_access_key_id
);
assert_eq!(
builder
.get_config_value(&AmazonS3ConfigKey::SecretAccessKey)
.unwrap(),
aws_secret_access_key
);
assert_eq!(
builder
.get_config_value(&AmazonS3ConfigKey::DefaultRegion)
.unwrap(),
aws_default_region
);
assert_eq!(
builder.get_config_value(&AmazonS3ConfigKey::Token).unwrap(),
aws_session_token
);
}
#[test]
fn s3_default_region() {
let config = AmazonBuilder::new().build(None).unwrap();
assert_eq!(config.region, "us-east-1");
}
#[tokio::test]
async fn s3_test_proxy_url() {
let s3 = AmazonBuilder::new()
.with_access_key_id("access_key_id")
.with_secret_access_key("secret_access_key")
.with_region("region")
.with_allow_http(true)
.with_proxy_url("https://example.com")
.build(None);
assert!(s3.is_ok());
}
#[test]
fn test_invalid_config() {
let err = AmazonBuilder::new()
.with_config(AmazonS3ConfigKey::ImdsV1Fallback, "enabled")
.with_region("region")
.build(None)
.unwrap_err()
.to_string();
assert_eq!(err, "Generic error: failed to parse \"enabled\" as boolean");
}
#[test]
fn aws_test_client_opts() {
let key = "AWS_PROXY_URL";
if let Ok(config_key) = key.to_ascii_lowercase().parse() {
assert_eq!(
AmazonS3ConfigKey::Client(ClientConfigKey::ProxyUrl),
config_key
);
} else {
panic!("{key} not propagated as ClientConfigKey");
}
}
}