1pub fn resolve(value: &str) -> anyhow::Result<String> {
7 resolve_with("passbox", value)
8}
9
10pub async fn config_secret(key: &str) -> anyhow::Result<String> {
13 static CACHE: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, String>>> =
14 std::sync::OnceLock::new();
15 let cache = CACHE.get_or_init(Default::default);
16
17 let value = crate::config::get_global(key)?;
19 if let Some(secret) = cache
20 .lock()
21 .unwrap_or_else(std::sync::PoisonError::into_inner)
22 .get(&value)
23 {
24 return Ok(secret.clone());
25 }
26
27 let raw = value.clone();
28 let secret = tokio::task::spawn_blocking(move || resolve(&raw))
29 .await?
30 .inspect_err(|e| tracing::warn!(key, err = %e, "config secret unavailable"))?;
31 cache
32 .lock()
33 .unwrap_or_else(std::sync::PoisonError::into_inner)
34 .insert(value, secret.clone());
35 Ok(secret)
36}
37
38fn expand_env(value: &str) -> anyhow::Result<String> {
39 let Some(name) = env_name(value) else {
40 return Ok(value.to_string());
41 };
42 match std::env::var(name) {
43 Ok(v) if !v.is_empty() => Ok(v),
44 _ => anyhow::bail!("environment variable {name} is unset or empty"),
45 }
46}
47
48fn env_name(value: &str) -> Option<&str> {
50 let name = value.trim().strip_prefix("${")?.strip_suffix('}')?;
51 let valid = !name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
52 valid.then_some(name)
53}
54
55fn resolve_with(passbox: &str, value: &str) -> anyhow::Result<String> {
56 let Some(name) = value.strip_prefix("passbox:") else {
57 return expand_env(value);
58 };
59
60 let out = std::process::Command::new(passbox)
63 .args(["exec", "--stdin", name, "--", "/bin/cat"])
64 .stdin(std::process::Stdio::null())
65 .output()?;
66
67 if !out.status.success() {
68 let reason = String::from_utf8_lossy(&out.stderr);
69 let reason = reason.lines().next().unwrap_or("no error output").trim();
70 anyhow::bail!("passbox could not read {name}: {reason}");
71 }
72
73 let text = String::from_utf8(out.stdout)?;
75 let secret = text.lines().next().unwrap_or_default().to_string();
76 anyhow::ensure!(!secret.is_empty(), "passbox returned no value for {name}");
77 Ok(secret)
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 fn stub(dir: &std::path::Path, script: &str) -> String {
85 use std::os::unix::fs::PermissionsExt;
86 let path = dir.join("passbox");
87 std::fs::write(&path, format!("#!/bin/sh\n{script}\n")).unwrap();
88 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
89 path.to_string_lossy().into_owned()
90 }
91
92 #[test]
93 fn a_plain_value_is_used_as_written() {
94 assert_eq!(resolve_with("/nonexistent", "plain").unwrap(), "plain");
95 assert_eq!(resolve_with("/nonexistent", "a${B}c").unwrap(), "a${B}c");
96 }
97
98 #[test]
99 fn an_env_reference_reads_the_variable() {
100 unsafe { std::env::set_var("ORCH_SECRET_REF_TEST_SET", "from-env") };
103 assert_eq!(
104 resolve_with("/nonexistent", "${ORCH_SECRET_REF_TEST_SET}").unwrap(),
105 "from-env"
106 );
107 let err = resolve_with("/nonexistent", "${ORCH_SECRET_REF_TEST_UNSET}")
108 .unwrap_err()
109 .to_string();
110 assert!(
111 err.contains("ORCH_SECRET_REF_TEST_UNSET is unset or empty"),
112 "{err}"
113 );
114 assert_eq!(env_name("${A-B}"), None);
115 assert_eq!(env_name("${}"), None);
116 }
117
118 #[test]
119 fn a_passbox_reference_reads_the_first_line() {
120 let dir = tempfile::tempdir().unwrap();
121 let bin = stub(
122 dir.path(),
123 r#"[ "$1 $2 $3 $4 $5" = "exec --stdin orch/relay-token -- /bin/cat" ] && printf 's3cret\nnote: x\n'"#,
124 );
125 assert_eq!(
126 resolve_with(&bin, "passbox:orch/relay-token").unwrap(),
127 "s3cret"
128 );
129 }
130
131 #[test]
132 fn a_failed_or_empty_read_is_an_error() {
133 let dir = tempfile::tempdir().unwrap();
134 let err = resolve_with(
135 &stub(dir.path(), "echo 'error: denied' >&2; exit 1"),
136 "passbox:x",
137 )
138 .unwrap_err()
139 .to_string();
140 assert!(
141 err.contains("passbox could not read x: error: denied"),
142 "{err}"
143 );
144 let empty = tempfile::tempdir().unwrap();
145 assert!(resolve_with(&stub(empty.path(), "true"), "passbox:x").is_err());
146 }
147}