use super::SecretsError;
use crate::http_client::Client;
use crate::service_discovery::json_lite::{self, JsonValue};
use crate::storage::{aws_credentials::CredentialsProvider, aws_sigv4};
pub(super) fn resolve(rest: &str) -> Result<String, SecretsError> {
let (secret_id, field) = match rest.split_once('#') {
Some((id, f)) => (id, Some(f)),
None => (rest, None),
};
if secret_id.is_empty() {
return Err(SecretsError::new("aws-sm:// reference is missing a secret name"));
}
let region = std::env::var("AWS_REGION")
.or_else(|_| std::env::var("AWS_DEFAULT_REGION"))
.map_err(|_| SecretsError::new("AWS_REGION or AWS_DEFAULT_REGION must be set to resolve an aws-sm:// reference"))?;
let access_key = std::env::var("AWS_ACCESS_KEY_ID").ok();
let secret_key = std::env::var("AWS_SECRET_ACCESS_KEY").ok();
let provider = CredentialsProvider::detect(®ion, access_key, secret_key);
let creds = provider.get()?;
let (scheme_and_host, host) = match std::env::var("AWS_ENDPOINT_URL_SECRETSMANAGER") {
Ok(url) => {
let host = url.strip_prefix("https://").or_else(|| url.strip_prefix("http://")).unwrap_or(&url).to_string();
(url.trim_end_matches('/').to_string(), host)
}
Err(_) => {
let host = format!("secretsmanager.{region}.amazonaws.com");
(format!("https://{host}"), host)
}
};
let body = format!(r#"{{"SecretId":"{}"}}"#, json_escape(secret_id));
let epoch_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let signed_headers = aws_sigv4::sign(
"POST",
&host,
"/",
body.as_bytes(),
®ion,
&creds.access_key,
&creds.secret_key,
creds.session_token.as_deref(),
epoch_secs,
"secretsmanager",
);
let url = format!("{scheme_and_host}/");
let client = Client::new();
let mut request = client
.post(&url)
.header("Content-Type", "application/x-amz-json-1.1")
.header("X-Amz-Target", "secretsmanager.GetSecretValue")
.body(body.into_bytes());
for (name, value) in &signed_headers {
if name.eq_ignore_ascii_case("host") {
continue;
}
request = request.header(name, value);
}
let response = request
.send()
.map_err(|e| SecretsError::new(format!("Secrets Manager request for '{secret_id}' failed: {e}")))?;
if !response.is_success() {
return Err(SecretsError::new(format!(
"Secrets Manager returned status {} for '{secret_id}': {}",
response.status(),
response.text().unwrap_or_default()
)));
}
let resp_body = response
.text()
.map_err(|e| SecretsError::new(format!("Secrets Manager response for '{secret_id}' was not valid UTF-8: {e}")))?;
let parsed = json_lite::parse(&resp_body)
.map_err(|e| SecretsError::new(format!("failed to parse Secrets Manager response for '{secret_id}': {e}")))?;
let secret_string = parsed
.get("SecretString")
.and_then(JsonValue::as_str)
.ok_or_else(|| SecretsError::new(format!("Secrets Manager secret '{secret_id}' has no SecretString (binary secrets are not supported)")))?;
match field {
None => Ok(secret_string.to_string()),
Some(field) => {
let inner = json_lite::parse(secret_string).map_err(|e| {
SecretsError::new(format!("SecretString for '{secret_id}' is not valid JSON, but field '{field}' was requested: {e}"))
})?;
inner
.get(field)
.and_then(JsonValue::as_str)
.map(str::to_string)
.ok_or_else(|| SecretsError::new(format!("Secrets Manager secret '{secret_id}' JSON has no field '{field}'")))
}
}
}
fn json_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
_ => out.push(c),
}
}
out
}