use std::collections::HashMap;
use std::io::Read;
use hyper::{self, header, Client};
use hyper::client::response::Response;
use rustc_serialize::{json, Decodable, Decoder};
use client::error::{Error, Result};
use std::time::Duration;
use chrono::{DateTime, FixedOffset, NaiveDateTime};
pub mod error;
#[derive(Debug)]
pub struct VaultDuration(pub Duration);
impl Decodable for VaultDuration {
fn decode<D: Decoder>(d: &mut D) -> ::std::result::Result<VaultDuration, D::Error> {
let num = try!(d.read_u64());
Ok(VaultDuration(Duration::from_secs(num)))
}
}
#[derive(Debug)]
pub struct VaultNaiveDateTime(pub NaiveDateTime);
impl Decodable for VaultNaiveDateTime {
fn decode<D: Decoder>(d: &mut D) -> ::std::result::Result<VaultNaiveDateTime, D::Error> {
let seconds_since_epoch = try!(d.read_i64());
let date_time = NaiveDateTime::from_timestamp_opt(seconds_since_epoch, 0);
match date_time {
Some(dt) => Ok(VaultNaiveDateTime(dt)),
None => {
Err(d.error(&format!("Could not parse: `{}` as a unix timestamp",
seconds_since_epoch,
)))
}
}
}
}
#[derive(Debug)]
pub struct VaultDateTime(pub DateTime<FixedOffset>);
impl Decodable for VaultDateTime {
fn decode<D: Decoder>(d: &mut D) -> ::std::result::Result<VaultDateTime, D::Error> {
let ts = try!(d.read_str());
let date_time = DateTime::parse_from_rfc3339(&ts);
match date_time {
Ok(dt) => Ok(VaultDateTime(dt)),
Err(e) => {
Err(d.error(&format!("Could not parse: `{}` as an RFC 3339 timestamp. Error: \
`{:?}`",
ts,
e)))
}
}
}
}
#[derive(Debug)]
pub struct VaultClient<'a, T>
where T: Decodable
{
pub host: &'a str,
pub token: String,
client: Client,
pub data: VaultResponse<T>,
}
#[derive(RustcDecodable, Debug)]
pub struct TokenData {
pub accessor: Option<String>,
pub creation_time: VaultNaiveDateTime,
pub creation_ttl: Option<VaultDuration>,
pub display_name: String,
pub explicit_max_ttl: Option<VaultDuration>,
pub id: String,
pub last_renewal_time: Option<VaultDuration>,
pub meta: Option<HashMap<String, String>>,
pub num_uses: u64,
pub orphan: bool,
pub path: String,
pub policies: Vec<String>,
pub renewable: Option<bool>,
pub role: Option<String>,
pub ttl: VaultDuration,
}
#[derive(RustcDecodable, RustcEncodable, Debug)]
struct SecretData {
value: String,
}
#[derive(RustcDecodable, Debug)]
pub struct Auth {
pub client_token: String,
pub accessor: String,
pub policies: Vec<String>,
pub metadata: HashMap<String, String>,
pub lease_duration: Option<VaultDuration>,
pub renewable: bool,
}
#[derive(RustcDecodable, Debug)]
pub struct VaultResponse<D>
where D: Decodable
{
pub lease_id: Option<String>,
pub renewable: Option<bool>,
pub lease_duration: Option<VaultDuration>,
pub data: Option<D>,
pub warnings: Option<Vec<String>>,
pub auth: Option<Auth>,
pub wrap_info: Option<WrapInfo>,
}
#[derive(RustcDecodable, Debug)]
pub struct WrapInfo {
pub ttl: VaultDuration,
pub token: String,
pub creation_time: VaultDateTime,
pub wrapped_accessor: String,
}
#[derive(RustcDecodable, RustcEncodable, Debug)]
pub struct WrapData {
response: String,
}
#[derive(RustcDecodable, RustcEncodable, Debug)]
struct AppIdPayload {
app_id: String,
user_id: String,
}
#[derive(RustcDecodable, RustcEncodable, Debug)]
pub struct PostgresqlLogin {
pub password: String,
pub username: String,
}
header! {
(XVaultToken, "X-Vault-Token") => [String]
}
header! {
(XVaultWrapTTL, "X-Vault-Wrap-TTL") => [String]
}
impl<'a> VaultClient<'a, TokenData> {
pub fn new(host: &'a str, token: &'a str) -> Result<VaultClient<'a, TokenData>> {
let client = Client::new();
let mut res = try!(
handle_hyper_response(client.get(&format!("{}/v1/auth/token/lookup-self", host)[..])
.header(XVaultToken(token.to_string()))
.send()));
let decoded: VaultResponse<TokenData> = try!(parse_vault_response(&mut res));
Ok(VaultClient {
host: host,
token: token.to_string(),
client: client,
data: decoded,
})
}
}
impl<'a> VaultClient<'a, ()> {
pub fn new_app_id(host: &'a str,
app_id: &'a str,
user_id: &'a str)
-> Result<VaultClient<'a, ()>> {
let client = Client::new();
let payload = try!(json::encode(&AppIdPayload {
app_id: app_id.to_string(),
user_id: user_id.to_string(),
}));
let mut res =
try!(handle_hyper_response(client.post(&format!("{}/v1/auth/app-id/login", host)[..])
.body(&payload)
.send()));
let decoded: VaultResponse<()> = try!(parse_vault_response(&mut res));
let token = match decoded.auth {
Some(ref auth) => auth.client_token.clone(),
None => {
return Err(Error::Vault(format!("No client token found in response: `{:?}`",
&decoded.auth)))
}
};
Ok(VaultClient {
host: host,
token: token,
client: client,
data: decoded,
})
}
}
impl<'a, T> VaultClient<'a, T>
where T: Decodable
{
pub fn renew(&mut self) -> Result<()> {
let mut res = try!(self.post(&format!("{}/v1/auth/token/renew-self", self.host), None));
let vault_res: VaultResponse<T> = try!(parse_vault_response(&mut res));
self.data.auth = vault_res.auth;
Ok(())
}
pub fn revoke(&mut self) -> Result<()> {
let _ = try!(self.post(&format!("{}/v1/auth/token/revoke-self", self.host), None));
Ok(())
}
pub fn renew_lease(&self, lease_id: &str, increment: Option<u64>) -> Result<VaultResponse<()>> {
let body = match increment {
Some(_) => Some(format!("{{\"increment\": {:?}}}", increment)),
None => None,
};
let mut res = try!(self.put(&format!("{}/v1/sys/renew/{}", self.host, lease_id)[..],
body.as_ref().map(String::as_ref)));
let vault_res: VaultResponse<()> = try!(parse_vault_response(&mut res));
Ok(vault_res)
}
pub fn lookup(&mut self) -> Result<VaultResponse<TokenData>> {
let mut res = try!(self.get(&format!("{}/v1/auth/token/lookup-self", self.host), None));
let vault_res: VaultResponse<TokenData> = try!(parse_vault_response(&mut res));
Ok(vault_res)
}
pub fn set_secret(&self, key: &str, value: &str) -> Result<()> {
let _ = try!(self.post(&format!("/v1/secret/{}", key)[..],
Some(&format!("{{\"value\": \"{}\"}}", self.escape(value))[..])));
Ok(())
}
fn escape(&self, input: &str) -> String {
input.replace("\n", "\\n")
}
pub fn get_secret(&self, key: &str) -> Result<String> {
let mut res = try!(self.get(&format!("/v1/secret/{}", key)[..], None));
let decoded: VaultResponse<SecretData> = try!(parse_vault_response(&mut res));
match decoded.data {
Some(data) => Ok(data.value),
_ => Err(Error::Vault(format!("No secret found in response: `{:#?}`", decoded))),
}
}
pub fn get_secret_wrapped(&self, key: &str, wrap_ttl: &str) -> Result<VaultResponse<()>> {
let mut res = try!(self.get(&format!("/v1/secret/{}", key)[..], Some(wrap_ttl)));
Ok(try!(parse_vault_response(&mut res)))
}
pub fn get_cubbyhole_response(&self) -> Result<VaultResponse<HashMap<String, String>>> {
let mut res = try!(self.get("/v1/cubbyhole/response", None));
let decoded: VaultResponse<WrapData> = try!(parse_vault_response(&mut res));
Ok(try!(json::decode(&decoded.data.unwrap().response[..])))
}
pub fn delete_secret(&self, key: &str) -> Result<()> {
let _ = try!(self.delete(&format!("/v1/secret/{}", key)[..]));
Ok(())
}
pub fn get_postgresql_backend(&self, name: &str) -> Result<VaultResponse<PostgresqlLogin>> {
let mut res = try!(self.get(&format!("/v1/postgresql/creds/{}", name)[..], None));
let decoded: VaultResponse<PostgresqlLogin> = try!(parse_vault_response(&mut res));
Ok(decoded)
}
fn get(&self, endpoint: &str, wrap_ttl: Option<&str>) -> Result<Response> {
let mut req = self.client
.get(&format!("{}{}", self.host, endpoint)[..])
.header(XVaultToken(self.token.to_string()))
.header(header::ContentType::json());
if wrap_ttl.is_some() {
req = req.header(XVaultWrapTTL(wrap_ttl.unwrap().to_string()));
}
Ok(try!(handle_hyper_response(req.send())))
}
fn delete(&self, endpoint: &str) -> Result<Response> {
Ok(try!(handle_hyper_response(self.client
.delete(&format!("{}{}", self.host, endpoint)[..])
.header(XVaultToken(self.token.to_string()))
.header(header::ContentType::json())
.send())))
}
fn post(&self, endpoint: &str, body: Option<&str>) -> Result<Response> {
let mut req = self.client
.post(&format!("{}{}", self.host, endpoint)[..])
.header(XVaultToken(self.token.to_string()))
.header(header::ContentType::json());
if let Some(body) = body {
req = req.body(body);
}
Ok(try!(handle_hyper_response(req.send())))
}
fn put(&self, endpoint: &str, body: Option<&str>) -> Result<Response> {
let mut req = self.client
.put(&format!("{}{}", self.host, endpoint)[..])
.header(XVaultToken(self.token.to_string()))
.header(header::ContentType::json());
if body.is_some() {
req = req.body(body.unwrap());
}
Ok(try!(handle_hyper_response(req.send())))
}
}
fn handle_hyper_response(res: ::std::result::Result<Response, hyper::Error>) -> Result<Response> {
let mut res = try!(res);
if res.status.is_success() {
Ok(res)
} else {
let mut error_msg = String::new();
let _ = res.read_to_string(&mut error_msg).unwrap_or({
error_msg.push_str("Could not read vault response.");
0
});
println!("Vault request failed: {:?}, error message: `{}`",
res,
error_msg);
Err(Error::Vault(format!("Vault request failed: {:?}, error message: `{}`",
res,
error_msg)))
}
}
fn parse_vault_response<T>(res: &mut Response) -> Result<VaultResponse<T>>
where T: Decodable
{
let mut body = String::new();
let _ = try!(res.read_to_string(&mut body));
println!("Response: {:?}", &body);
let vault_res: VaultResponse<T> = try!(json::decode(&body));
Ok(vault_res)
}