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::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 KvVersion {
171 pub fn api_path(&self, mount: &str, path: &str) -> String {
172 match self {
173 KvVersion::V1 => format!("v1/{}/{}", mount, path),
174 KvVersion::V2 => format!("v1/{}/data/{}", mount, path),
175 }
176 }
177}
178
179impl VaultSource {
180 pub fn builder() -> VaultSourceBuilder {
181 VaultSourceBuilder::new()
182 }
183
184 fn build_blocking_client(&self) -> Result<reqwest::blocking::Client, ConfigError> {
185 let mut builder = reqwest::blocking::Client::builder();
186
187 #[cfg(feature = "tls")]
188 if let Some(tls) = &self.config.tls {
189 if let Some(ca_bytes) = &tls.ca_cert_bytes {
190 let cert = reqwest::Certificate::from_pem(&ca_bytes)
191 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
192
193 builder = builder.add_root_certificate(cert);
194 }
195
196 if let (Some(cert), Some(key)) = (&tls.client_cert, &tls.client_key) {
197 let mut identity_bytes = cert.clone();
198 identity_bytes.extend_from_slice(key);
199 let identity = reqwest::Identity::from_pem(&identity_bytes)
200 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
201 builder = builder.identity(identity);
202 }
203
204 if tls.danger_accept_invalid_certs {
205 builder = builder.danger_accept_invalid_certs(true);
206 }
207 }
208
209 builder
210 .build()
211 .map_err(|e| ConfigError::Foreign(Box::new(e)))
212 }
213
214 #[cfg(feature = "async")]
215 fn build_async_client(&self) -> Result<reqwest::Client, ConfigError> {
216 let mut builder = reqwest::Client::builder();
217
218 #[cfg(feature = "tls")]
219 if let Some(tls) = &self.config.tls {
220 if let Some(ca_bytes) = &tls.ca_cert_bytes {
221 let cert = reqwest::Certificate::from_pem(&ca_bytes)
222 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
223
224 builder = builder.add_root_certificate(cert);
225 }
226
227 if let (Some(cert), Some(key)) = (&tls.client_cert, &tls.client_key) {
228 let mut identity_bytes = cert.clone();
229 identity_bytes.extend_from_slice(key);
230 let identity = reqwest::Identity::from_pem(&identity_bytes)
231 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
232 builder = builder.identity(identity);
233 }
234
235 if tls.danger_accept_invalid_certs {
236 builder = builder.danger_accept_invalid_certs(true);
237 }
238 }
239
240 builder
241 .build()
242 .map_err(|e| ConfigError::Foreign(Box::new(e)))
243 }
244}
245
246impl Source for VaultSource {
247 fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
248 Box::new(self.clone())
249 }
250
251 fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
252 let client = self.build_blocking_client()?;
253 let resp = client
254 .get(self.config.address.as_str())
255 .header("X-Vault-Token", &self.config.token)
256 .send()
257 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
258
259 if !resp.status().is_success() {
260 return Err(ConfigError::Message("Vault request failed".into()));
261 }
262
263 let raw: serde_json::Value = resp.json().map_err(|e| ConfigError::Foreign(Box::new(e)))?;
264 let json_obj = raw
265 .get("data")
266 .and_then(|x| {
267 if self.kv_version == KvVersion::V2 {
268 x.get("data")
269 } else {
270 Some(x)
271 }
272 })
273 .and_then(|x| x.as_object())
274 .ok_or_else(|| ConfigError::Message("Vault response missing data".into()))?;
275
276 let mut secret = std::collections::HashMap::new();
277 flatten_json(
278 "",
279 &serde_json::Value::Object(json_obj.clone()),
280 &mut secret,
281 );
282 Ok(secret)
283 }
284}
285
286#[cfg(feature = "async")]
287#[async_trait]
288impl AsyncSource for VaultSource {
289 async fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
290 let client = self.build_async_client()?;
291
292 let resp = client
293 .get(self.config.address.as_str())
294 .header("X-Vault-Token", &self.config.token)
295 .send()
296 .await
297 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
298
299 if !resp.status().is_success() {
300 return Err(ConfigError::Message("Vault request failed".into()));
301 }
302
303 let raw: serde_json::Value = resp
304 .json()
305 .await
306 .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
307 let json_obj = raw
308 .get("data")
309 .and_then(|x| {
310 if self.kv_version == KvVersion::V2 {
311 x.get("data")
312 } else {
313 Some(x)
314 }
315 })
316 .and_then(|x| x.as_object())
317 .ok_or_else(|| ConfigError::Message("Vault response missing data".into()))?;
318
319 let mut secret = std::collections::HashMap::new();
320 flatten_json(
321 "",
322 &serde_json::Value::Object(json_obj.clone()),
323 &mut secret,
324 );
325 Ok(secret)
326 }
327}