seaweed-volume: read security.toml values as TOML strings (#12582)

The line parser only trimmed quotes, so `key = "abc\n"` gave the Rust
volume server the bytes `abc\n` with a literal backslash while the Go
servers sign with a newline, and every JWT failed. Literal strings kept
their quotes and a trailing comment stayed part of the value.

Decode each value with the TOML parser; text it rejects keeps the old
quote trimming.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chris LuandClaude Opus 5.5 authored and GitHub committed 2026-11-12 09:19:18 -08:00
1 parent b7d6b48921
commit c2625ddf00
1 file changed
+43 -1
+43 -1
View File
@@ -1175,7 +1175,7 @@ fn parse_security_config_with_env(path: &str, env: EnvLookup<'_>) -> SecurityCon
if let Some((key, value)) = trimmed.split_once('=') {
let key = key.trim();
let value = value.trim().trim_matches('"');
let value: &str = &toml_value_text(value);
match section {
Section::JwtSigningRead => match key {
"key" => cfg.jwt_read_signing_key = value.as_bytes().to_vec(),
@@ -1265,6 +1265,20 @@ fn parse_security_config_with_env(path: &str, env: EnvLookup<'_>) -> SecurityCon
cfg
}
/// Reads the right-hand side of `key = value` the way TOML (and so Go) does:
/// escapes decoded, literal strings kept as written, a trailing comment
/// dropped. Text TOML rejects keeps the plain quote trimming.
fn toml_value_text(raw: &str) -> String {
match format!("v = {raw}").parse::<toml::Table>() {
Ok(mut table) => match table.remove("v") {
Some(toml::Value::String(text)) => text,
Some(other) => other.to_string(),
None => String::new(),
},
Err(_) => raw.trim().trim_matches('"').to_string(),
}
}
fn resolve_security_config_path(path: &str, env: EnvLookup<'_>) -> Option<PathBuf> {
if !path.is_empty() {
return Some(PathBuf::from(path));
@@ -1868,6 +1882,34 @@ ui = true
assert!(cfg.access_ui);
}
#[test]
fn test_parse_security_config_reads_toml_strings() {
for (encoded, expected) in [
(r#""plain""#, "plain"),
(r#""key\n""#, "key\n"),
(r#""quote\"slash\\tab\t""#, "quote\"slash\\tab\t"),
(r#""key""#, "key"),
("'literal\\key'", "literal\\key"),
(r#""key" # comment"#, "key"),
(r#""YWJj==""#, "YWJj=="),
("unquoted", "unquoted"),
] {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(
tmp.path(),
format!(
"[jwt.signing]\nkey = {encoded}\n\n[jwt.signing.read]\nkey = {encoded}\n\
expires_after_seconds = 30 # seconds\n"
),
)
.unwrap();
let cfg = parse_security_config_with_env(tmp.path().to_str().unwrap(), &|_| None);
assert_eq!(cfg.jwt_signing_key, expected.as_bytes(), "{encoded}");
assert_eq!(cfg.jwt_read_signing_key, expected.as_bytes(), "{encoded}");
assert_eq!(cfg.jwt_read_signing_expires, 30, "{encoded}");
}
}
#[test]
fn test_parse_security_config_discovers_current_directory_default() {
let _guard = process_state_lock();