config_vault_source/
lib.rs1pub mod builder;
50mod utils;
51
52pub use builder::VaultSourceBuilder;
53
54use std::ops::Deref;
55use std::str::FromStr;
56
57use config::{Config, Source};
58use config::{ConfigError, Map, Value};
59
60#[cfg(feature = "async")]
61use async_trait::async_trait;
62#[cfg(feature = "async")]
63use config::AsyncSource;
64use url::Url;
65
66use crate::utils::flatten_json;
67
68#[derive(Debug, Clone, PartialEq)]
87pub struct VaultSource {
88 config: VaultConfig,
89 kv_version: KvVersion,
90}
91
92#[derive(Debug, Clone, PartialEq)]
93pub struct VaultAddr(Url);
94
95impl Deref for VaultAddr {
96 type Target = Url;
97
98 fn deref(&self) -> &Self::Target {
99 &self.0
100 }
101}
102
103impl FromStr for VaultAddr {
104 type Err = ConfigError;
105
106 fn from_str(s: &str) -> Result<Self, Self::Err> {
107 let url = url::Url::parse(s)
108 .map_err(|e| ConfigError::Message(format!("Invalid Vault address: {e}")))?;
109
110 if url.cannot_be_a_base() {
111 return Err(ConfigError::Message(
112 "Vault address cannot be a base URL".into(),
113 ));
114 }
115
116 if !url.path().trim_matches('/').is_empty() {
117 return Err(ConfigError::Message(
118 "Vault address must not contain a path (e.g. use https://host:8200, not https://host:8200/v1)"
119 .into(),
120 ));
121 }
122
123 Ok(VaultAddr(url))
124 }
125}
126
127impl TryFrom<&str> for VaultAddr {
128 type Error = ConfigError;
129
130 fn try_from(value: &str) -> Result<Self, Self::Error> {
131 value.parse()
132 }
133}
134
135impl TryFrom<String> for VaultAddr {
136 type Error = ConfigError;
137
138 fn try_from(value: String) -> Result<Self, Self::Error> {
139 value.parse()
140 }
141}
142
143#[derive(Debug, Clone, PartialEq)]
144pub struct VaultConfig {
145 pub address: VaultAddr,
146 pub token: String,
147 pub mount: String,
148 pub path: String,
149
150 #[cfg(feature = "tls")]
151 pub tls: Option<TlsConfig>,
152}
153
154#[cfg(feature = "tls")]
155#[derive(Debug, Clone, PartialEq)]
156pub struct TlsConfig {
157 pub ca_cert_bytes: Option<Vec<u8>>,
158 pub client_cert: Option<Vec<u8>>,
159 pub client_key: Option<Vec<u8>>,
160 pub danger_accept_invalid_certs: bool,
161}
162
163#[derive(Debug, Clone, PartialEq, Default)]
164pub enum KvVersion {
165 V1,
166 #[default]
167 V2,
168}
169
170impl VaultSource {
171 pub fn builder() -> VaultSourceBuilder {
172 VaultSourceBuilder::new()
173 }
174
175 fn build_read_url(&self) -> Result<url::Url, ConfigError> {
176 let api_path = match self.kv_version {
177 KvVersion::V1 => format!("v1/{}/{}", self.config.mount, self.config.path),
178 KvVersion::V2 => format!("v1/{}/data/{}", self.config.mount, self.config.path),
179 };
180
181 let mut url = self.config.address.0.clone();
182
183 url.path_segments_mut()
184 .map_err(|_| ConfigError::Message("Vault base URL cannot be a base".into()))?
185 .pop_if_empty()
186 .extend(api_path.split("/"));
187
188 Ok(url)
189 }
190
191 fn build_blocking_client(&self) -> Result<reqwest::blocking::Client, ConfigError> {
192 let mut builder = reqwest::blocking::Client::builder();
193
194 #[cfg(feature = "tls")]
195 if let Some(tls) = &self.config.tls {
196 if let Some(ca_bytes) = &tls.ca_cert_bytes {
197 let cert = reqwest::Certificate::from_pem(&ca_bytes)
198 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
199
200 builder = builder.add_root_certificate(cert);
201 }
202
203 if let (Some(cert), Some(key)) = (&tls.client_cert, &tls.client_key) {
204 let mut identity_bytes = cert.clone();
205 identity_bytes.extend_from_slice(key);
206 let identity = reqwest::Identity::from_pem(&identity_bytes)
207 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
208 builder = builder.identity(identity);
209 }
210
211 if tls.danger_accept_invalid_certs {
212 builder = builder.danger_accept_invalid_certs(true);
213 }
214 }
215
216 builder
217 .build()
218 .map_err(|e| ConfigError::Foreign(Box::new(e)))
219 }
220
221 #[cfg(feature = "async")]
222 fn build_async_client(&self) -> Result<reqwest::Client, ConfigError> {
223 let mut builder = reqwest::Client::builder();
224
225 #[cfg(feature = "tls")]
226 if let Some(tls) = &self.config.tls {
227 if let Some(ca_bytes) = &tls.ca_cert_bytes {
228 let cert = reqwest::Certificate::from_pem(&ca_bytes)
229 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
230
231 builder = builder.add_root_certificate(cert);
232 }
233
234 if let (Some(cert), Some(key)) = (&tls.client_cert, &tls.client_key) {
235 let mut identity_bytes = cert.clone();
236 identity_bytes.extend_from_slice(key);
237 let identity = reqwest::Identity::from_pem(&identity_bytes)
238 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
239 builder = builder.identity(identity);
240 }
241
242 if tls.danger_accept_invalid_certs {
243 builder = builder.danger_accept_invalid_certs(true);
244 }
245 }
246
247 builder
248 .build()
249 .map_err(|e| ConfigError::Foreign(Box::new(e)))
250 }
251}
252
253impl Source for VaultSource {
254 fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
255 Box::new(self.clone())
256 }
257
258 fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
259 let client = self.build_blocking_client()?;
260 let url = self.build_read_url()?;
261 let resp = client
262 .get(url)
263 .header("X-Vault-Token", &self.config.token)
264 .send()
265 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
266
267 if !resp.status().is_success() {
268 return Err(ConfigError::Message("Vault request failed".into()));
269 }
270
271 let raw: serde_json::Value = resp.json().map_err(|e| ConfigError::Foreign(Box::new(e)))?;
272 let json_obj = raw
273 .get("data")
274 .and_then(|x| {
275 if self.kv_version == KvVersion::V2 {
276 x.get("data")
277 } else {
278 Some(x)
279 }
280 })
281 .and_then(|x| x.as_object())
282 .ok_or_else(|| ConfigError::Message("Vault response missing data".into()))?;
283
284 let mut secret = std::collections::HashMap::new();
285 flatten_json(
286 "",
287 &serde_json::Value::Object(json_obj.clone()),
288 &mut secret,
289 );
290 Ok(secret)
291 }
292}
293
294#[cfg(feature = "async")]
295#[async_trait]
296impl AsyncSource for VaultSource {
297 async fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
298 let client = self.build_async_client()?;
299 let url = self.build_read_url()?;
300 let resp = client
301 .get(url)
302 .header("X-Vault-Token", &self.config.token)
303 .send()
304 .await
305 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
306
307 if !resp.status().is_success() {
308 return Err(ConfigError::Message("Vault request failed".into()));
309 }
310
311 let raw: serde_json::Value = resp
312 .json()
313 .await
314 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
315 let json_obj = raw
316 .get("data")
317 .and_then(|x| {
318 if self.kv_version == KvVersion::V2 {
319 x.get("data")
320 } else {
321 Some(x)
322 }
323 })
324 .and_then(|x| x.as_object())
325 .ok_or_else(|| ConfigError::Message("Vault response missing data".into()))?;
326
327 let mut secret = std::collections::HashMap::new();
328 flatten_json(
329 "",
330 &serde_json::Value::Object(json_obj.clone()),
331 &mut secret,
332 );
333 Ok(secret)
334 }
335}