use crate::RelError;
use std::fmt;
use std::path::PathBuf;
#[derive(Clone, PartialEq, Eq)]
pub enum ConnectionSecret {
Env(String),
File(PathBuf),
Literal(Redacted),
}
impl ConnectionSecret {
pub fn literal(s: impl Into<String>) -> Self {
ConnectionSecret::Literal(Redacted::new(s))
}
pub fn resolve(&self) -> Result<Redacted, RelError> {
match self {
ConnectionSecret::Env(name) => std::env::var(name)
.map(Redacted::new)
.map_err(|_| RelError::Backend(format!("environment variable {name} is not set"))),
ConnectionSecret::File(path) => std::fs::read_to_string(path)
.map(|s| Redacted::new(s.trim_end().to_string()))
.map_err(|e| {
RelError::Backend(format!(
"connection secret file {} could not be read: {}",
path.display(),
e.kind()
))
}),
ConnectionSecret::Literal(r) => Ok(r.clone()),
}
}
}
impl fmt::Debug for ConnectionSecret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConnectionSecret::Env(n) => write!(f, "ConnectionSecret::Env({n:?})"),
ConnectionSecret::File(p) => write!(f, "ConnectionSecret::File({p:?})"),
ConnectionSecret::Literal(_) => f.write_str("ConnectionSecret::Literal(<redacted>)"),
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct Redacted(String);
impl Redacted {
pub fn new(s: impl Into<String>) -> Self {
Redacted(s.into())
}
pub fn expose(&self) -> &str {
&self.0
}
}
impl fmt::Debug for Redacted {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("<redacted>")
}
}
impl fmt::Display for Redacted {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("<redacted>")
}
}
#[cfg(test)]
mod tests {
use super::*;
const SECRET: &str = "postgres://u:hunter2@db/x";
#[test]
fn nothing_prints_the_literal() {
let c = ConnectionSecret::literal(SECRET);
assert!(!format!("{c:?}").contains("hunter2"));
let r = c.resolve().unwrap();
assert!(!format!("{r:?} {r}").contains("hunter2"));
assert_eq!(
r.expose(),
SECRET,
"control: the text is still reachable on purpose"
);
}
#[test]
fn a_reference_prints_its_name_and_resolution_errors_never_print_a_value() {
let e = ConnectionSecret::Env("PERSISTANT_TEST_SURELY_UNSET_DSN".to_string());
assert!(format!("{e:?}").contains("PERSISTANT_TEST_SURELY_UNSET_DSN"));
let err = e.resolve().unwrap_err().to_string();
assert!(err.contains("PERSISTANT_TEST_SURELY_UNSET_DSN"), "{err}");
}
#[test]
fn a_file_secret_is_trimmed_and_unreadable_files_name_the_path_only() {
let dir = std::env::temp_dir().join(format!("persistant-secret-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = dir.join("dsn");
std::fs::write(&p, format!("{SECRET}\n")).unwrap();
let r = ConnectionSecret::File(p.clone()).resolve().unwrap();
assert_eq!(r.expose(), SECRET);
std::fs::remove_dir_all(&dir).unwrap();
let err = ConnectionSecret::File(p).resolve().unwrap_err().to_string();
assert!(!err.contains("hunter2"), "{err}");
}
}