config_vault_source/
lib.rs1pub mod builder;
124mod utils;
125
126pub use builder::VaultSourceBuilder;
127
128use std::ops::Deref;
129use std::str::FromStr;
130
131use config::{Config, Source};
132use config::{ConfigError, Map, Value};
133
134#[cfg(feature = "async")]
135use async_trait::async_trait;
136#[cfg(feature = "async")]
137use config::AsyncSource;
138use url::Url;
139
140use crate::utils::flatten_json;
141
142#[derive(Debug, Clone, PartialEq)]
161pub struct VaultSource {
162 config: VaultConfig,
163 kv_version: KvVersion,
164}
165
166#[derive(Debug, Clone, PartialEq)]
167pub struct VaultAddr(Url);
168
169impl Deref for VaultAddr {
170 type Target = Url;
171
172 fn deref(&self) -> &Self::Target {
173 &self.0
174 }
175}
176
177impl FromStr for VaultAddr {
178 type Err = ConfigError;
179
180 fn from_str(s: &str) -> Result<Self, Self::Err> {
181 let url = url::Url::parse(s)
182 .map_err(|e| ConfigError::Message(format!("Invalid Vault address: {e}")))?;
183
184 if url.cannot_be_a_base() {
185 return Err(ConfigError::Message(
186 "Vault address cannot be a base URL".into(),
187 ));
188 }
189
190 if !url.path().trim_matches('/').is_empty() {
191 return Err(ConfigError::Message(
192 "Vault address must not contain a path (e.g. use https://host:8200, not https://host:8200/v1)"
193 .into(),
194 ));
195 }
196
197 Ok(VaultAddr(url))
198 }
199}
200
201impl TryFrom<&str> for VaultAddr {
202 type Error = ConfigError;
203
204 fn try_from(value: &str) -> Result<Self, Self::Error> {
205 value.parse()
206 }
207}
208
209impl TryFrom<String> for VaultAddr {
210 type Error = ConfigError;
211
212 fn try_from(value: String) -> Result<Self, Self::Error> {
213 value.parse()
214 }
215}
216
217#[derive(Debug, Clone, PartialEq)]
218pub struct VaultConfig {
219 pub address: VaultAddr,
220 pub token: String,
221 pub mount: String,
222 pub path: String,
223
224 #[cfg(feature = "tls")]
225 pub tls: Option<TlsConfig>,
226}
227
228#[cfg(feature = "tls")]
229#[derive(Debug, Clone, PartialEq)]
230pub struct TlsConfig {
231 pub ca_cert_bytes: Option<Vec<u8>>,
232 pub client_cert: Option<Vec<u8>>,
233 pub client_key: Option<Vec<u8>>,
234 pub danger_accept_invalid_certs: bool,
235}
236
237#[derive(Debug, Clone, PartialEq, Default)]
238pub enum KvVersion {
239 V1,
240 #[default]
241 V2,
242}
243
244impl VaultSource {
245 pub fn builder() -> VaultSourceBuilder {
246 VaultSourceBuilder::new()
247 }
248
249 fn build_read_url(&self) -> Result<url::Url, ConfigError> {
250 let api_path = match self.kv_version {
251 KvVersion::V1 => format!("v1/{}/{}", self.config.mount, self.config.path),
252 KvVersion::V2 => format!("v1/{}/data/{}", self.config.mount, self.config.path),
253 };
254
255 let mut url = self.config.address.0.clone();
256
257 url.path_segments_mut()
258 .map_err(|_| ConfigError::Message("Vault base URL cannot be a base".into()))?
259 .pop_if_empty()
260 .extend(api_path.split("/"));
261
262 Ok(url)
263 }
264
265 fn build_blocking_client(&self) -> Result<reqwest::blocking::Client, ConfigError> {
266 let mut builder = reqwest::blocking::Client::builder();
267
268 #[cfg(feature = "tls")]
269 if let Some(tls) = &self.config.tls {
270 if let Some(ca_bytes) = &tls.ca_cert_bytes {
271 let cert = reqwest::Certificate::from_pem(&ca_bytes)
272 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
273
274 builder = builder.add_root_certificate(cert);
275 }
276
277 if let (Some(cert), Some(key)) = (&tls.client_cert, &tls.client_key) {
278 let mut identity_bytes = cert.clone();
279 identity_bytes.extend_from_slice(key);
280 let identity = reqwest::Identity::from_pem(&identity_bytes)
281 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
282 builder = builder.identity(identity);
283 }
284
285 if tls.danger_accept_invalid_certs {
286 builder = builder.danger_accept_invalid_certs(true);
287 }
288 }
289
290 builder
291 .build()
292 .map_err(|e| ConfigError::Foreign(Box::new(e)))
293 }
294
295 #[cfg(feature = "async")]
296 fn build_async_client(&self) -> Result<reqwest::Client, ConfigError> {
297 let mut builder = reqwest::Client::builder();
298
299 #[cfg(feature = "tls")]
300 if let Some(tls) = &self.config.tls {
301 if let Some(ca_bytes) = &tls.ca_cert_bytes {
302 let cert = reqwest::Certificate::from_pem(&ca_bytes)
303 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
304
305 builder = builder.add_root_certificate(cert);
306 }
307
308 if let (Some(cert), Some(key)) = (&tls.client_cert, &tls.client_key) {
309 let mut identity_bytes = cert.clone();
310 identity_bytes.extend_from_slice(key);
311 let identity = reqwest::Identity::from_pem(&identity_bytes)
312 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
313 builder = builder.identity(identity);
314 }
315
316 if tls.danger_accept_invalid_certs {
317 builder = builder.danger_accept_invalid_certs(true);
318 }
319 }
320
321 builder
322 .build()
323 .map_err(|e| ConfigError::Foreign(Box::new(e)))
324 }
325}
326
327impl Source for VaultSource {
328 fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
329 Box::new(self.clone())
330 }
331
332 fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
333 let client = self.build_blocking_client()?;
334 let url = self.build_read_url()?;
335 let resp = client
336 .get(url)
337 .header("X-Vault-Token", &self.config.token)
338 .send()
339 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
340
341 if !resp.status().is_success() {
342 return Err(ConfigError::Message("Vault request failed".into()));
343 }
344
345 let raw: serde_json::Value = resp.json().map_err(|e| ConfigError::Foreign(Box::new(e)))?;
346 let json_obj = raw
347 .get("data")
348 .and_then(|x| {
349 if self.kv_version == KvVersion::V2 {
350 x.get("data")
351 } else {
352 Some(x)
353 }
354 })
355 .and_then(|x| x.as_object())
356 .ok_or_else(|| ConfigError::Message("Vault response missing data".into()))?;
357
358 let mut secret = std::collections::HashMap::new();
359 flatten_json(
360 "",
361 &serde_json::Value::Object(json_obj.clone()),
362 &mut secret,
363 );
364 Ok(secret)
365 }
366}
367
368#[cfg(feature = "async")]
369#[async_trait]
370impl AsyncSource for VaultSource {
371 async fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
372 let client = self.build_async_client()?;
373 let url = self.build_read_url()?;
374 let resp = client
375 .get(url)
376 .header("X-Vault-Token", &self.config.token)
377 .send()
378 .await
379 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
380
381 if !resp.status().is_success() {
382 return Err(ConfigError::Message("Vault request failed".into()));
383 }
384
385 let raw: serde_json::Value = resp
386 .json()
387 .await
388 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
389 let json_obj = raw
390 .get("data")
391 .and_then(|x| {
392 if self.kv_version == KvVersion::V2 {
393 x.get("data")
394 } else {
395 Some(x)
396 }
397 })
398 .and_then(|x| x.as_object())
399 .ok_or_else(|| ConfigError::Message("Vault response missing data".into()))?;
400
401 let mut secret = std::collections::HashMap::new();
402 flatten_json(
403 "",
404 &serde_json::Value::Object(json_obj.clone()),
405 &mut secret,
406 );
407 Ok(secret)
408 }
409}