persistant_relational/
secret.rs1use crate::RelError;
3use std::fmt;
4use std::path::PathBuf;
5
6#[derive(Clone, PartialEq, Eq)]
18pub enum ConnectionSecret {
19 Env(String),
21 File(PathBuf),
23 Literal(Redacted),
26}
27
28impl ConnectionSecret {
29 pub fn literal(s: impl Into<String>) -> Self {
31 ConnectionSecret::Literal(Redacted::new(s))
32 }
33
34 pub fn resolve(&self) -> Result<Redacted, RelError> {
36 match self {
37 ConnectionSecret::Env(name) => std::env::var(name)
38 .map(Redacted::new)
39 .map_err(|_| RelError::Backend(format!("environment variable {name} is not set"))),
40 ConnectionSecret::File(path) => std::fs::read_to_string(path)
41 .map(|s| Redacted::new(s.trim_end().to_string()))
42 .map_err(|e| {
43 RelError::Backend(format!(
44 "connection secret file {} could not be read: {}",
45 path.display(),
46 e.kind()
47 ))
48 }),
49 ConnectionSecret::Literal(r) => Ok(r.clone()),
50 }
51 }
52}
53
54impl fmt::Debug for ConnectionSecret {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 match self {
57 ConnectionSecret::Env(n) => write!(f, "ConnectionSecret::Env({n:?})"),
58 ConnectionSecret::File(p) => write!(f, "ConnectionSecret::File({p:?})"),
59 ConnectionSecret::Literal(_) => f.write_str("ConnectionSecret::Literal(<redacted>)"),
60 }
61 }
62}
63
64#[derive(Clone, PartialEq, Eq)]
66pub struct Redacted(String);
67
68impl Redacted {
69 pub fn new(s: impl Into<String>) -> Self {
71 Redacted(s.into())
72 }
73
74 pub fn expose(&self) -> &str {
76 &self.0
77 }
78}
79
80impl fmt::Debug for Redacted {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 f.write_str("<redacted>")
83 }
84}
85
86impl fmt::Display for Redacted {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 f.write_str("<redacted>")
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 const SECRET: &str = "postgres://u:hunter2@db/x";
97
98 #[test]
99 fn nothing_prints_the_literal() {
100 let c = ConnectionSecret::literal(SECRET);
101 assert!(!format!("{c:?}").contains("hunter2"));
102 let r = c.resolve().unwrap();
103 assert!(!format!("{r:?} {r}").contains("hunter2"));
104 assert_eq!(
105 r.expose(),
106 SECRET,
107 "control: the text is still reachable on purpose"
108 );
109 }
110
111 #[test]
112 fn a_reference_prints_its_name_and_resolution_errors_never_print_a_value() {
113 let e = ConnectionSecret::Env("PERSISTANT_TEST_SURELY_UNSET_DSN".to_string());
114 assert!(format!("{e:?}").contains("PERSISTANT_TEST_SURELY_UNSET_DSN"));
115 let err = e.resolve().unwrap_err().to_string();
116 assert!(err.contains("PERSISTANT_TEST_SURELY_UNSET_DSN"), "{err}");
117 }
118
119 #[test]
120 fn a_file_secret_is_trimmed_and_unreadable_files_name_the_path_only() {
121 let dir = std::env::temp_dir().join(format!("persistant-secret-{}", std::process::id()));
122 std::fs::create_dir_all(&dir).unwrap();
123 let p = dir.join("dsn");
124 std::fs::write(&p, format!("{SECRET}\n")).unwrap();
125 let r = ConnectionSecret::File(p.clone()).resolve().unwrap();
126 assert_eq!(r.expose(), SECRET);
127 std::fs::remove_dir_all(&dir).unwrap();
128 let err = ConnectionSecret::File(p).resolve().unwrap_err().to_string();
129 assert!(!err.contains("hunter2"), "{err}");
130 }
131}