From fb78f3b718c92dd9bbbb6c591d41662dcf75f928 Mon Sep 17 00:00:00 2001 From: Tianning Li Date: Tue, 31 Mar 2026 17:36:03 -0400 Subject: [PATCH] feat(secrets): auto-extract API key from JSON-structured Secrets Manager secrets When DD_API_KEY_SECRET_ARN is set and the fetched secret is a JSON object, automatically extract the value of the hardcoded "dd_api_key" field as the API key. Falls back to using the raw secret string if the value is not valid JSON or the "dd_api_key" field is absent, preserving existing behavior for plain-string secrets. Co-Authored-By: Claude Sonnet 4.6 --- bottlecap/src/secrets/decrypt.rs | 46 ++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/bottlecap/src/secrets/decrypt.rs b/bottlecap/src/secrets/decrypt.rs index 47467c7ee..b014c3b16 100644 --- a/bottlecap/src/secrets/decrypt.rs +++ b/bottlecap/src/secrets/decrypt.rs @@ -202,8 +202,20 @@ async fn decrypt_aws_sm( ); let v = request(json_body, headers?, client).await?; + extract_secret_string(&v) +} + +// When a Secrets Manager secret is a JSON object, this key is used to extract the API key. +// Falls back to the raw secret string if the key is absent or the value is not valid JSON. +const JSON_SECRET_DD_API_KEY: &str = "dd_api_key"; +fn extract_secret_string(v: &Value) -> Result> { if let Some(secret_string) = v["SecretString"].as_str() { + if let Ok(parsed) = serde_json::from_str::(secret_string) + && let Some(extracted) = parsed[JSON_SECRET_DD_API_KEY].as_str() + { + return Ok(extracted.to_string()); + } Ok(secret_string.to_string()) } else { Err(Error::new(std::io::ErrorKind::InvalidData, v.to_string()).into()) @@ -419,6 +431,40 @@ mod tests { use super::*; use chrono::{NaiveDateTime, TimeZone}; + fn make_sm_response(secret_string: &str) -> Value { + serde_json::json!({ "SecretString": secret_string }) + } + + #[test] + fn test_json_secret_extraction() { + let v = make_sm_response(r#"{"dd_api_key":"abc123"}"#); + let result = extract_secret_string(&v).expect("should extract dd_api_key"); + assert_eq!(result, "abc123"); + } + + #[test] + fn test_json_secret_missing_key_falls_back_to_raw() { + let raw = r#"{"other_key":"abc123"}"#; + let v = make_sm_response(raw); + let result = extract_secret_string(&v).expect("should fall back to raw JSON string"); + assert_eq!(result, raw); + } + + #[test] + fn test_plain_secret_unaffected() { + let v = make_sm_response("abc123"); + let result = extract_secret_string(&v).expect("should return raw value"); + assert_eq!(result, "abc123"); + } + + #[test] + fn test_malformed_json_secret_falls_back_to_raw() { + let raw = r#"{"dd_api_key":"abc123""#; // missing closing brace + let v = make_sm_response(raw); + let result = extract_secret_string(&v).expect("should fall back to raw string"); + assert_eq!(result, raw); + } + #[test] fn key_cleanup() { let key = clean_api_key(Some(" 32alxcxf\n".to_string()));