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