From ec420c446706754829250440b625dac5bf9a5738 Mon Sep 17 00:00:00 2001 From: Chris MacNaughton Date: Mon, 8 Feb 2021 09:00:32 +0100 Subject: [PATCH] Add support for custom secret types. This change additionally refactors the default secret methods, `get_secret` and `set_secret` to use the new custom get and set methods with a new internal type. Closes #42 --- CHANGELOG.md | 4 ++ src/client/mod.rs | 105 +++++++++++++++++++++++++++++++++++++++------- src/lib.rs | 20 +++++++++ 3 files changed, 114 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f1049c..ffc195a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ FEATURES - Update tested Vault releases to include latest versions. - Migrate CI testing to Github actions. +- Add support for custoim secret types (https://github.com/ChrisMacNaughton/vault-rs/pull/44). + + The new secret types allow for managing arbitrary data in Vault + as long as the data is serializable! BREAKING CHANGES diff --git a/src/client/mod.rs b/src/client/mod.rs index 36076b4..61f8b03 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -625,6 +625,16 @@ pub enum HttpVerb { LIST, } +#[derive(Debug, Serialize)] +struct SecretContainer { + data: T, +} + +#[derive(Debug, Deserialize, Serialize)] +struct DefaultSecretType> { + value: T, +} + /// endpoint response variants #[derive(Debug)] pub enum EndpointResponse { @@ -968,23 +978,49 @@ where /// assert!(res.is_ok()); /// ``` pub fn set_secret, S2: AsRef>(&self, key: S1, value: S2) -> Result<()> { - let _ = self.post::<_, String>( - &format!("/v1/secret/data/{}", key.into())[..], - Some( - &format!( - "{{\"data\": {{\"value\": \"{}\"}}}}", - self.escape(value.as_ref()) - )[..], - ), + let secret = DefaultSecretType { + value: value.as_ref(), + }; + self.set_custom_secret(key, &secret) + } + + /// Saves a secret + /// + /// ``` + /// # extern crate hashicorp_vault as vault; + /// # use vault::Client; + /// use serde::{Deserialize, Serialize}; + /// + /// #[derive(Deserialize, Serialize)] + /// struct MyThing { + /// awesome: String, + /// thing: String, + /// } + /// let host = "http://127.0.0.1:8200"; + /// let token = "test12345"; + /// let client = Client::new(host, token).unwrap(); + /// let secret = MyThing { + /// awesome: "I really am cool".into(), + /// thing: "this is also in the secret".into(), + /// }; + /// let res = client.set_custom_secret("hello_set", &secret); + /// assert!(res.is_ok()); + /// ``` + pub fn set_custom_secret(&self, secret_name: S1, secret: &S2) -> Result<()> + where + S1: Into, + S2: Serialize, + { + let secret = SecretContainer { data: secret }; + let json = serde_json::to_string(&secret)?; + let _ = self.put::<_, String>( + &format!("/v1/secret/data/{}", secret_name.into())[..], + Some(&json), None, )?; Ok(()) } - fn escape>(&self, input: S) -> String { - input.as_ref().replace("\n", "\\n").replace("\"", "\\\"") - } - /// /// List secrets at specified path /// @@ -1036,10 +1072,49 @@ where /// assert_eq!(res.unwrap(), "world"); /// ``` pub fn get_secret>(&self, key: S) -> Result { - let res = self.get::<_, String>(&format!("/v1/secret/data/{}", key.as_ref())[..], None)?; - let decoded: VaultResponse> = parse_vault_response(res)?; + let secret: DefaultSecretType = self.get_custom_secret(key)?; + Ok(secret.value) + } + + /// + /// Fetches a saved secret + /// + /// ``` + /// # extern crate hashicorp_vault as vault; + /// # use vault::Client; + /// use serde::{Deserialize, Serialize}; + /// + /// #[derive(Debug, Deserialize, Serialize)] + /// struct MyThing { + /// awesome: String, + /// thing: String, + /// } + /// let host = "http://127.0.0.1:8200"; + /// let token = "test12345"; + /// let client = Client::new(host, token).unwrap(); + /// let secret = MyThing { + /// awesome: "I really am cool".into(), + /// thing: "this is also in the secret".into(), + /// }; + /// let res1 = client.set_custom_secret("custom_secret", &secret); + /// assert!(res1.is_ok()); + /// let res2: Result = client.get_custom_secret("custom_secret"); + /// assert!(res2.is_ok()); + /// let thing = res2.unwrap(); + /// assert_eq!(thing.awesome, "I really am cool"); + /// assert_eq!(thing.thing, "this is also in the secret"); + /// ``` + pub fn get_custom_secret, S2: DeserializeOwned + std::fmt::Debug>( + &self, + secret_name: S, + ) -> Result { + let res = self.get::<_, String>( + &format!("/v1/secret/data/{}", secret_name.as_ref())[..], + None, + )?; + let decoded: VaultResponse> = parse_vault_response(res)?; match decoded.data { - Some(data) => Ok(data.data.value), + Some(data) => Ok(data.data), _ => Err(Error::Vault(format!( "No secret found in response: `{:#?}`", decoded diff --git a/src/lib.rs b/src/lib.rs index bfddae3..a368946 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,6 +123,7 @@ mod tests { use crate::client::{self, EndpointResponse}; use crate::Error; use reqwest::StatusCode; + use serde::{Deserialize, Serialize}; /// vault host for testing const HOST: &str = "http://127.0.0.1:8200"; @@ -392,4 +393,23 @@ mod tests { _ => panic!("expected empty response, received: {:?}", res), } } + + #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] + struct CustomSecretType { + name: String, + } + + #[test] + fn it_can_set_and_get_a_custom_secret_type() { + let input = CustomSecretType { + name: "test".into(), + }; + + let client = Client::new(HOST, TOKEN).unwrap(); + + let res = client.set_custom_secret("custom_type", &input); + assert!(res.is_ok()); + let res: CustomSecretType = client.get_custom_secret("custom_type").unwrap(); + assert_eq!(res, input); + } }