use backend::Backend;
use errors::*;
use reqwest;
use reqwest::header::Connection;
use rustc_serialize::json;
use secretfile::{Location, Secretfile, SecretfileLookup};
use std::collections::BTreeMap;
use std::env;
use std::fs::File;
use std::io::Read;
header! { (XVaultToken, "X-Vault-Token") => [String] }
fn default_addr() -> Result<String> {
env::var("VAULT_ADDR").map_err(|_| ErrorKind::MissingVaultAddr.into())
}
fn default_token() -> Result<String> {
env::var("VAULT_TOKEN")
.or_else(|_: env::VarError| -> Result<String> {
let mut path = env::home_dir().ok_or_else(|| {
let err: Error = ErrorKind::NoHomeDirectory.into();
err
})?;
path.push(".vault-token");
let mut f = File::open(path)?;
let mut token = String::new();
f.read_to_string(&mut token)?;
Ok(token)
})
.chain_err(|| ErrorKind::MissingVaultToken)
}
#[derive(Debug, RustcDecodable)]
struct Secret {
data: BTreeMap<String, String>,
lease_duration: u64,
}
pub struct Client {
client: reqwest::Client,
addr: reqwest::Url,
token: String,
secrets: BTreeMap<String, Secret>,
}
impl Client {
pub fn is_enabled() -> bool {
default_addr().is_ok()
}
pub fn default() -> Result<Client> {
let client = reqwest::Client::new().map_err(|e| {
let err: Error = format!("{}", e).into();
err
})?;
Client::new(client, &default_addr()?, default_token()?)
}
fn new<U, S>(client: reqwest::Client, addr: U, token: S) -> Result<Client>
where U: reqwest::IntoUrl,
S: Into<String>
{
let addr = addr.into_url()?;
Ok(Client {
client: client,
addr: addr,
token: token.into(),
secrets: BTreeMap::new(),
})
}
fn get_secret(&self, path: &str) -> Result<Secret> {
let url = self.addr.join(&format!("v1/{}", path))?;
debug!("Getting secret {}", url);
let req = self.client.get(url.clone())
.header(Connection::close())
.header(XVaultToken(self.token.clone()));
let mut res = req.send().map_err(|e| format!("{}", e))?;
if !res.status().is_success() {
let status = res.status().to_owned();
let err: Error = ErrorKind::UnexpectedHttpStatus(status).into();
return Err(err).chain_err(|| ErrorKind::Url(url));
}
let mut body = String::new();
res.read_to_string(&mut body)?;
Ok(json::decode(&body)?)
}
fn get_loc(&mut self,
searched_for: &str,
loc: Option<Location>)
-> Result<String> {
match loc {
None => Err(ErrorKind::MissingEntry(searched_for.to_owned()).into()),
Some(Location::PathWithKey(ref path, ref key)) => {
if !self.secrets.contains_key(path) {
let secret = self.get_secret(path)?;
self.secrets.insert(path.to_owned(), secret);
}
let secret = self.secrets.get(path).unwrap();
secret.data
.get(key)
.ok_or_else(|| {
let err: Error =
ErrorKind::MissingKeyInSecret(path.to_owned(),
key.to_owned())
.into();
err
})
.map(|v| v.clone())
}
Some(Location::Path(ref path)) => {
Err(ErrorKind::MissingKeyInPath(path.to_owned()).into())
}
}
}
}
impl Backend for Client {
fn name(&self) -> &'static str {
"vault"
}
fn var(&mut self, secretfile: &Secretfile, credential: &str) -> Result<String> {
let loc = secretfile.var(credential).cloned();
self.get_loc(credential, loc)
}
fn file(&mut self, secretfile: &Secretfile, path: &str) -> Result<String> {
let loc = secretfile.file(path).cloned();
self.get_loc(path, loc)
}
}