A secret comes from a file or from an env var, never both: two sources is an error rather than a precedence rule, which would silently use the wrong credential (CLAUDE.md Cost and safety, "The API key never appears in SQL or logs"). Errors name the sources, never the value.
What the secret is, for errors ("the target password").
14 pub what: String,
The file named in the config, if any.
16 pub file: Option<PathBuf>,
21#[derive(Debug, PartialEq, Eq)] 22pub enum SecretError { 23 Both { what: String, file: PathBuf, env: String }, 24 Neither { what: String, env: String }, 25 Unreadable { what: String, file: PathBuf, reason: String }, 26 Empty { what: String, source: String }, 27} 28 29impl fmt::Display for SecretError { 30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 31 match self { 32 SecretError::Both { what, file, env } => write!( 33 f, 34 "{what} is set twice, in the file {} and in ${env}; set exactly one", 35 file.display() 36 ), 37 SecretError::Neither { what, env } => write!( 38 f, 39 "{what} is not set: name a file in the config or set ${env}" 40 ), 41 SecretError::Unreadable { what, file, reason } => { 42 write!(f, "{what}: cannot read {}: {reason}", file.display()) 43 } 44 SecretError::Empty { what, source } => write!(f, "{what} in {source} is empty"), 45 } 46 } 47} 48 49impl std::error::Error for SecretError {}
Which source the value came from; the value itself is never kept here.
58impl Sources {
Checks the sources without reading them: the refusal to start
happens before anything connects. env is the env var's value.
61 pub fn choose(&self, env: Option<&str>) -> Result<Option<Source>, SecretError> { 62 let env = env.filter(|v| !v.is_empty()); 63 match (&self.file, env) { 64 (Some(file), Some(_)) => Err(SecretError::Both { 65 what: self.what.clone(), 66 file: file.clone(), 67 env: self.env.clone(), 68 }), 69 (Some(file), None) => Ok(Some(Source::File(file.clone()))), 70 (None, Some(_)) => Ok(Some(Source::Env(self.env.clone()))), 71 (None, None) => Ok(None), 72 } 73 }
As [Sources::choose], for a secret that must be set.
Reads the secret from the process environment and the filesystem.
84 pub fn resolve(&self, required: bool) -> Result<Option<String>, SecretError> { 85 let env = std::env::var(&self.env).ok(); 86 let source = if required { 87 Some(self.require(env.as_deref())?) 88 } else { 89 self.choose(env.as_deref())? 90 }; 91 let Some(source) = source else { 92 return Ok(None); 93 }; 94 let value = match &source { 95 Source::Env(_) => env.unwrap_or_default(), 96 Source::File(file) => read(&self.what, file)?, 97 }; 98 let value = value.trim_end_matches(['\r', '\n']).to_owned(); 99 if value.is_empty() { 100 let source = match source { 101 Source::File(f) => f.display().to_string(), 102 Source::Env(e) => format!("${e}"), 103 }; 104 return Err(SecretError::Empty { what: self.what.clone(), source }); 105 } 106 Ok(Some(value)) 107 } 108}
110fn read(what: &str, file: &Path) -> Result<String, SecretError> { 111 std::fs::read_to_string(file).map_err(|e| SecretError::Unreadable { 112 what: what.to_owned(), 113 file: file.to_owned(), 114 reason: e.to_string(), 115 }) 116} 117 118#[cfg(test)] 119mod tests { 120 use super::*; 121 122 fn sources(file: Option<&str>) -> Sources { 123 Sources { 124 what: "the target password".into(), 125 file: file.map(PathBuf::from), 126 env: "PGPASSWORD_TARGET".into(), 127 } 128 } 129 130 #[test] 131 fn both_set_is_refused_naming_the_sources_not_the_value() { 132 let err = sources(Some("/run/secrets/pw")) 133 .choose(Some("hunter2")) 134 .unwrap_err() 135 .to_string(); 136 assert!(err.contains("/run/secrets/pw"), "{err}"); 137 assert!(err.contains("$PGPASSWORD_TARGET"), "{err}"); 138 assert!(!err.contains("hunter2"), "{err}"); 139 } 140 141 #[test] 142 fn neither_set_is_refused_when_required() { 143 let err = sources(None).require(None).unwrap_err().to_string(); 144 assert!(err.contains("$PGPASSWORD_TARGET"), "{err}"); 145 assert_eq!(sources(None).choose(None), Ok(None)); 146 } 147 148 #[test] 149 fn one_source_is_chosen() { 150 assert_eq!( 151 sources(Some("/f")).require(None), 152 Ok(Source::File("/f".into())) 153 ); 154 assert_eq!( 155 sources(None).require(Some("x")), 156 Ok(Source::Env("PGPASSWORD_TARGET".into())) 157 ); 158 // An empty env var is unset, as libpq treats PGPASSWORD. 159 assert_eq!(sources(Some("/f")).choose(Some("")), Ok(Some(Source::File("/f".into())))); 160 } 161}