use async_trait::async_trait;
use chrono::{DateTime, FixedOffset, Utc};
use crate::{non_empty_env_var, AwsCredentials, CredentialsError, ProvideAwsCredentials};
#[derive(Debug, Clone)]
pub struct EnvironmentProvider {
prefix: String,
}
impl Default for EnvironmentProvider {
fn default() -> Self {
EnvironmentProvider {
prefix: "AWS".to_owned(),
}
}
}
impl EnvironmentProvider {
pub fn with_prefix(prefix: &str) -> Self {
EnvironmentProvider {
prefix: prefix.to_owned(),
}
}
}
trait EnvironmentVariableProvider {
fn prefix(&self) -> &str;
fn access_key_id_var(&self) -> String {
format!("{}_ACCESS_KEY_ID", self.prefix())
}
fn secret_access_key_var(&self) -> String {
format!("{}_SECRET_ACCESS_KEY", self.prefix())
}
fn session_token_var(&self) -> String {
format!("{}_SESSION_TOKEN", self.prefix())
}
fn credential_expiration_var(&self) -> String {
format!("{}_CREDENTIAL_EXPIRATION", self.prefix())
}
}
impl EnvironmentVariableProvider for EnvironmentProvider {
fn prefix(&self) -> &str {
self.prefix.as_str()
}
}
#[async_trait]
impl ProvideAwsCredentials for EnvironmentProvider {
async fn credentials(&self) -> Result<AwsCredentials, CredentialsError> {
let env_key = get_critical_variable(self.access_key_id_var())?;
let env_secret = get_critical_variable(self.secret_access_key_var())?;
let token = non_empty_env_var(&self.session_token_var());
let var_name = self.credential_expiration_var();
let expires_at = match non_empty_env_var(&var_name) {
Some(val) => Some(
DateTime::<FixedOffset>::parse_from_rfc3339(&val)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| {
CredentialsError::new(format!(
"Invalid {} in environment '{}': {}",
var_name, val, e
))
})?,
),
_ => None,
};
Ok(AwsCredentials::new(env_key, env_secret, token, expires_at))
}
}
fn get_critical_variable(var_name: String) -> Result<String, CredentialsError> {
non_empty_env_var(&var_name)
.ok_or_else(|| CredentialsError::new(format!("No (or empty) {} in environment", var_name)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::lock_env;
use chrono::Utc;
use std::env;
static AWS_ACCESS_KEY_ID: &str = "AWS_ACCESS_KEY_ID";
static AWS_SECRET_ACCESS_KEY: &str = "AWS_SECRET_ACCESS_KEY";
static AWS_SESSION_TOKEN: &str = "AWS_SESSION_TOKEN";
static AWS_CREDENTIAL_EXPIRATION: &str = "AWS_CREDENTIAL_EXPIRATION";
static E_NO_ACCESS_KEY_ID: &str = "No (or empty) AWS_ACCESS_KEY_ID in environment";
static E_NO_SECRET_ACCESS_KEY: &str = "No (or empty) AWS_SECRET_ACCESS_KEY in environment";
static E_INVALID_EXPIRATION: &str = "Invalid AWS_CREDENTIAL_EXPIRATION in environment";
#[tokio::test]
async fn get_temporary_credentials_from_env() {
let _guard = lock_env();
env::set_var(AWS_ACCESS_KEY_ID, "id");
env::set_var(AWS_SECRET_ACCESS_KEY, "secret");
env::set_var(AWS_SESSION_TOKEN, "token");
let result = EnvironmentProvider::default().credentials().await;
env::remove_var(AWS_ACCESS_KEY_ID);
env::remove_var(AWS_SECRET_ACCESS_KEY);
env::remove_var(AWS_SESSION_TOKEN);
assert!(result.is_ok());
let creds = result.ok().unwrap();
assert_eq!(creds.aws_access_key_id(), "id");
assert_eq!(creds.aws_secret_access_key(), "secret");
assert_eq!(creds.token(), &Some("token".to_string()));
}
#[tokio::test]
async fn get_non_temporary_credentials_from_env() {
let _guard = lock_env();
env::set_var(AWS_ACCESS_KEY_ID, "id");
env::set_var(AWS_SECRET_ACCESS_KEY, "secret");
env::remove_var(AWS_SESSION_TOKEN);
let result = EnvironmentProvider::default().credentials().await;
env::remove_var(AWS_ACCESS_KEY_ID);
env::remove_var(AWS_SECRET_ACCESS_KEY);
assert!(result.is_ok());
let creds = result.ok().unwrap();
assert_eq!(creds.aws_access_key_id(), "id");
assert_eq!(creds.aws_secret_access_key(), "secret");
assert_eq!(creds.token(), &None);
}
#[tokio::test]
async fn environment_provider_missing_key_id() {
let _guard = lock_env();
env::remove_var(AWS_ACCESS_KEY_ID);
env::set_var(AWS_SECRET_ACCESS_KEY, "secret");
env::remove_var(AWS_SESSION_TOKEN);
let result = EnvironmentProvider::default().credentials().await;
env::remove_var(AWS_SECRET_ACCESS_KEY);
assert!(result.is_err());
assert_eq!(
result.err(),
Some(CredentialsError::new(E_NO_ACCESS_KEY_ID))
);
}
#[tokio::test]
async fn environment_provider_missing_secret() {
let _guard = lock_env();
env::remove_var(AWS_SECRET_ACCESS_KEY);
env::set_var(AWS_ACCESS_KEY_ID, "id");
env::remove_var(AWS_SESSION_TOKEN);
let result = EnvironmentProvider::default().credentials().await;
env::remove_var(AWS_ACCESS_KEY_ID);
assert!(result.is_err());
assert_eq!(
result.err(),
Some(CredentialsError::new(E_NO_SECRET_ACCESS_KEY))
);
}
#[tokio::test]
async fn environment_provider_missing_credentials() {
let _guard = lock_env();
env::remove_var(AWS_SECRET_ACCESS_KEY);
env::remove_var(AWS_ACCESS_KEY_ID);
env::remove_var(AWS_SESSION_TOKEN);
let result = EnvironmentProvider::default().credentials().await;
assert!(result.is_err());
assert_eq!(
result.err(),
Some(CredentialsError::new(E_NO_ACCESS_KEY_ID))
);
}
#[tokio::test]
async fn environment_provider_bad_expiration() {
let _guard = lock_env();
env::set_var(AWS_ACCESS_KEY_ID, "id");
env::set_var(AWS_SECRET_ACCESS_KEY, "secret");
env::set_var(AWS_SESSION_TOKEN, "token");
env::set_var(AWS_CREDENTIAL_EXPIRATION, "lore ipsum");
let result = EnvironmentProvider::default().credentials().await;
env::remove_var(AWS_ACCESS_KEY_ID);
env::remove_var(AWS_SECRET_ACCESS_KEY);
env::remove_var(AWS_SESSION_TOKEN);
env::remove_var(AWS_CREDENTIAL_EXPIRATION);
assert!(result.is_err());
assert!(match &result.err() {
&Some(CredentialsError { ref message }) => message.starts_with(E_INVALID_EXPIRATION),
_ => false,
});
}
#[tokio::test]
async fn get_temporary_credentials_with_expiration_from_env() {
let _guard = lock_env();
let now = Utc::now();
let now_str = now.to_rfc3339();
env::set_var(AWS_ACCESS_KEY_ID, "id");
env::set_var(AWS_SECRET_ACCESS_KEY, "secret");
env::set_var(AWS_SESSION_TOKEN, "token");
env::set_var(AWS_CREDENTIAL_EXPIRATION, now_str);
let result = EnvironmentProvider::default().credentials().await;
env::remove_var(AWS_ACCESS_KEY_ID);
env::remove_var(AWS_SECRET_ACCESS_KEY);
env::remove_var(AWS_SESSION_TOKEN);
env::remove_var(AWS_CREDENTIAL_EXPIRATION);
assert!(result.is_ok());
let creds = result.ok().unwrap();
assert_eq!(creds.aws_access_key_id(), "id");
assert_eq!(creds.aws_secret_access_key(), "secret");
assert_eq!(creds.token(), &Some("token".to_string()));
assert_eq!(creds.expires_at(), &Some(now));
}
#[tokio::test]
async fn regression_test_rfc_3339_compat() {
let _guard = lock_env();
env::set_var(AWS_CREDENTIAL_EXPIRATION, "1996-12-19t16:39:57-08:00");
env::set_var(AWS_ACCESS_KEY_ID, "id");
env::set_var(AWS_SECRET_ACCESS_KEY, "secret");
let result = EnvironmentProvider::default().credentials().await;
env::remove_var(AWS_CREDENTIAL_EXPIRATION);
env::remove_var(AWS_ACCESS_KEY_ID);
env::remove_var(AWS_SECRET_ACCESS_KEY);
assert_eq!(
result.unwrap().expires_at().unwrap().to_rfc3339(),
"1996-12-20T00:39:57+00:00"
);
}
#[tokio::test]
async fn alternative_prefix() {
let _guard = lock_env();
let now = Utc::now();
let now_str = now.to_rfc3339();
env::set_var("MYAPP_ACCESS_KEY_ID", "id");
env::set_var("MYAPP_SECRET_ACCESS_KEY", "secret");
env::set_var("MYAPP_SESSION_TOKEN", "token");
env::set_var("MYAPP_CREDENTIAL_EXPIRATION", now_str);
let result = EnvironmentProvider::with_prefix("MYAPP")
.credentials()
.await;
env::remove_var("MYAPP_ACCESS_KEY_ID");
env::remove_var("MYAPP_SECRET_ACCESS_KEY");
env::remove_var("MYAPP_SESSION_TOKEN");
env::remove_var("MYAPP_CREDENTIAL_EXPIRATION");
assert!(result.is_ok());
let creds = result.ok().unwrap();
assert_eq!(creds.aws_access_key_id(), "id");
assert_eq!(creds.aws_secret_access_key(), "secret");
assert_eq!(creds.token(), &Some("token".to_string()));
assert_eq!(creds.expires_at(), &Some(now));
}
}