credentials/
lib.rs

1//! Access secure credentials at runtime with multiple backends.
2//!
3//! For more information, see [the
4//! homepage](https://github.com/emk/credentials).
5//!
6//! ```
7//! use credentials;
8//! use std::env;
9//!
10//! env::set_var("PASSWORD", "secret");
11//! assert_eq!("secret", credentials::var("PASSWORD").unwrap());
12//! ```
13
14#![warn(missing_docs)]
15#![allow(clippy::redundant_closure)]
16
17use backend::Backend;
18use lazy_static::lazy_static;
19use log::trace;
20use std::cell::RefCell;
21use std::convert::AsRef;
22use std::default::Default;
23use std::ops::DerefMut;
24use std::path::Path;
25use std::sync::{Mutex, MutexGuard};
26
27// Be very careful not to export any more of the Secretfile API than
28// strictly necessary, because we don't want to stablize too much at this
29// point.
30pub use errors::{Error, Result};
31pub use secretfile::{Secretfile, SecretfileKeys};
32
33mod backend;
34mod chained;
35mod envvar;
36mod errors;
37mod secretfile;
38mod vault;
39
40/// Options which can be passed to `Client::new`.
41pub struct Options {
42    secretfile: Option<Secretfile>,
43    allow_override: bool,
44}
45
46impl Default for Options {
47    /// Create an `Options` object using the default values for each
48    /// option.
49    fn default() -> Options {
50        Options {
51            secretfile: None,
52            allow_override: true,
53        }
54    }
55}
56
57impl Options {
58    /// Specify a `Secretfile` for the `Client` to use.  This takes `self`
59    /// by value, so it consumes the `Options` structure it is called on,
60    /// and returns a new one.  Defaults to `Secretfile::default()`.
61    pub fn secretfile(mut self, secretfile: Secretfile) -> Options {
62        self.secretfile = Some(secretfile);
63        self
64    }
65
66    /// Allow secrets in environment variables and local files to override
67    /// the ones specified in our `Secretfile`.  Defaults to true.
68    pub fn allow_override(mut self, allow_override: bool) -> Options {
69        self.allow_override = allow_override;
70        self
71    }
72}
73
74/// A client which fetches secrets.  Under normal circumstances, it's
75/// usually easier to use the static `credentials::var` and
76/// `credentials::file` methods instead, but you may need to use this to
77/// customize behavior.
78pub struct Client {
79    secretfile: Secretfile,
80    backend: chained::Client,
81}
82
83impl Client {
84    /// Create a new client using the specified options.
85    pub fn new(options: Options) -> Result<Client> {
86        let secretfile = match options.secretfile {
87            Some(sf) => sf,
88            None => Secretfile::default()?,
89        };
90        let over = options.allow_override;
91        Ok(Client {
92            secretfile,
93            backend: chained::Client::with_default_backends(over)?,
94        })
95    }
96
97    /// Create a new client using the default options.
98    pub fn default() -> Result<Client> {
99        Client::new(Default::default())
100    }
101
102    /// Create a new client using the specified `Secretfile`.
103    pub fn with_secretfile(secretfile: Secretfile) -> Result<Client> {
104        Client::new(Options::default().secretfile(secretfile))
105    }
106
107    /// Provide access to a copy of the Secretfile we're using.
108    pub fn secretfile(&self) -> &Secretfile {
109        &self.secretfile
110    }
111
112    /// Fetch the value of an environment-variable-style credential.
113    pub fn var<S: AsRef<str>>(&mut self, name: S) -> Result<String> {
114        let name_ref = name.as_ref();
115        trace!("getting secure credential {}", name_ref);
116        self.backend
117            .var(&self.secretfile, name_ref)
118            .map_err(|err| Error::Credential {
119                name: name_ref.to_owned(),
120                cause: Box::new(err),
121            })
122    }
123
124    /// Fetch the value of a file-style credential.
125    pub fn file<S: AsRef<Path>>(&mut self, path: S) -> Result<String> {
126        let path_ref = path.as_ref();
127        let path_str = path_ref.to_str().ok_or_else(|| Error::Credential {
128            name: format!("{}", path_ref.display()),
129            cause: Box::new(Error::NonUnicodePath {
130                path: path_ref.to_owned(),
131            }),
132        })?;
133        trace!("getting secure credential {}", path_str);
134        self.backend
135            .file(&self.secretfile, path_str)
136            .map_err(|err| Error::Credential {
137                name: path_str.to_owned(),
138                cause: Box::new(err),
139            })
140    }
141}
142
143lazy_static! {
144    // Our shared global client, initialized by `lazy_static!` and
145    // protected by a Mutex.
146    //
147    // Rust deliberately makes it a nuisance to use mutable global
148    // variables.  In this case, the `Mutex` provides thread-safe locking,
149    // the `RefCell` makes this assignable, and the `Option` makes this
150    // optional.  This is a message from the language saying, "Really? A
151    // mutable global that might be null? Have you really thought about
152    // this?"  But the global default client is only for convenience, so
153    // we're OK with it, at least so far.
154    static ref CLIENT: Mutex<RefCell<Option<Client>>> =
155        Mutex::new(RefCell::new(None));
156}
157
158/// Call `body` with the default global client, or return an error if we
159/// can't allocate a default global client.
160#[allow(clippy::let_and_return)] // See `result`.
161fn with_client<F>(body: F) -> Result<String>
162where
163    F: FnOnce(&mut Client) -> Result<String>,
164{
165    let client_cell: MutexGuard<'_, _> = CLIENT.lock().unwrap();
166
167    // Try to set up the client if we haven't already.
168    if client_cell.borrow().is_none() {
169        *client_cell.borrow_mut() = Some(Client::default()?);
170    }
171
172    // Call the provided function.  I have to break out `result` separately
173    // for mysterious reasons related to the borrow checker and global
174    // mutable state.
175    let result = match client_cell.borrow_mut().deref_mut() {
176        Some(client) => body(client),
177        // We theoretically handed this just above, and exited if we
178        // failed.
179        None => panic!("Should have a client, but we don't"),
180    };
181    result
182}
183
184/// Fetch the value of an environment-variable-style credential.
185pub fn var<S: AsRef<str>>(name: S) -> Result<String> {
186    with_client(|client| client.var(name))
187}
188
189/// Fetch the value of a file-style credential.
190pub fn file<S: AsRef<Path>>(path: S) -> Result<String> {
191    with_client(|client| client.file(path))
192}
193
194#[cfg(test)]
195mod test {
196    use super::file;
197    use std::fs;
198    use std::io::Read;
199
200    #[test]
201    fn test_file() {
202        // Some arbitrary file contents.
203        let mut f = fs::File::open("Cargo.toml").unwrap();
204        let mut expected = String::new();
205        f.read_to_string(&mut expected).unwrap();
206
207        assert_eq!(expected, file("Cargo.toml").unwrap());
208        assert!(file("nosuchfile.txt").is_err());
209    }
210}