#![warn(missing_docs)]
#![allow(clippy::redundant_closure)]
use backend::Backend;
use lazy_static::lazy_static;
use std::convert::AsRef;
use std::default::Default;
use std::future::Future;
use std::path::Path;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::trace;
pub use errors::{Error, Result};
pub use secretfile::{Secretfile, SecretfileKeys};
mod backend;
mod chained;
mod envvar;
mod errors;
mod secretfile;
mod vault;
pub struct Options {
secretfile: Option<Secretfile>,
allow_override: bool,
}
impl Default for Options {
fn default() -> Options {
Options {
secretfile: None,
allow_override: true,
}
}
}
impl Options {
pub fn secretfile(mut self, secretfile: Secretfile) -> Options {
self.secretfile = Some(secretfile);
self
}
pub fn allow_override(mut self, allow_override: bool) -> Options {
self.allow_override = allow_override;
self
}
}
pub struct Client {
secretfile: Secretfile,
backend: chained::Client,
}
impl Client {
pub async fn new(options: Options) -> Result<Client> {
let secretfile = match options.secretfile {
Some(sf) => sf,
None => Secretfile::default()?,
};
let over = options.allow_override;
Ok(Client {
secretfile,
backend: chained::Client::with_default_backends(over).await?,
})
}
pub async fn default() -> Result<Client> {
Client::new(Default::default()).await
}
pub async fn with_secretfile(secretfile: Secretfile) -> Result<Client> {
Client::new(Options::default().secretfile(secretfile)).await
}
pub fn secretfile(&self) -> &Secretfile {
&self.secretfile
}
pub async fn var<S: AsRef<str>>(&mut self, name: S) -> Result<String> {
let name_ref = name.as_ref();
trace!("getting secure credential {}", name_ref);
self.backend
.var(&self.secretfile, name_ref)
.await
.map_err(|err| Error::Credential {
name: name_ref.to_owned(),
source: Box::new(err),
})
}
pub async fn file<S: AsRef<Path>>(&mut self, path: S) -> Result<String> {
let path_ref = path.as_ref();
let path_str = path_ref.to_str().ok_or_else(|| Error::Credential {
name: format!("{}", path_ref.display()),
source: Box::new(Error::NonUnicodePath {
path: path_ref.to_owned(),
}),
})?;
trace!("getting secure credential {}", path_str);
self.backend
.file(&self.secretfile, path_str)
.await
.map_err(|err| Error::Credential {
name: path_str.to_owned(),
source: Box::new(err),
})
}
}
lazy_static! {
static ref CLIENT: Arc<Mutex<Option<Client>>> =
Arc::new(Mutex::new(None));
}
async fn with_client<F>(body: F) -> Result<String>
where
F: for<'a> FnOnce(
&'a mut Client,
)
-> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>>,
{
let mut client_cell = CLIENT.clone().lock_owned().await;
if client_cell.is_none() {
*client_cell = Some(Client::default().await?);
}
match client_cell.as_mut() {
Some(client) => body(client).await,
None => panic!("Should have a client, but we don't"),
}
}
pub async fn var<S: AsRef<str>>(name: S) -> Result<String> {
let name = name.as_ref().to_owned();
with_client(|client| Box::pin(client.var(name))).await
}
pub async fn file<S: AsRef<Path>>(path: S) -> Result<String> {
let path = path.as_ref().to_owned();
with_client(|client| Box::pin(client.file(path))).await
}
#[cfg(test)]
mod test {
use super::file;
use std::fs;
use std::io::Read;
use std::path::Path;
#[tokio::test]
async fn test_file() {
let mut f = fs::File::open("Cargo.toml").unwrap();
let mut expected = String::new();
f.read_to_string(&mut expected).unwrap();
assert_eq!(expected, file(&Path::new("Cargo.toml")).await.unwrap());
assert!(file(&Path::new("nosuchfile.txt")).await.is_err());
}
}