config_vault/
lib.rs

1//! # config-vault
2//!
3//! `config-vault` is an extension for the `config` crate that allows loading configurations
4//! directly from HashiCorp Vault.
5//!
6//! This library implements a custom `Source` for the `config` crate that can
7//! connect to a HashiCorp Vault server and load secrets from the KV2 engine as
8//! configuration values.
9//!
10//! ## Example
11//!
12//! ```
13//! use config::{Config, ConfigError};
14//! use config_vault::VaultSource;
15//!
16//! fn load_config() -> Result<Config, ConfigError> {
17//!     let vault_source = VaultSource::new(
18//!         "http://127.0.0.1:8200".to_string(),  // Vault address
19//!         "hvs.EXAMPLE_TOKEN".to_string(),      // Vault token
20//!         "secret".to_string(),                 // KV mount name
21//!         "dev".to_string(),        // Secret path
22//!     );
23//!
24//!     Config::builder()
25//!         .add_source(vault_source)
26//!         // You can add other sources
27//!         .build()
28//! }
29//! ```
30//!
31//! If you want to use the KV1 engine, you can use the `new_v1` method instead of `new`:
32//!
33//! ```
34//! use config_vault::VaultSource;
35//!
36//! let vault_source = VaultSource::new_v1(
37//!         "http://127.0.0.1:8200".to_string(),  // Vault address
38//!         "hvs.EXAMPLE_TOKEN".to_string(),      // Vault token
39//!         "secret".to_string(),                 // KV mount name
40//!         "dev".to_string(),        // Secret path
41//! );
42//! ```
43
44use std::collections::HashMap;
45
46use config::{ConfigError, Map, Source, Value};
47use reqwest::blocking::Client;
48use serde_json::Value as JsonValue;
49use url::Url;
50
51/// A `Source` for the `config` library that loads configurations from HashiCorp Vault.
52///
53/// This source connects to a HashiCorp Vault server and loads a secret from
54/// the version 2 of the KV (Key-Value) engine. The values from the secret are included
55/// in the configuration as flat key-value pairs.
56///
57/// # Example
58///
59/// ```
60/// use config_vault::VaultSource;
61///
62/// let vault = VaultSource::new(
63///     "http://vault.example.com:8200".to_string(),
64///     "my-token".to_string(),
65///     "secret".to_string(),
66///     "dev".to_string(),
67/// );
68/// ```
69#[derive(Debug, Clone)]
70pub struct VaultSource {
71    vault_addr: String,
72    vault_token: String,
73    vault_mount: String,
74    vault_path: String,
75    kv_version: KvVersion,
76}
77
78#[derive(Debug, Clone, PartialEq)]
79pub enum KvVersion {
80    V1 = 1,
81    V2,
82}
83
84impl KvVersion {
85    fn get_api_path(&self, mount: &str, path: &str) -> String {
86        match self {
87            KvVersion::V1 => format!("v1/{}/{}", mount, path),
88            _ => format!("v1/{}/data/{}", mount, path),
89        }
90    }
91}
92
93impl VaultSource {
94    /// Creates a new instance of `VaultSource`.
95    ///
96    /// # Parameters
97    ///
98    /// * `vault_addr` - Complete URL of the Vault server (e.g. "http://127.0.0.1:8200")
99    /// * `vault_token` - Authentication token for Vault
100    /// * `vault_mount` - Name of the KV engine mount (e.g. "secret")
101    /// * `vault_path` - Path to the secret within the mount (e.g. "dev")
102    ///
103    /// # Example
104    ///
105    /// ```
106    /// use config_vault::VaultSource;
107    ///
108    /// let source = VaultSource::new(
109    ///     "http://127.0.0.1:8200".to_string(),
110    ///     "hvs.EXAMPLE_TOKEN".to_string(),
111    ///     "secret".to_string(),
112    ///     "dev".to_string(),
113    /// );
114    /// ```
115    pub fn new(
116        vault_addr: String,
117        vault_token: String,
118        vault_mount: String,
119        vault_path: String,
120    ) -> Self {
121        Self {
122            vault_addr,
123            vault_token,
124            vault_mount,
125            vault_path,
126            kv_version: KvVersion::V2,
127        }
128    }
129
130    /// Creates a new instance of `VaultSource` with kv_version V1
131    ///
132    /// # Parameters
133    ///
134    /// * `vault_addr` - Complete URL of the Vault server (e.g. "http://127.0.0.1:8200")
135    /// * `vault_token` - Authentication token for Vault
136    /// * `vault_mount` - Name of the KV engine mount (e.g. "secret")
137    /// * `vault_path` - Path to the secret within the mount (e.g. "dev")
138    ///
139    /// # Example
140    ///
141    /// ```
142    /// use config_vault::VaultSource;
143    ///
144    /// let source = VaultSource::new_v1(
145    ///     "http://127.0.0.1:8200".to_string(),
146    ///     "hvs.EXAMPLE_TOKEN".to_string(),
147    ///     "secret".to_string(),
148    ///     "dev".to_string(),
149    /// );
150    /// ```
151    pub fn new_v1(
152        vault_addr: String,
153        vault_token: String,
154        vault_mount: String,
155        vault_path: String,
156    ) -> Self {
157        Self {
158            vault_addr,
159            vault_token,
160            vault_mount,
161            vault_path,
162            kv_version: KvVersion::V1,
163        }
164    }
165
166    /// Changes the KvVersion
167    ///
168    /// This function takes the target KvVersion and replaces the existing one.
169    ///
170    pub fn set_kv_version(&mut self, kv_version: KvVersion) {
171        self.kv_version = kv_version;
172    }
173
174    /// Builds the URL for Vault's KV1/KV2 engine read API.
175    ///
176    /// This function takes the base address of Vault and builds the complete URL
177    /// to access the read API of the KV1 engine with the specified path.
178    ///
179    /// # Returns
180    ///
181    /// * `Result<Url, ConfigError>` - The constructed URL or an error if the address is invalid
182    fn build_kv_read_url(&self) -> Result<Url, ConfigError> {
183        let api_path = self
184            .kv_version
185            .get_api_path(&self.vault_mount, &self.vault_path);
186
187        let mut url = Url::parse(&self.vault_addr)
188            .map_err(|e| ConfigError::Message(format!("Invalid Vault address URL: {}", e)))?;
189
190        url.path_segments_mut()
191            .map_err(|_| ConfigError::Message("Vault address URL cannot be a base".into()))?
192            .pop_if_empty() // Remove trailing slash if any
193            .extend(api_path.split('/')); // Add the API path segments
194
195        Ok(url)
196    }
197}
198
199impl Source for VaultSource {
200    fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
201        Box::new(self.clone())
202    }
203
204    /// Implementation of the `collect` method from `Source`.
205    ///
206    /// This method makes an HTTP request to the Vault API to obtain
207    /// configuration values stored in the specified secret.
208    ///
209    /// # Returns
210    ///
211    /// * `Result<Map<String, Value>, ConfigError>` - A map with configuration values
212    ///   or an error if the request fails or the response format is not as expected.
213    fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
214        let url = self.build_kv_read_url()?;
215
216        let client = Client::new();
217        let response = client
218            .get(url)
219            .header("X-Vault-Token", &self.vault_token)
220            .send()
221            .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
222
223        if response.status().is_success() {
224            let raw = response
225                .json::<JsonValue>()
226                .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
227
228            let json_obj = raw
229                .get("data")
230                .and_then(|x| {
231                    if self.kv_version == KvVersion::V2 {
232                        x.get("data")
233                    } else {
234                        Some(x)
235                    }
236                })
237                .and_then(|x| x.as_object())
238                .unwrap();
239
240            let mut secret = HashMap::new();
241            for (k, v) in json_obj {
242                secret.insert(k.clone(), Value::from(v.as_str().unwrap()));
243            }
244
245            Ok(secret)
246        } else {
247            Err(ConfigError::Message(format!(
248                "Failed to fetch secret from Vault (wrong kv version?): {}",
249                response.status()
250            )))
251        }
252    }
253}