Skip to content

Commit 43ff705

Browse files
feat(secrets): auto-extract API key from JSON-structured Secrets Manager secrets (#1146)
## Summary - When `DD_API_KEY_SECRET_ARN` is set and the fetched secret is a JSON object, automatically extract the `dd_api_key` field as the API key - Falls back to 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 - No new environment variable introduced; the JSON key name is hardcoded as `dd_api_key` ## Test Plan - [x] Unit tests cover JSON extraction (`dd_api_key` present), fallback to raw (key absent), and plain string secrets - [x] Verify with a real Secrets Manager secret in JSON format: `{"dd_api_key": "<your-key>"}` - [x] Verify plain-string secrets continue to work unchanged - [x] Integration test using [test function](https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions/integ-tianning-base-node-lambda?subtab=envVars&tab=testing), secret in [Json](https://us-east-1.console.aws.amazon.com/secretsmanager/secret?name=secret-in-json&region=us-east-1) and secret in [plain text](https://us-east-1.console.aws.amazon.com/secretsmanager/secret?name=securet-with-plain-text&region=us-east-1). The positive and negative tests all performed as expected. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4dab37a commit 43ff705

File tree

1 file changed

+46
-0
lines changed

1 file changed

+46
-0
lines changed

bottlecap/src/secrets/decrypt.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,20 @@ async fn decrypt_aws_sm(
202202
);
203203

204204
let v = request(json_body, headers?, client).await?;
205+
extract_secret_string(&v)
206+
}
207+
208+
// When a Secrets Manager secret is a JSON object, this key is used to extract the API key.
209+
// Falls back to the raw secret string if the key is absent or the value is not valid JSON.
210+
const JSON_SECRET_DD_API_KEY: &str = "dd_api_key";
205211

212+
fn extract_secret_string(v: &Value) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
206213
if let Some(secret_string) = v["SecretString"].as_str() {
214+
if let Ok(parsed) = serde_json::from_str::<Value>(secret_string)
215+
&& let Some(extracted) = parsed[JSON_SECRET_DD_API_KEY].as_str()
216+
{
217+
return Ok(extracted.to_string());
218+
}
207219
Ok(secret_string.to_string())
208220
} else {
209221
Err(Error::new(std::io::ErrorKind::InvalidData, v.to_string()).into())
@@ -419,6 +431,40 @@ mod tests {
419431
use super::*;
420432
use chrono::{NaiveDateTime, TimeZone};
421433

434+
fn make_sm_response(secret_string: &str) -> Value {
435+
serde_json::json!({ "SecretString": secret_string })
436+
}
437+
438+
#[test]
439+
fn test_json_secret_extraction() {
440+
let v = make_sm_response(r#"{"dd_api_key":"abc123"}"#);
441+
let result = extract_secret_string(&v).expect("should extract dd_api_key");
442+
assert_eq!(result, "abc123");
443+
}
444+
445+
#[test]
446+
fn test_json_secret_missing_key_falls_back_to_raw() {
447+
let raw = r#"{"other_key":"abc123"}"#;
448+
let v = make_sm_response(raw);
449+
let result = extract_secret_string(&v).expect("should fall back to raw JSON string");
450+
assert_eq!(result, raw);
451+
}
452+
453+
#[test]
454+
fn test_plain_secret_unaffected() {
455+
let v = make_sm_response("abc123");
456+
let result = extract_secret_string(&v).expect("should return raw value");
457+
assert_eq!(result, "abc123");
458+
}
459+
460+
#[test]
461+
fn test_malformed_json_secret_falls_back_to_raw() {
462+
let raw = r#"{"dd_api_key":"abc123""#; // missing closing brace
463+
let v = make_sm_response(raw);
464+
let result = extract_secret_string(&v).expect("should fall back to raw string");
465+
assert_eq!(result, raw);
466+
}
467+
422468
#[test]
423469
fn key_cleanup() {
424470
let key = clean_api_key(Some(" 32alxcxf\n".to_string()));

0 commit comments

Comments
 (0)