hashicorp_vault/
lib.rs

1#![deny(
2    missing_docs,
3    missing_debug_implementations,
4    trivial_casts,
5    trivial_numeric_casts,
6    unsafe_code,
7    unstable_features,
8    unused_import_braces,
9    unused_qualifications,
10    unused_results
11)]
12#![cfg_attr(test, deny(warnings))]
13#![cfg_attr(feature = "clippy", allow(unstable_features))]
14#![cfg_attr(feature = "clippy", feature(plugin))]
15#![cfg_attr(feature = "clippy", plugin(clippy))]
16#![cfg_attr(feature = "clippy", deny(clippy))]
17
18//! Client API for interacting with [Vault](https://www.vaultproject.io/docs/http/index.html)
19
20extern crate base64;
21extern crate reqwest;
22#[macro_use]
23extern crate log;
24#[macro_use]
25extern crate quick_error;
26pub extern crate chrono;
27extern crate serde;
28pub extern crate url;
29
30/// vault client
31pub mod client;
32pub use crate::client::error::{Error, Result};
33pub use crate::client::VaultClient as Client;
34use url::Url;
35
36/// Waiting to stabilize: https://github.com/rust-lang/rust/issues/33417
37///
38/// An attempted conversion that consumes `self`, which may or may not be expensive.
39///
40/// Library authors should not directly implement this trait, but should prefer implementing
41/// the [`TryFrom`] trait, which offers greater flexibility and provides an equivalent `TryInto`
42/// implementation for free, thanks to a blanket implementation in the standard library.
43///
44/// [`TryFrom`]: trait.TryFrom.html
45pub trait TryInto<T>: Sized {
46    /// The type returned in the event of a conversion error.
47    type Err;
48
49    /// Performs the conversion.
50    fn try_into(self) -> ::std::result::Result<T, Self::Err>;
51}
52
53/// Waiting to stabilize: https://github.com/rust-lang/rust/issues/33417
54///
55/// Attempt to construct `Self` via a conversion.
56pub trait TryFrom<T>: Sized {
57    /// The type returned in the event of a conversion error.
58    type Err;
59
60    /// Performs the conversion.
61    fn try_from(_: T) -> ::std::result::Result<Self, Self::Err>;
62}
63
64impl<T, U> TryInto<U> for T
65where
66    U: TryFrom<T>,
67{
68    type Err = U::Err;
69
70    fn try_into(self) -> ::std::result::Result<U, U::Err> {
71        U::try_from(self)
72    }
73}
74
75impl TryFrom<Url> for Url {
76    type Err = Error;
77    fn try_from(u: Url) -> ::std::result::Result<Self, Self::Err> {
78        Ok(u)
79    }
80}
81
82impl<'a> TryFrom<&'a Url> for Url {
83    type Err = Error;
84    fn try_from(u: &Url) -> ::std::result::Result<Self, Self::Err> {
85        Ok(u.clone())
86    }
87}
88
89impl<'a> TryFrom<&'a str> for Url {
90    type Err = Error;
91    fn try_from(s: &str) -> ::std::result::Result<Self, Self::Err> {
92        match Url::parse(s) {
93            Ok(u) => Ok(u),
94            Err(e) => Err(e.into()),
95        }
96    }
97}
98
99impl<'a> TryFrom<&'a String> for Url {
100    type Err = Error;
101    fn try_from(s: &String) -> ::std::result::Result<Self, Self::Err> {
102        match Url::parse(s) {
103            Ok(u) => Ok(u),
104            Err(e) => Err(e.into()),
105        }
106    }
107}
108
109impl TryFrom<String> for Url {
110    type Err = Error;
111    fn try_from(s: String) -> ::std::result::Result<Self, Self::Err> {
112        match Url::parse(&s) {
113            Ok(u) => Ok(u),
114            Err(e) => Err(e.into()),
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use crate::client::HttpVerb::*;
122    use crate::client::VaultClient as Client;
123    use crate::client::{self, EndpointResponse};
124    use crate::Error;
125    use reqwest::StatusCode;
126    use serde::{Deserialize, Serialize};
127    use serde_json::Value;
128
129    /// vault host for testing
130    const HOST: &str = "http://127.0.0.1:8200";
131    /// root token needed for testing
132    const TOKEN: &str = "test12345";
133
134    #[test]
135    fn it_can_create_a_client() {
136        let _ = Client::new(HOST, TOKEN).unwrap();
137    }
138
139    #[test]
140    fn it_can_create_a_client_from_a_string_reference() {
141        let _ = Client::new(&HOST.to_string(), TOKEN).unwrap();
142    }
143
144    #[test]
145    fn it_can_create_a_client_from_a_string() {
146        let _ = Client::new(HOST.to_string(), TOKEN).unwrap();
147    }
148
149    #[test]
150    fn it_can_query_secrets() {
151        let client = Client::new(HOST, TOKEN).unwrap();
152        let res = client.set_secret("hello_query", "world");
153        assert!(res.is_ok());
154        let res = client.get_secret("hello_query").unwrap();
155        assert_eq!(res, "world");
156    }
157
158    #[test]
159    fn it_can_store_json_secrets() {
160        let client = Client::new(HOST, TOKEN).unwrap();
161        let json = "{\"foo\": {\"bar\": [\"baz\"]}}";
162        let res = client.set_secret("json_secret", json);
163        assert!(res.is_ok());
164        let res = client.get_secret("json_secret").unwrap();
165        assert_eq!(res, json)
166    }
167
168    #[test]
169    fn it_can_list_secrets() {
170        let client = Client::new(HOST, TOKEN).unwrap();
171
172        let _res = client.set_secret("hello/fred", "world").unwrap();
173        // assert!(res.is_ok());
174        let res = client.set_secret("hello/bob", "world");
175        assert!(res.is_ok());
176
177        let res = client.list_secrets("hello");
178        assert!(res.is_ok());
179        assert_eq!(res.unwrap(), ["bob", "fred"]);
180
181        let res = client.list_secrets("hello/");
182        assert!(res.is_ok());
183        assert_eq!(res.unwrap(), ["bob", "fred"]);
184    }
185
186    #[test]
187    fn it_can_detect_404_status() {
188        let client = Client::new(HOST, TOKEN).unwrap();
189
190        let res = client.list_secrets("non/existent/key");
191        assert!(res.is_err());
192
193        if let Err(Error::VaultResponse(_, response)) = res {
194            assert_eq!(response.status(), StatusCode::NOT_FOUND);
195        } else {
196            panic!("Error should match on VaultResponse with reqwest response.");
197        }
198    }
199
200    #[test]
201    fn it_can_write_secrets_with_newline() {
202        let client = Client::new(HOST, TOKEN).unwrap();
203
204        let res = client.set_secret("hello_set", "world\n");
205        assert!(res.is_ok());
206        let res = client.get_secret("hello_set").unwrap();
207        assert_eq!(res, "world\n");
208    }
209
210    #[test]
211    fn it_returns_err_on_forbidden() {
212        let client = Client::new(HOST, "test123456");
213        // assert_eq!(Err("Forbidden".to_string()), client);
214        assert!(client.is_err());
215    }
216
217    #[test]
218    fn it_can_delete_a_secret() {
219        let client = Client::new(HOST, TOKEN).unwrap();
220
221        let res = client.set_secret("hello_delete", "world");
222        assert!(res.is_ok());
223        let res = client.get_secret("hello_delete").unwrap();
224        assert_eq!(res, "world");
225        let res = client.delete_secret("hello_delete");
226        assert!(res.is_ok());
227        let res = client.get_secret("hello_delete");
228        assert!(res.is_err());
229    }
230
231    #[test]
232    fn it_can_perform_approle_workflow() {
233        use std::collections::HashMap;
234
235        let c = Client::new(HOST, TOKEN).unwrap();
236        let mut body = "{\"type\":\"approle\"}";
237        // Ensure we do not currently have an approle backend enabled.
238        // Older vault versions (<1.2.0) seem to have an AppRole backend
239        // enabled by default, so calling the POST to create a new one
240        // fails with a 400 status
241        let _: EndpointResponse<()> = c
242            .call_endpoint(DELETE, "sys/auth/approle", None, None)
243            .unwrap();
244        // enable approle auth backend
245        let mut res: EndpointResponse<()> = c
246            .call_endpoint(POST, "sys/auth/approle", None, Some(body))
247            .unwrap();
248        panic_non_empty(&res);
249        // make a new approle
250        body = "{\"secret_id_ttl\":\"10m\", \"token_ttl\":\"20m\", \"token_max_ttl\":\"30m\", \
251                \"secret_id_num_uses\":40}";
252        res = c
253            .call_endpoint(POST, "auth/approle/role/test_role", None, Some(body))
254            .unwrap();
255        panic_non_empty(&res);
256
257        // let's test the properties endpoint while we're here
258        let _ = c.get_app_role_properties("test_role").unwrap();
259
260        // get approle's role-id
261        let res: EndpointResponse<HashMap<String, String>> = c
262            .call_endpoint(GET, "auth/approle/role/test_role/role-id", None, None)
263            .unwrap();
264        let data = match res {
265            EndpointResponse::VaultResponse(res) => res.data.unwrap(),
266            _ => panic!("expected vault response, got: {:?}", res),
267        };
268        let role_id = &data["role_id"];
269        assert!(!role_id.is_empty());
270
271        // now get a secret id for this approle
272        let res: EndpointResponse<HashMap<String, Value>> = c
273            .call_endpoint(POST, "auth/approle/role/test_role/secret-id", None, None)
274            .unwrap();
275        let data = match res {
276            EndpointResponse::VaultResponse(res) => res.data.unwrap(),
277            _ => panic!("expected vault response, got: {:?}", res),
278        };
279        let secret_id = &data["secret_id"].as_str().unwrap();
280
281        // now finally we can try to actually login!
282        let _ = Client::new_app_role(HOST, &role_id[..], Some(&secret_id[..])).unwrap();
283
284        // clean up by disabling approle auth backend
285        let res = c
286            .call_endpoint(DELETE, "sys/auth/approle", None, None)
287            .unwrap();
288        panic_non_empty(&res);
289    }
290
291    #[test]
292    fn it_can_read_a_wrapped_secret() {
293        let client = Client::new(HOST, TOKEN).unwrap();
294        let res = client.set_secret("hello_delete_2", "second world");
295        assert!(res.is_ok());
296        // wrap the secret's value in `sys/wrapping/unwrap` with a TTL of 2 minutes
297        let res = client.get_secret_wrapped("hello_delete_2", "2m").unwrap();
298        let wrapping_token = res.wrap_info.unwrap().token;
299        // make a new client with the wrapping token
300        let c2 = Client::new_no_lookup(HOST, wrapping_token).unwrap();
301        // read the cubbyhole response (can only do this once!)
302        let res = c2.get_unwrapped_response().unwrap();
303        assert_eq!(res.data.unwrap()["value"], "second world");
304    }
305
306    #[test]
307    fn it_can_store_policies() {
308        // use trailing slash for host to ensure Url processing fixes this later
309        let c = Client::new("http://127.0.0.1:8200/", TOKEN).unwrap();
310        let body = "{\"policy\":\"{}\"}";
311        // enable approle auth backend
312        let res: EndpointResponse<()> = c
313            .call_endpoint(PUT, "sys/policy/test_policy_1", None, Some(body))
314            .unwrap();
315        panic_non_empty(&res);
316        let res: EndpointResponse<()> = c
317            .call_endpoint(PUT, "sys/policy/test_policy_2", None, Some(body))
318            .unwrap();
319        panic_non_empty(&res);
320        let client_policies = c.policies().unwrap();
321        let expected_policies = ["default", "test_policy_1", "test_policy_2", "root"];
322        let _ = expected_policies
323            .iter()
324            .map(|p| {
325                assert!(client_policies.contains(&(*p).to_string()));
326            })
327            .last();
328        let token_name = "policy_test_token".to_string();
329        let token_opts = client::TokenOptions::default()
330            .policies(vec!["test_policy_1", "test_policy_2"].into_iter())
331            .default_policy(false)
332            .id(&token_name[..])
333            .ttl(client::VaultDuration::minutes(1));
334        let _ = c.create_token(&token_opts).unwrap();
335        let body = format!("{{\"token\":\"{}\"}}", &token_name);
336        let res: EndpointResponse<client::TokenData> = c
337            .call_endpoint(POST, "auth/token/lookup", None, Some(&body))
338            .unwrap();
339        match res {
340            EndpointResponse::VaultResponse(res) => {
341                let data = res.data.unwrap();
342                let mut policies = data.policies;
343                policies.sort();
344                assert_eq!(policies, ["test_policy_1", "test_policy_2"]);
345            }
346            _ => panic!("expected vault response, got: {:?}", res),
347        }
348        // clean-up
349        let res: EndpointResponse<()> = c
350            .call_endpoint(DELETE, "sys/policy/test_policy_1", None, None)
351            .unwrap();
352        panic_non_empty(&res);
353        let res: EndpointResponse<()> = c
354            .call_endpoint(DELETE, "sys/policy/test_policy_2", None, None)
355            .unwrap();
356        panic_non_empty(&res);
357    }
358
359    #[test]
360    fn it_can_list_things() {
361        let c = Client::new(HOST, TOKEN).unwrap();
362        let _ = c
363            .create_token(&client::TokenOptions::default().ttl(client::VaultDuration::minutes(1)))
364            .unwrap();
365        let res: EndpointResponse<client::ListResponse> = c
366            .call_endpoint(LIST, "auth/token/accessors", None, None)
367            .unwrap();
368        match res {
369            EndpointResponse::VaultResponse(res) => {
370                let data = res.data.unwrap();
371                assert!(data.keys.len() > 2);
372            }
373            _ => panic!("expected vault response, got: {:?}", res),
374        }
375    }
376
377    #[test]
378    fn it_can_encrypt_decrypt_transit() {
379        let key_id = "test-vault-rs";
380        let plaintext = b"data\0to\0encrypt";
381
382        let client = Client::new(HOST, TOKEN).unwrap();
383        let enc_resp = client.transit_encrypt(None, key_id, plaintext);
384        let encrypted = enc_resp.unwrap();
385        let dec_resp = client.transit_decrypt(None, key_id, encrypted);
386        let payload = dec_resp.unwrap();
387        assert_eq!(plaintext, payload.as_slice());
388    }
389
390    // helper fn to panic on empty responses
391    fn panic_non_empty(res: &EndpointResponse<()>) {
392        match *res {
393            EndpointResponse::Empty => {}
394            _ => panic!("expected empty response, received: {:?}", res),
395        }
396    }
397
398    #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
399    struct CustomSecretType {
400        name: String,
401    }
402
403    #[test]
404    fn it_can_set_and_get_a_custom_secret_type() {
405        let input = CustomSecretType {
406            name: "test".into(),
407        };
408
409        let client = Client::new(HOST, TOKEN).unwrap();
410
411        let res = client.set_custom_secret("custom_type", &input);
412        assert!(res.is_ok());
413        let res: CustomSecretType = client.get_custom_secret("custom_type").unwrap();
414        assert_eq!(res, input);
415    }
416}