use crate::constants::{MAX_REQ_TIME_SEC, VERSION};
use aws_sdk_secretsmanager::config::interceptors::BeforeTransmitInterceptorContextMut;
use aws_sdk_secretsmanager::config::{ConfigBag, Intercept, RuntimeComponents};
#[cfg(not(test))]
use aws_sdk_secretsmanager::Client as SecretsManagerClient;
use aws_workload_credentials_provider_common::config::types::SecretsManagerConfig;
use std::env::VarError;
use std::fs;
use std::time::Duration;
#[cfg(not(test))]
use std::env::var; #[cfg(test)]
use tests::var_test as var;
#[doc(hidden)]
pub fn err_response(err_code: &str, msg: &str) -> String {
if msg.is_empty() || err_code == "InternalFailure" {
return String::from("{\"__type\":\"InternalFailure\"}");
}
serde_json::json!({
"__type": err_code,
"message": msg
})
.to_string()
}
#[doc(hidden)]
pub fn get_token(ssrf_env_variables: &[String]) -> Result<String, Box<dyn std::error::Error>> {
#[allow(clippy::redundant_closure)]
let found = ssrf_env_variables
.iter()
.map(|n| var(n))
.filter_map(|r| r.ok())
.next();
if found.is_none() {
return Err(Box::new(VarError::NotPresent));
}
let val = found.unwrap();
if !val.starts_with("file://") {
return Ok(val);
}
let file = val.strip_prefix("file://").unwrap();
Ok(fs::read_to_string(file)?.trim().to_string())
}
#[doc(hidden)]
#[cfg(not(test))]
pub use time_out_impl as time_out;
#[cfg(test)]
pub use time_out_test as time_out;
#[doc(hidden)]
pub fn time_out_impl() -> Duration {
Duration::from_secs(MAX_REQ_TIME_SEC)
}
#[cfg(test)]
pub fn time_out_test() -> Duration {
Duration::from_secs(10) }
#[doc(hidden)]
#[cfg(not(test))]
pub async fn validate_and_create_asm_client(
config: &SecretsManagerConfig,
) -> Result<SecretsManagerClient, Box<dyn std::error::Error>> {
use aws_config::{BehaviorVersion, Region};
use aws_secretsmanager_caching::error::is_transient_error;
let default_config = &aws_config::load_defaults(BehaviorVersion::latest()).await;
let mut asm_builder = aws_sdk_secretsmanager::config::Builder::from(default_config)
.interceptor(AgentModifierInterceptor);
if let Some(ref region) = config.region {
asm_builder.set_region(Some(Region::new(region.clone())));
}
if config.validate_credentials {
let mut sts_builder = aws_sdk_sts::config::Builder::from(default_config);
if let Some(ref region) = config.region {
sts_builder.set_region(Some(Region::new(region.clone())));
}
let sts_client = aws_sdk_sts::Client::from_conf(sts_builder.build());
match sts_client.get_caller_identity().send().await {
Ok(_) => (),
Err(e) if config.ignore_transient_errors && is_transient_error(&e) => (),
Err(e) => Err(e)?,
};
}
Ok(aws_sdk_secretsmanager::Client::from_conf(
asm_builder.build(),
))
}
#[doc(hidden)]
#[derive(Debug)]
pub struct AgentModifierInterceptor;
#[doc(hidden)]
#[cfg(not(test))]
pub async fn create_role_asm_client(
config: &SecretsManagerConfig,
base_config: &aws_config::SdkConfig,
role_arn: &str,
) -> Result<SecretsManagerClient, Box<dyn std::error::Error>> {
use aws_config::Region;
let provider = aws_config::sts::AssumeRoleProvider::builder(role_arn)
.configure(base_config)
.session_name("secrets-manager-provider")
.build()
.await;
let shared_provider = aws_sdk_secretsmanager::config::SharedCredentialsProvider::new(provider);
let mut sts_builder = aws_sdk_sts::config::Builder::from(base_config)
.credentials_provider(shared_provider.clone());
if let Some(region) = &config.region {
sts_builder.set_region(Some(Region::new(region.clone())));
}
let sts_client = aws_sdk_sts::Client::from_conf(sts_builder.build());
sts_client.get_caller_identity().send().await?;
let mut asm_builder = aws_sdk_secretsmanager::config::Builder::from(base_config)
.credentials_provider(shared_provider)
.interceptor(AgentModifierInterceptor);
if let Some(region) = &config.region {
asm_builder.set_region(Some(Region::new(region.clone())));
}
Ok(SecretsManagerClient::from_conf(asm_builder.build()))
}
#[doc(hidden)]
impl Intercept for AgentModifierInterceptor {
fn name(&self) -> &'static str {
"AgentModifierInterceptor"
}
fn modify_before_signing(
&self,
context: &mut BeforeTransmitInterceptorContextMut<'_>,
_runtime_components: &RuntimeComponents,
_cfg: &mut ConfigBag,
) -> Result<(), aws_sdk_secretsmanager::error::BoxError> {
let request = context.request_mut();
let agent = request.headers().get("user-agent").unwrap_or_default(); let full_agent = format!(
"{agent} {}/{}",
aws_workload_credentials_provider_common::constants::PROVIDER_NAME,
VERSION.unwrap_or("0.0.0")
);
request.headers_mut().insert("user-agent", full_agent);
Ok(())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use aws_workload_credentials_provider_common::config::validator::ConfigValidator;
use std::cell::RefCell;
use std::env::temp_dir;
use std::thread_local;
use std::time::{SystemTime, UNIX_EPOCH};
pub struct CleanUp<'a> {
pub file: Option<&'a str>,
}
impl Drop for CleanUp<'_> {
fn drop(&mut self) {
ENVVAR.set(None);
if let Some(name) = self.file {
let _ = std::fs::remove_file(name);
}
}
}
pub fn tmpfile_name(suffix: &str) -> String {
format!(
"{}/{}_{:?}_{suffix}",
temp_dir().display(),
std::process::id(),
SystemTime::now().duration_since(UNIX_EPOCH).unwrap()
)
}
thread_local! {
static ENVVAR: RefCell<Option<Vec<(&'static str, &'static str)>>> = const { RefCell::new(None) };
}
pub fn set_test_var(key: &'static str, val: &'static str) {
ENVVAR.set(Some(vec![(key, val)]));
}
pub fn set_test_vars(vars: Vec<(&'static str, &'static str)>) {
ENVVAR.set(Some(vars));
}
pub fn var_test(key: &str) -> Result<String, VarError> {
if key == "FAIL_TOKEN" {
return Err(VarError::NotPresent);
}
if let Some(varvec) = ENVVAR.with_borrow(|v| v.clone()) {
let found = varvec.iter().find(|keyval| keyval.0 == key);
if let Some(some_found) = found {
return Ok(some_found.1.to_string());
}
} else {
return Ok("xyzzy".to_string()); }
Err(VarError::NotPresent) }
#[test]
fn test_env_set() {
let _cleanup = CleanUp { file: None };
set_test_var("AWS_TOKEN", "abc123");
let cfg = ConfigValidator::new()
.validate(None)
.expect("config failed");
assert_eq!(
get_token(&cfg.secrets_manager.security.ssrf_env_variables).expect("token fail"),
"abc123"
);
}
#[test]
fn test_alt_env_set() {
let _cleanup = CleanUp { file: None };
set_test_var("AWS_SESSION_TOKEN", "123abc");
let cfg = ConfigValidator::new()
.validate(None)
.expect("config failed");
assert_eq!(
get_token(&cfg.secrets_manager.security.ssrf_env_variables).expect("token fail"),
"123abc"
);
}
#[test]
fn test_file_token() {
let token = "4 chosen by fair dice roll, guaranteed to be random";
let tmpfile = tmpfile_name("test_file_token");
let _cleanup = CleanUp {
file: Some(&tmpfile),
};
std::fs::write(&tmpfile, token).expect("could not write");
let file = Box::new(format!("file://{tmpfile}"));
set_test_var("AWS_TOKEN", Box::leak(file));
let cfg = ConfigValidator::new()
.validate(None)
.expect("config failed");
assert_eq!(
get_token(&cfg.secrets_manager.security.ssrf_env_variables).expect("token fail"),
token
);
}
#[test]
fn test_file_token_missing() {
#[cfg(unix)]
const NO_SUCH_FILE_ERROR_MSG: &str = "No such file or directory (os error 2)";
#[cfg(windows)]
const NO_SUCH_FILE_ERROR_MSG: &str =
"The system cannot find the file specified. (os error 2)";
let _cleanup = CleanUp { file: None };
set_test_var("AWS_TOKEN", "file:///NoSuchFile");
let cfg = ConfigValidator::new()
.validate(None)
.expect("config failed");
assert_eq!(
get_token(&cfg.secrets_manager.security.ssrf_env_variables)
.err()
.unwrap()
.to_string(),
NO_SUCH_FILE_ERROR_MSG
);
}
#[test]
fn two_tokens() {
let _cleanup = CleanUp { file: None };
set_test_vars(vec![
("AWS_TOKEN", "yzzyx"),
("AWS_SESSION_TOKEN", "CTAtoken"),
]); let cfg = ConfigValidator::new()
.validate(None)
.expect("config failed");
assert_eq!(
get_token(&cfg.secrets_manager.security.ssrf_env_variables).expect("token fail"),
"yzzyx"
);
}
#[test]
fn test_env_fail() {
let _cleanup = CleanUp { file: None };
set_test_var("", "");
let env_vars = vec!["NOSUCHENV".to_string()];
assert!(get_token(&env_vars)
.err()
.unwrap()
.downcast_ref::<VarError>()
.unwrap()
.eq(&VarError::NotPresent));
}
#[test]
fn test_time_out() {
assert_eq!(time_out_impl(), Duration::from_secs(MAX_REQ_TIME_SEC));
}
}