secret_loader/
loader.rs

1// Copyright (c) The secret-loader Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use 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/// An type that can load secrets from multiple locations
16///
17/// This enum is the main type of this crate and represents a secret
18/// that can be loaded from multiple locations. This type can be converted
19/// to a [`SecretString`](secrecy::SecretString) by using [`SecretLoader::into_secret`]
20/// or one of the `TryFrom<_>`/`TryInto<_>` implementations.
21#[derive(Debug, Clone)]
22pub enum SecretLoader {
23    /// A secret that will be loaded from an environment variable
24    Env(String),
25    /// A secret that will be loaded from a file
26    File(Utf8PathBuf),
27    /// A plaintext secret
28    Plain(Secret<String>),
29}
30
31impl SecretLoader {
32    /// Constructs a new `SecretLoader` from a provided str.
33    ///
34    /// # Examples
35    ///
36    /// ```
37    /// use secret_loader::SecretLoader;
38    ///
39    /// let env_cred = SecretLoader::new("env:SECRET");
40    /// let file_cred = SecretLoader::new("file:/some/file/path");
41    /// let plain_cred = SecretLoader::new("plaintextpasswordsarebad");
42    /// ```
43    pub fn new<S: AsRef<str>>(val: S) -> Self {
44        val.as_ref().parse().unwrap()
45    }
46
47    /// Converts a `SecretLoader` into a [`SecretString`](secrecy::SecretString)
48    ///
49    /// Use this method to actually 'load' or 'resolve' a usable `Secret`
50    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    /// Returns true if the secret will be loaded from an environment variable.
60    ///
61    /// ```
62    /// # use secret_loader::SecretLoader;
63    /// assert!(SecretLoader::new("env:SECRET").is_env());
64    /// ```
65    pub fn is_env(&self) -> bool {
66        matches!(self, Self::Env(_))
67    }
68
69    /// Returns true if the secret will be loaded from a file.
70    ///
71    /// ```
72    /// # use secret_loader::SecretLoader;
73    /// assert!(SecretLoader::new("file:/some/file/path").is_file());
74    /// ```
75    pub fn is_file(&self) -> bool {
76        matches!(self, Self::File(_))
77    }
78
79    /// Returns true if the secret is in plaintext.
80    ///
81    /// ```
82    /// # use secret_loader::SecretLoader;
83    /// assert!(SecretLoader::new("plaintextpasswordsarebad").is_plain());
84    /// ```
85    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}