Skip to main content

credentials/
secretfile.rs

1//! Map application-level credential names to secrets in the backend store.
2//!
3//! In the case of Vault, this is necessary to transform
4//! environment-variable-style credential names into Vault secret paths and
5//! keys: from `MY_SECRET_PASSWORD` to the path `secret/my_secret` and the
6//! key `"password"`.
7
8use lazy_static::lazy_static;
9use regex::{Captures, Regex};
10use std::cell::RefCell;
11use std::collections::{btree_map, BTreeMap};
12use std::env;
13use std::fs::File;
14use std::io::{self, BufRead};
15use std::iter::Iterator;
16use std::path::Path;
17use std::str::FromStr;
18use std::sync::Mutex;
19
20use crate::errors::*;
21
22lazy_static! {
23    // For command-line binaries used directly by users, it may occasionally be
24    // desirable to build a `Secretfile` directly into an executable.
25    //
26    // For an explanation of `lazy_static!`, `Mutex` and the other funky Rust
27    // stuff going on here, see `CLIENT` in `lib.rs`.
28    static ref BUILT_IN_SECRETFILE: Mutex<RefCell<Option<Secretfile>>> =
29        Mutex::new(RefCell::new(None));
30}
31
32/// Interpolate environment variables into a string.
33fn interpolate_env(text: &str) -> Result<String> {
34    // Only compile this Regex once.
35    lazy_static! {
36        static ref RE: Regex = Regex::new(
37            r"(?x)
38\$(?:
39    (?P<name>[a-zA-Z_][a-zA-Z0-9_]*)
40  |
41    \{(?P<name2>[a-zA-Z_][a-zA-Z0-9_]*)\}
42  )"
43        )
44        .unwrap();
45    }
46
47    // Perform the replacement.  This is mostly error-handling logic,
48    // because `replace_all` doesn't anticipate any errors.
49    let mut err = None;
50    let result = RE.replace_all(text, |caps: &Captures<'_>| {
51        let name = caps
52            .name("name")
53            .or_else(|| caps.name("name2"))
54            .unwrap()
55            .as_str();
56        match env::var(name) {
57            Ok(s) => s.to_owned(),
58            Err(env_err) => {
59                err = Some(Error::UndefinedEnvironmentVariable {
60                    name: name.to_owned(),
61                    cause: env_err,
62                });
63                "".to_owned()
64            }
65        }
66    });
67    match err {
68        None => Ok(result.into_owned()),
69        Some(err) => Err(err),
70    }
71}
72
73/// The location of a secret in a given backend.  This is exported to the
74/// rest of this crate, but isn't part of the public `Secretfile` API,
75/// because we might add more types of locations in the future.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum Location {
78    // Used for systems which identify credentials with simple string keys.
79    Path(String),
80    /// Used for systems like Vault where a path _and_ a hash key are
81    /// needed to identify a specific credential.
82    PathWithKey(String, String),
83}
84
85impl Location {
86    /// Create a new `Location` from a regex `Captures` containing the
87    /// named match `path` and optionally `key`.
88    fn from_caps<'a>(caps: &Captures<'a>) -> Result<Location> {
89        let path_opt = caps.name("path").map(|m| m.as_str());
90        let key_opt = caps.name("key").map(|m| m.as_str());
91        match (path_opt, key_opt) {
92            (Some(path), None) => Ok(Location::Path(interpolate_env(path)?)),
93            (Some(path), Some(key)) => Ok(Location::PathWithKey(
94                interpolate_env(path)?,
95                key.to_owned(),
96            )),
97            (_, _) => {
98                let all = caps.get(0).unwrap().as_str().to_owned();
99                Err(Error::Parse { input: all })
100            }
101        }
102    }
103}
104
105/// A basic interface for loading a `Secretfile` and listing the various
106/// variables and files contained inside.
107#[derive(Debug, Clone)]
108pub struct Secretfile {
109    varmap: BTreeMap<String, Location>,
110    filemap: BTreeMap<String, Location>,
111}
112
113impl Secretfile {
114    fn read_internal(read: &mut dyn io::Read) -> Result<Secretfile> {
115        // Only compile this Regex once.
116        lazy_static! {
117            // Match an individual line in a Secretfile.
118            static ref RE: Regex = Regex::new(r"(?x)
119^(?:
120   # Blank line with optional comment.
121   \s*(?:\#.*)?
122 |
123   (?:
124     # VAR
125     (?P<var>[a-zA-Z_][a-zA-Z0-9_]*)
126   |
127     # >file
128     >(?P<file>\S+)
129   )
130   \s+
131   # path/to/secret:key
132   (?P<path>\S+?)(?::(?P<key>\S+))?
133   \s*
134 )$").unwrap();
135        }
136
137        let mut sf = Secretfile {
138            varmap: BTreeMap::new(),
139            filemap: BTreeMap::new(),
140        };
141        let buffer = io::BufReader::new(read);
142        for line_or_err in buffer.lines() {
143            let line = line_or_err?;
144            match RE.captures(&line) {
145                Some(ref caps) if caps.name("path").is_some() => {
146                    let location = Location::from_caps(caps)?;
147                    if caps.name("file").is_some() {
148                        let file =
149                            interpolate_env(caps.name("file").unwrap().as_str())?;
150                        sf.filemap.insert(file, location);
151                    } else if caps.name("var").is_some() {
152                        let var = caps.name("var").unwrap().as_str().to_owned();
153                        sf.varmap.insert(var, location);
154                    }
155                }
156                Some(_) => {
157                    // Blank or comment
158                }
159                _ => {
160                    return Err(Error::Parse {
161                        input: line.to_owned(),
162                    })
163                }
164            }
165        }
166        Ok(sf)
167    }
168
169    /// Read in from an `io::Read` object.
170    pub fn read(read: &mut dyn io::Read) -> Result<Secretfile> {
171        Secretfile::read_internal(read).map_err(|err| Error::Secretfile(Box::new(err)))
172    }
173
174    /// Load the `Secretfile` at the specified path.
175    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Secretfile> {
176        let path = path.as_ref();
177        let mut file = File::open(path).map_err(|err| Error::FileRead {
178            path: path.to_owned(),
179            cause: Box::new(err.into()),
180        })?;
181        Secretfile::read(&mut file).map_err(|err| Error::FileRead {
182            path: path.to_owned(),
183            cause: Box::new(err),
184        })
185    }
186
187    /// Set a built-in `Secretfile`. This is intended for command-line
188    /// applications called directly by users, which do not normally have a
189    /// `Secretfile` in the current directory, and which probably want to ignore
190    /// one if it exists.
191    ///
192    /// This must be called before `credentials::var`.
193    pub fn set_built_in(secretfile: Option<Secretfile>) {
194        let guard = BUILT_IN_SECRETFILE
195            .lock()
196            .expect("Unable to lock `BUILT_IN_SECRETFILE`");
197        *guard.borrow_mut() = secretfile;
198    }
199
200    /// Load the default `Secretfile`. This is normally `Secretfile` in the
201    /// current working directory, but it can be overridden using
202    /// `Secretfile::set_built_in`.
203    pub fn default() -> Result<Secretfile> {
204        // We have to use some extra temporary variables to keep the borrow
205        // checker happy.
206        let guard = BUILT_IN_SECRETFILE
207            .lock()
208            .expect("Unable to lock `BUILT_IN_SECRETFILE`");
209        let built_in_opt = guard.borrow().to_owned();
210        if let Some(built_in) = built_in_opt {
211            Ok(built_in)
212        } else {
213            let mut path = env::current_dir()
214                .map_err(|err| Error::Secretfile(Box::new(err.into())))?;
215            path.push("Secretfile");
216            Secretfile::from_path(path)
217        }
218    }
219
220    /// Return an iterator over the environment variables listed in this
221    /// file.
222    pub fn vars(&self) -> SecretfileKeys<'_> {
223        SecretfileKeys {
224            keys: self.varmap.keys(),
225        }
226    }
227
228    /// Return an iterator over the credential files listed in this file.
229    pub fn files(&self) -> SecretfileKeys<'_> {
230        SecretfileKeys {
231            keys: self.filemap.keys(),
232        }
233    }
234}
235
236impl FromStr for Secretfile {
237    type Err = Error;
238
239    fn from_str(s: &str) -> Result<Secretfile> {
240        let mut cursor = io::Cursor::new(s.as_bytes());
241        Secretfile::read(&mut cursor)
242    }
243}
244
245/// Internal methods for looking up `Location`s in `Secretfile`.  These are
246/// hidden in a separate trait so that we can export them _within_ this
247/// crate, but not expose them to other crates.
248pub trait SecretfileLookup {
249    /// Fetch the backend path for a variable listed in a `Secretfile`.
250    fn var(&self, name: &str) -> Option<&Location>;
251
252    /// Fetch the backend path for a file listed in a `Secretfile`.
253    fn file(&self, name: &str) -> Option<&Location>;
254}
255
256impl SecretfileLookup for Secretfile {
257    fn var(&self, name: &str) -> Option<&Location> {
258        self.varmap.get(name)
259    }
260
261    fn file(&self, name: &str) -> Option<&Location> {
262        self.filemap.get(name)
263    }
264}
265
266/// An iterator over the keys mentioned in a `Secretfile`.
267#[derive(Clone)]
268pub struct SecretfileKeys<'a> {
269    /// Our actual iterator, wrapped up only so that we don't need to
270    /// expose the underlying implementation type in our stable API.
271    keys: btree_map::Keys<'a, String, Location>,
272}
273
274// 'a is a lifetime specifier bound to the underlying collection we're
275// iterating over, which keeps anybody from modifying it while we
276// iterating.
277impl<'a> Iterator for SecretfileKeys<'a> {
278    type Item = &'a String;
279
280    fn next(&mut self) -> Option<&'a String> {
281        self.keys.next()
282    }
283}
284
285#[test]
286fn test_parse() {
287    use std::str::FromStr;
288
289    let data = "\
290# This is a comment.
291
292FOO_USERNAME secret/$SECRET_NAME:username\n\
293FOO_PASSWORD secret/${SECRET_NAME}:password\n\
294
295# Try a Keywhiz-style secret, too.
296FOO_USERNAME2 ${SECRET_NAME}_username\n\
297
298# Credentials to copy to a file.  Interpolation allowed on the left here.
299>$SOMEDIR/.conf/key.pem secret/ssl:key_pem\n\
300";
301    env::set_var("SECRET_NAME", "foo");
302    env::set_var("SOMEDIR", "/home/foo");
303    let secretfile = Secretfile::from_str(data).unwrap();
304    assert_eq!(
305        &Location::PathWithKey("secret/foo".to_owned(), "username".to_owned()),
306        secretfile.var("FOO_USERNAME").unwrap()
307    );
308    assert_eq!(
309        &Location::PathWithKey("secret/foo".to_owned(), "password".to_owned()),
310        secretfile.var("FOO_PASSWORD").unwrap()
311    );
312    assert_eq!(
313        &Location::Path("foo_username".to_owned()),
314        secretfile.var("FOO_USERNAME2").unwrap()
315    );
316    assert_eq!(
317        &Location::PathWithKey("secret/ssl".to_owned(), "key_pem".to_owned()),
318        secretfile.file("/home/foo/.conf/key.pem").unwrap()
319    );
320
321    assert_eq!(
322        vec!["FOO_PASSWORD", "FOO_USERNAME", "FOO_USERNAME2"],
323        secretfile.vars().collect::<Vec<_>>()
324    );
325    assert_eq!(
326        vec!["/home/foo/.conf/key.pem"],
327        secretfile.files().collect::<Vec<_>>()
328    );
329}