1use std::convert::Infallible;
5use std::convert::TryFrom;
6use std::env;
7use std::fs;
8use std::str::FromStr;
9
10use camino::Utf8PathBuf;
11use secrecy::Secret;
12
13use crate::error::LoadError;
14
15#[derive(Debug, Clone)]
22pub enum SecretLoader {
23 Env(String),
25 File(Utf8PathBuf),
27 Plain(Secret<String>),
29}
30
31impl SecretLoader {
32 pub fn new<S: AsRef<str>>(val: S) -> Self {
44 val.as_ref().parse().unwrap()
45 }
46
47 pub fn into_secret(self) -> Result<Secret<String>, LoadError> {
51 let secret = match self {
52 Self::Env(env_var) => env::var(env_var)?.parse().expect("Infallible"),
53 Self::File(path) => fs::read_to_string(path)?.parse().expect("Infallible"),
54 Self::Plain(secret) => secret,
55 };
56 Ok(secret)
57 }
58
59 pub fn is_env(&self) -> bool {
66 matches!(self, Self::Env(_))
67 }
68
69 pub fn is_file(&self) -> bool {
76 matches!(self, Self::File(_))
77 }
78
79 pub fn is_plain(&self) -> bool {
86 matches!(self, Self::Plain(_))
87 }
88}
89
90impl FromStr for SecretLoader {
91 type Err = Infallible;
92
93 fn from_str(s: &str) -> Result<Self, Self::Err> {
94 let cred = match s {
95 val if val.starts_with("env:") => Self::Env(val[4..].to_owned()),
96 val if val.starts_with("file:") => Self::File(val[5..].parse()?),
97 val => Self::Plain(val.parse()?),
98 };
99 Ok(cred)
100 }
101}
102
103impl From<String> for SecretLoader {
104 fn from(s: String) -> Self {
105 s.parse().expect("Infallible")
106 }
107}
108
109impl TryFrom<SecretLoader> for Secret<String> {
110 type Error = LoadError;
111
112 fn try_from(value: SecretLoader) -> Result<Self, Self::Error> {
113 value.into_secret()
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use std::convert::TryInto;
120 use std::env;
121 use std::io::Write;
122
123 use secrecy::ExposeSecret;
124 use serial_test::serial;
125 use tempfile::NamedTempFile;
126
127 use super::*;
128
129 fn setup_env(value: Option<&str>) {
130 match value {
131 Some(value) => env::set_var("SECRET", value),
132 None => env::remove_var("SECRET"),
133 }
134 }
135
136 fn env_is_set() -> bool {
137 env::var("SECRET").is_ok()
138 }
139
140 #[test]
141 fn parse_env() {
142 let cred = "env:SECRET".parse().unwrap();
143 match cred {
144 SecretLoader::Env(env_var) => {
145 assert_eq!(env_var, "SECRET");
146 }
147 _ => panic!("Wrong loader type"),
148 }
149 }
150
151 #[test]
152 fn parse_file() {
153 let cred = "file:/home/user/.secrets".parse().unwrap();
154 match cred {
155 SecretLoader::File(path) => {
156 assert_eq!(path, "/home/user/.secrets");
157 }
158 _ => panic!("Wrong loader type"),
159 }
160 }
161
162 #[test]
163 fn parse_plain() {
164 let cred = "plaincredentialstorageisbad".parse().unwrap();
165 match cred {
166 SecretLoader::Plain(secret) => {
167 assert_eq!(secret.expose_secret(), "plaincredentialstorageisbad");
168 }
169 _ => panic!("Wrong loader type"),
170 }
171 }
172
173 #[test]
174 #[serial(Env)]
175 fn secret_from_env_present() {
176 let cred: SecretLoader = "env:SECRET".parse().unwrap();
177
178 setup_env(Some("superenvsecret"));
179 assert!(env_is_set());
180
181 let secret: Secret<String> = cred.try_into().unwrap();
182 assert_eq!(secret.expose_secret(), "superenvsecret");
183 }
184
185 #[test]
186 #[serial(Env)]
187 fn secret_from_env_missing() {
188 let cred: SecretLoader = "env:SECRET".parse().unwrap();
189
190 setup_env(None);
191 assert!(!env_is_set());
192
193 let secret: Result<Secret<String>, _> = cred.try_into();
194
195 assert!(matches!(secret.unwrap_err(), LoadError::Env(_)));
196 }
197
198 #[test]
199 fn secret_from_file_present() {
200 let mut tempfile = NamedTempFile::new().unwrap();
201 write!(tempfile, "superfilesecret").unwrap();
202 let tempfile = tempfile.into_temp_path();
203
204 let cred: SecretLoader = format!("file:{}", tempfile.display()).parse().unwrap();
205 let secret: Secret<String> = cred.try_into().unwrap();
206
207 assert_eq!(secret.expose_secret(), "superfilesecret");
208 tempfile.close().unwrap();
209 }
210
211 #[test]
212 fn secret_from_file_missing() {
213 let cred: SecretLoader = "file:/does/not/exist".parse().unwrap();
214
215 let secret: Result<Secret<String>, _> = cred.try_into();
216
217 assert!(matches!(secret.unwrap_err(), LoadError::Io(_)));
218 }
219
220 #[test]
221 fn secret_from_plain() {
222 let cred: SecretLoader = "plaincredentialstorageisbad".parse().unwrap();
223 let secret: Secret<String> = cred.try_into().unwrap();
224
225 assert_eq!(secret.expose_secret(), "plaincredentialstorageisbad");
226 }
227}