1use anyhow::{Context, Result, bail};
10use base64::{Engine, engine::general_purpose::STANDARD};
11use greentic_secrets_core::rt::sync_await;
12use greentic_secrets_spec::{
13 Envelope, KeyProvider, Scope, SecretListItem, SecretMeta, SecretRecord, SecretUri,
14 SecretVersion, SecretsBackend, SecretsError, SecretsResult, VersionedSecret,
15};
16use reqwest::{Client, Method, Response, StatusCode};
17use serde::{Deserialize, Serialize};
18use serde_json::{Map, Value, json};
19use std::collections::HashMap;
20use std::fs;
21use std::sync::Arc;
22use std::time::Duration;
23
24const DEFAULT_KV_MOUNT: &str = "secret";
25const DEFAULT_KV_PREFIX: &str = "greentic";
26const DEFAULT_TRANSIT_MOUNT: &str = "transit";
27const DEFAULT_TRANSIT_KEY: &str = "greentic";
28const TEAM_PLACEHOLDER: &str = "_";
29
30fn read_body(response: Response) -> SecretsResult<String> {
31 sync_await(async {
32 response.text().await.map_err(|err| {
33 SecretsError::Backend(format!("failed to read vault response body: {err}"))
34 })
35 })
36}
37
38pub struct BackendComponents {
40 pub backend: Box<dyn SecretsBackend>,
41 pub key_provider: Box<dyn KeyProvider>,
42}
43
44pub async fn build_backend() -> Result<BackendComponents> {
46 let config = Arc::new(VaultProviderConfig::from_env()?);
47 let client = config.build_http_client()?;
48
49 let backend = VaultSecretsBackend::new(config.clone(), client.clone());
50 let key_provider = VaultTransitProvider::new(config, client);
51 Ok(BackendComponents {
52 backend: Box::new(backend),
53 key_provider: Box::new(key_provider),
54 })
55}
56
57#[derive(Clone)]
58struct VaultSecretsBackend {
59 config: Arc<VaultProviderConfig>,
60 client: Client,
61}
62
63impl VaultSecretsBackend {
64 fn new(config: Arc<VaultProviderConfig>, client: Client) -> Self {
65 Self { config, client }
66 }
67
68 fn request(&self, method: Method, path: &str, body: Option<Value>) -> SecretsResult<Response> {
69 self.config.request(&self.client, method, path, body)
70 }
71
72 fn kv_data_path(&self, uri: &SecretUri) -> String {
73 let team = uri.scope().team().unwrap_or(TEAM_PLACEHOLDER);
74 format!(
75 "{prefix}/{env}/{tenant}/{team}/{category}/{name}",
76 prefix = self.config.kv_prefix,
77 env = uri.scope().env(),
78 tenant = uri.scope().tenant(),
79 team = team,
80 category = uri.category(),
81 name = uri.name()
82 )
83 }
84
85 fn kv_api_path(&self, suffix: &str) -> String {
86 format!(
87 "v1/{mount}/{suffix}",
88 mount = self.config.kv_mount.trim_matches('/'),
89 suffix = suffix.trim_start_matches('/')
90 )
91 }
92
93 fn list_keys(&self, prefix: &str) -> SecretsResult<Vec<String>> {
94 let path = self.kv_api_path(&format!(
95 "metadata/{suffix}",
96 suffix = prefix.trim_start_matches('/')
97 ));
98 let method = Method::from_bytes(b"LIST").expect("LIST method supported");
99 let response = self.request(method, &path, None)?;
100 match response.status() {
101 StatusCode::NOT_FOUND => Ok(Vec::new()),
102 status if status.is_success() => {
103 let body = read_body(response)?;
104 let list: KeyListResponse = serde_json::from_str(&body).map_err(|err| {
105 SecretsError::Storage(format!(
106 "failed to decode vault key list: {err}; body={body}"
107 ))
108 })?;
109 Ok(list.data.keys.unwrap_or_default())
110 }
111 status => {
112 let body = read_body(response)?;
113 Err(SecretsError::Storage(format!(
114 "list keys failed: {status} {body}"
115 )))
116 }
117 }
118 }
119
120 fn write_secret(&self, uri: &SecretUri, payload: Option<StoredRecord>) -> SecretsResult<u64> {
121 let data_path = self.kv_data_path(uri);
122 let path = self.kv_api_path(&format!("data/{data_path}"));
123 let mut data_obj = Map::new();
124 if let Some(record) = payload {
125 let encoded = serde_json::to_vec(&record).map_err(|err| {
126 SecretsError::Storage(format!("failed to encode secret payload: {err}"))
127 })?;
128 data_obj.insert("record".into(), Value::String(STANDARD.encode(encoded)));
129 } else {
130 data_obj.insert("__greentic_deleted".into(), Value::Bool(true));
131 }
132 let mut body_obj = Map::new();
133 body_obj.insert("data".into(), Value::Object(data_obj));
134 let body = Value::Object(body_obj);
135 let response = self.request(Method::POST, &path, Some(body))?;
136 let status = response.status();
137 let body = read_body(response)?;
138 if !status.is_success() {
139 return Err(SecretsError::Storage(format!(
140 "write secret failed: {status} {body}"
141 )));
142 }
143 let parsed: KvWriteResponse = serde_json::from_str(&body).map_err(|err| {
144 SecretsError::Storage(format!(
145 "failed to decode vault write response: {err}; body={body}"
146 ))
147 })?;
148 let metadata = parsed.data.into_metadata()?;
149 metadata.version_or(None)
150 }
151
152 fn read_secret(
153 &self,
154 uri: &SecretUri,
155 version: Option<u64>,
156 ) -> SecretsResult<Option<SecretSnapshot>> {
157 let data_path = self.kv_data_path(uri);
158 let mut path = self.kv_api_path(&format!("data/{data_path}"));
159 if let Some(v) = version {
160 path.push_str(&format!("?version={v}"));
161 }
162 let response = self.request(Method::GET, &path, None)?;
163 match response.status() {
164 StatusCode::NOT_FOUND => Ok(None),
165 status if status.is_success() => {
166 let body = read_body(response)?;
167 let parsed: KvReadResponse = serde_json::from_str(&body).map_err(|err| {
168 SecretsError::Storage(format!(
169 "failed to decode vault read response: {err}; body={body}"
170 ))
171 })?;
172 let metadata = parsed.data.metadata;
173 let version = metadata.version_or(version)?;
174 let deleted = metadata.destroyed
175 || !metadata.deletion_time.is_empty()
176 || parsed.data.data.greentic_deleted.unwrap_or(false);
177 if deleted {
178 return Ok(Some(SecretSnapshot {
179 version,
180 deleted: true,
181 record: None,
182 }));
183 }
184 let record = parsed
185 .data
186 .data
187 .record
188 .map(|value| decode_stored_record(&value))
189 .transpose()?;
190 Ok(Some(SecretSnapshot {
191 version,
192 deleted: false,
193 record,
194 }))
195 }
196 status => {
197 let body = read_body(response)?;
198 Err(SecretsError::Storage(format!(
199 "read secret failed: {status} {body}"
200 )))
201 }
202 }
203 }
204
205 fn list_versions(&self, uri: &SecretUri) -> SecretsResult<Vec<SecretVersionEntry>> {
206 let metadata_path =
207 self.kv_api_path(&format!("metadata/{data}", data = self.kv_data_path(uri)));
208 let response = self.request(Method::GET, &metadata_path, None)?;
209 match response.status() {
210 StatusCode::NOT_FOUND => Ok(Vec::new()),
211 status if status.is_success() => {
212 let body = read_body(response)?;
213 let parsed: KvMetadataResponse = serde_json::from_str(&body).map_err(|err| {
214 SecretsError::Storage(format!(
215 "failed to decode metadata response: {err}; body={body}"
216 ))
217 })?;
218 Ok(self.fold_metadata_versions(uri, parsed.data)?)
219 }
220 status => {
221 let body = read_body(response)?;
222 Err(SecretsError::Storage(format!(
223 "metadata lookup failed: {status} {body}"
224 )))
225 }
226 }
227 }
228
229 fn fold_metadata_versions(
230 &self,
231 uri: &SecretUri,
232 data: KvMetadataData,
233 ) -> SecretsResult<Vec<SecretVersionEntry>> {
234 let mut entries = Vec::new();
235 if let Some(map) = data.versions {
236 for (version, meta) in map {
237 let snapshot = self.read_secret(uri, Some(version))?;
238 let resolved_version = meta.version_or(Some(version))?;
239 let deleted = snapshot.is_none_or(|snap| {
240 snap.deleted || meta.destroyed || !meta.deletion_time.is_empty()
241 });
242 entries.push(SecretVersionEntry {
243 version: resolved_version,
244 deleted,
245 });
246 }
247 } else if let Some(latest) = data.current_version {
248 let snapshot = self.read_secret(uri, Some(latest))?;
249 let deleted = snapshot.is_none_or(|snap| snap.deleted);
250 entries.push(SecretVersionEntry {
251 version: latest,
252 deleted,
253 });
254 }
255 entries.sort_by_key(|entry| entry.version);
256 Ok(entries)
257 }
258
259 fn list_secrets_for_scope(&self, scope: &Scope) -> SecretsResult<Vec<SecretUri>> {
260 let team_segment = scope.team().unwrap_or(TEAM_PLACEHOLDER);
261 let base_path = format!(
262 "{}/{}/{}/{}",
263 self.config.kv_prefix,
264 scope.env(),
265 scope.tenant(),
266 team_segment
267 );
268
269 let mut uris = Vec::new();
270 for category_key in self.list_keys(&base_path)? {
271 let category = category_key.trim_end_matches('/');
272 if category.is_empty() {
273 continue;
274 }
275 let names_path = format!("{base_path}/{category}");
276 for name_key in self.list_keys(&names_path)? {
277 let name = name_key.trim_end_matches('/');
278 if name.is_empty() {
279 continue;
280 }
281 let scope_clone = Scope::new(
282 scope.env().to_string(),
283 scope.tenant().to_string(),
284 scope.team().map(|v| v.to_string()),
285 )?;
286 let uri = SecretUri::new(scope_clone, category, name)?;
287 uris.push(uri);
288 }
289 }
290 Ok(uris)
291 }
292}
293
294impl SecretsBackend for VaultSecretsBackend {
295 fn put(&self, record: SecretRecord) -> SecretsResult<SecretVersion> {
296 let stored = StoredRecord::from_record(&record)?;
297 let version = self.write_secret(&record.meta.uri, Some(stored))?;
298 Ok(SecretVersion {
299 version,
300 deleted: false,
301 })
302 }
303
304 fn get(&self, uri: &SecretUri, version: Option<u64>) -> SecretsResult<Option<VersionedSecret>> {
305 match self.read_secret(uri, version)? {
306 Some(snapshot) => snapshot.into_versioned(),
307 None => Ok(None),
308 }
309 }
310
311 fn list(
312 &self,
313 scope: &Scope,
314 category_prefix: Option<&str>,
315 name_prefix: Option<&str>,
316 ) -> SecretsResult<Vec<SecretListItem>> {
317 let mut items = Vec::new();
318 for uri in self.list_secrets_for_scope(scope)? {
319 if let Some(prefix) = category_prefix
320 && !uri.category().starts_with(prefix)
321 {
322 continue;
323 }
324 if let Some(prefix) = name_prefix
325 && !uri.name().starts_with(prefix)
326 {
327 continue;
328 }
329 if let Some(versioned) = self.get(&uri, None)?
330 && let Some(record) = versioned.record()
331 {
332 items.push(SecretListItem::from_meta(
333 &record.meta,
334 Some(versioned.version.to_string()),
335 ));
336 }
337 }
338 Ok(items)
339 }
340
341 fn delete(&self, uri: &SecretUri) -> SecretsResult<SecretVersion> {
342 if self.get(uri, None)?.is_none() {
343 return Err(SecretsError::NotFound {
344 entity: uri.to_string(),
345 });
346 }
347 let version = self.write_secret(uri, None)?;
348 Ok(SecretVersion {
349 version,
350 deleted: true,
351 })
352 }
353
354 fn versions(&self, uri: &SecretUri) -> SecretsResult<Vec<SecretVersion>> {
355 Ok(self
356 .list_versions(uri)?
357 .into_iter()
358 .map(|entry| SecretVersion {
359 version: entry.version,
360 deleted: entry.deleted,
361 })
362 .collect())
363 }
364
365 fn exists(&self, uri: &SecretUri) -> SecretsResult<bool> {
366 Ok(self.get(uri, None)?.is_some())
367 }
368}
369
370#[derive(Clone)]
371struct VaultTransitProvider {
372 config: Arc<VaultProviderConfig>,
373 client: Client,
374}
375
376impl VaultTransitProvider {
377 fn new(config: Arc<VaultProviderConfig>, client: Client) -> Self {
378 Self { config, client }
379 }
380
381 fn request_transit(&self, operation: &str, body: Value) -> SecretsResult<Value> {
382 let path = format!(
383 "v1/{}/{}{}",
384 self.config.transit_mount.trim_matches('/'),
385 operation,
386 self.config.transit_key
387 );
388 let response = self.request(Method::POST, &path, Some(body))?;
389 let status = response.status();
390 let body = read_body(response)?;
391 if !status.is_success() {
392 return Err(SecretsError::Backend(format!(
393 "vault transit call failed: {status} {body}"
394 )));
395 }
396 serde_json::from_str(&body).map_err(|err| {
397 SecretsError::Backend(format!(
398 "failed to parse transit response: {err}; body={body}"
399 ))
400 })
401 }
402
403 fn request(&self, method: Method, path: &str, body: Option<Value>) -> SecretsResult<Response> {
404 self.config.request(&self.client, method, path, body)
405 }
406}
407
408impl KeyProvider for VaultTransitProvider {
409 fn wrap_dek(&self, _scope: &Scope, dek: &[u8]) -> SecretsResult<Vec<u8>> {
410 let body = json!({"plaintext": STANDARD.encode(dek)});
411 let response = self.request_transit("encrypt/", body)?;
412 let ciphertext = response
413 .get("data")
414 .and_then(|data| data.get("ciphertext"))
415 .and_then(|value| value.as_str())
416 .ok_or_else(|| SecretsError::Backend("encrypt response missing ciphertext".into()))?;
417 Ok(ciphertext.as_bytes().to_vec())
418 }
419
420 fn unwrap_dek(&self, _scope: &Scope, wrapped: &[u8]) -> SecretsResult<Vec<u8>> {
421 let ciphertext = std::str::from_utf8(wrapped)
422 .map_err(|_| SecretsError::Backend("invalid ciphertext encoding".into()))?;
423 let body = json!({"ciphertext": ciphertext});
424 let response = self.request_transit("decrypt/", body)?;
425 let plaintext = response
426 .get("data")
427 .and_then(|data| data.get("plaintext"))
428 .and_then(|value| value.as_str())
429 .ok_or_else(|| SecretsError::Backend("decrypt response missing plaintext".into()))?;
430 STANDARD
431 .decode(plaintext.as_bytes())
432 .map_err(|err| SecretsError::Backend(format!("failed to decode plaintext: {err}")))
433 }
434}
435
436#[derive(Clone, Debug)]
437struct VaultProviderConfig {
438 addr: String,
439 token: String,
440 namespace: Option<String>,
441 kv_mount: String,
442 kv_prefix: String,
443 transit_mount: String,
444 transit_key: String,
445 timeout: Duration,
446 ca_bundle: Option<Vec<u8>>,
447 insecure_skip_tls: bool,
448}
449
450impl VaultProviderConfig {
451 fn from_env() -> Result<Self> {
452 let addr = std::env::var("VAULT_ADDR").context("set VAULT_ADDR to the Vault server URL")?;
453 let token =
454 std::env::var("VAULT_TOKEN").context("set VAULT_TOKEN for Vault authentication")?;
455 let namespace = std::env::var("VAULT_NAMESPACE").ok();
456 let kv_mount =
457 std::env::var("VAULT_KV_MOUNT").unwrap_or_else(|_| DEFAULT_KV_MOUNT.to_string());
458 let kv_prefix =
459 std::env::var("VAULT_KV_PREFIX").unwrap_or_else(|_| DEFAULT_KV_PREFIX.to_string());
460 let transit_mount = std::env::var("VAULT_TRANSIT_MOUNT")
461 .unwrap_or_else(|_| DEFAULT_TRANSIT_MOUNT.to_string());
462 let transit_key =
463 std::env::var("VAULT_TRANSIT_KEY").unwrap_or_else(|_| DEFAULT_TRANSIT_KEY.to_string());
464 let timeout = std::env::var("VAULT_HTTP_TIMEOUT_SECS")
465 .ok()
466 .and_then(|value| value.parse::<u64>().ok())
467 .filter(|value| *value > 0)
468 .map(Duration::from_secs)
469 .unwrap_or_else(|| Duration::from_secs(15));
470 let ca_bundle = std::env::var("VAULT_CA_BUNDLE")
471 .ok()
472 .map(|path| fs::read(path).context("failed to read VAULT_CA_BUNDLE"))
473 .transpose()?;
474 let insecure_skip_tls = std::env::var("VAULT_INSECURE_SKIP_TLS")
475 .map(|value| matches!(value.as_str(), "1" | "true" | "TRUE"))
476 .unwrap_or(false);
477
478 Ok(Self {
479 addr,
480 token,
481 namespace,
482 kv_mount,
483 kv_prefix,
484 transit_mount,
485 transit_key,
486 timeout,
487 ca_bundle,
488 insecure_skip_tls,
489 })
490 }
491
492 fn build_http_client(&self) -> Result<Client> {
493 let mut builder = Client::builder().timeout(self.timeout).use_rustls_tls();
494 if let Some(ca) = self.ca_bundle.as_ref() {
495 let cert = reqwest::Certificate::from_pem(ca)
496 .or_else(|_| reqwest::Certificate::from_der(ca))
497 .context("failed to parse VAULT_CA_BUNDLE")?;
498 builder = builder.add_root_certificate(cert);
499 }
500 if self.insecure_skip_tls {
501 bail!("VAULT_INSECURE_SKIP_TLS is not permitted");
502 }
503 builder.build().context("failed to build Vault HTTP client")
504 }
505
506 fn request(
507 &self,
508 client: &Client,
509 method: Method,
510 path: &str,
511 body: Option<Value>,
512 ) -> SecretsResult<Response> {
513 sync_await(self.request_async(client, method, path, body))
514 }
515
516 async fn request_async(
517 &self,
518 client: &Client,
519 method: Method,
520 path: &str,
521 body: Option<Value>,
522 ) -> SecretsResult<Response> {
523 let url = format!(
524 "{}/{}",
525 self.addr.trim_end_matches('/'),
526 path.trim_start_matches('/')
527 );
528 let mut builder = client.request(method, url);
529 builder = builder.header("X-Vault-Token", &self.token);
530 if let Some(namespace) = &self.namespace {
531 builder = builder.header("X-Vault-Namespace", namespace);
532 }
533 if let Some(payload) = body {
534 builder = builder.json(&payload);
535 }
536 builder
537 .send()
538 .await
539 .map_err(|err| SecretsError::Backend(format!("vault request failed: {err}")))
540 }
541}
542
543#[derive(Deserialize)]
544struct KeyListResponse {
545 data: KeyListData,
546}
547
548#[derive(Deserialize)]
549struct KeyListData {
550 keys: Option<Vec<String>>,
551}
552
553#[derive(Deserialize)]
554struct KvWriteResponse {
555 data: KvWriteData,
556}
557
558#[derive(Deserialize)]
559struct KvWriteData {
560 #[serde(default)]
561 metadata: Option<VersionMetadata>,
562 #[serde(default)]
563 version: Option<u64>,
564 #[serde(default)]
565 destroyed: Option<bool>,
566 #[serde(default)]
567 deletion_time: Option<String>,
568}
569
570impl KvWriteData {
571 fn into_metadata(self) -> SecretsResult<VersionMetadata> {
572 if let Some(meta) = self.metadata {
573 return Ok(meta);
574 }
575 let version = self.version.ok_or_else(|| {
576 SecretsError::Storage(
577 "vault write response missing version metadata (enable kv v2?)".into(),
578 )
579 })?;
580 Ok(VersionMetadata {
581 version: Some(version),
582 destroyed: self.destroyed.unwrap_or(false),
583 deletion_time: self.deletion_time.unwrap_or_default(),
584 })
585 }
586}
587
588#[derive(Deserialize)]
589struct VersionMetadata {
590 #[serde(default)]
591 version: Option<u64>,
592 #[serde(default)]
593 destroyed: bool,
594 #[serde(default)]
595 deletion_time: String,
596}
597
598impl VersionMetadata {
599 fn version_or(&self, fallback: Option<u64>) -> SecretsResult<u64> {
600 self.version
601 .or(fallback)
602 .ok_or_else(|| SecretsError::Storage("vault metadata missing version".into()))
603 }
604}
605
606#[derive(Deserialize)]
607struct KvReadResponse {
608 data: KvDataEnvelope,
609}
610
611#[derive(Deserialize)]
612struct KvDataEnvelope {
613 data: KvRecordData,
614 metadata: VersionMetadata,
615}
616
617#[derive(Deserialize)]
618struct KvRecordData {
619 #[serde(default)]
620 record: Option<String>,
621 #[serde(default, rename = "__greentic_deleted")]
622 greentic_deleted: Option<bool>,
623}
624
625#[derive(Deserialize)]
626struct KvMetadataResponse {
627 data: KvMetadataData,
628}
629
630#[derive(Deserialize)]
631struct KvMetadataData {
632 #[serde(default)]
633 versions: Option<HashMap<u64, VersionMetadata>>, #[serde(default)]
635 current_version: Option<u64>,
636}
637
638struct SecretVersionEntry {
639 version: u64,
640 deleted: bool,
641}
642
643struct SecretSnapshot {
644 version: u64,
645 deleted: bool,
646 record: Option<StoredRecord>,
647}
648
649impl SecretSnapshot {
650 fn into_versioned(self) -> SecretsResult<Option<VersionedSecret>> {
651 if self.deleted {
652 return Ok(None);
653 }
654
655 let record = self
656 .record
657 .ok_or_else(|| SecretsError::Storage("missing secret record".into()))?
658 .into_record()?;
659
660 Ok(Some(VersionedSecret {
661 version: self.version,
662 deleted: false,
663 record: Some(record),
664 }))
665 }
666}
667
668#[derive(Clone, Serialize, Deserialize)]
669struct StoredRecord {
670 meta: SecretMeta,
671 envelope: StoredEnvelope,
672 value: String,
673}
674
675impl StoredRecord {
676 fn from_record(record: &SecretRecord) -> SecretsResult<Self> {
677 Ok(Self {
678 meta: record.meta.clone(),
679 envelope: StoredEnvelope::from_envelope(&record.envelope),
680 value: STANDARD.encode(&record.value),
681 })
682 }
683
684 fn into_record(self) -> SecretsResult<SecretRecord> {
685 Ok(SecretRecord::new(
686 self.meta,
687 decode_bytes(&self.value)?,
688 self.envelope.into_envelope()?,
689 ))
690 }
691}
692
693#[derive(Clone, Serialize, Deserialize, Default)]
694struct StoredEnvelope {
695 algorithm: String,
696 nonce: String,
697 hkdf_salt: String,
698 wrapped_dek: String,
699}
700
701impl StoredEnvelope {
702 fn from_envelope(envelope: &Envelope) -> Self {
703 Self {
704 algorithm: envelope.algorithm.to_string(),
705 nonce: STANDARD.encode(&envelope.nonce),
706 hkdf_salt: STANDARD.encode(&envelope.hkdf_salt),
707 wrapped_dek: STANDARD.encode(&envelope.wrapped_dek),
708 }
709 }
710
711 fn into_envelope(self) -> SecretsResult<Envelope> {
712 Ok(Envelope {
713 algorithm: self
714 .algorithm
715 .parse()
716 .map_err(|_| SecretsError::Storage("invalid algorithm".into()))?,
717 nonce: decode_bytes(&self.nonce)?,
718 hkdf_salt: decode_bytes(&self.hkdf_salt)?,
719 wrapped_dek: decode_bytes(&self.wrapped_dek)?,
720 })
721 }
722}
723
724fn decode_stored_record(encoded: &str) -> SecretsResult<StoredRecord> {
725 let bytes = STANDARD
726 .decode(encoded.as_bytes())
727 .map_err(|err| SecretsError::Storage(format!("failed to decode stored payload: {err}")))?;
728 serde_json::from_slice(&bytes)
729 .map_err(|err| SecretsError::Storage(format!("failed to decode stored record: {err}")))
730}
731
732fn decode_bytes(input: &str) -> SecretsResult<Vec<u8>> {
733 STANDARD
734 .decode(input.as_bytes())
735 .map_err(|err| SecretsError::Storage(err.to_string()))
736}
737
738#[cfg(test)]
739mod tests {
740 use super::*;
741 use once_cell::sync::Lazy;
742 use std::sync::Mutex;
743
744 static ENV_GUARD: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
745
746 struct EnvReset {
747 vars: Vec<(&'static str, Option<String>)>,
748 }
749
750 impl EnvReset {
751 fn new(pairs: &[(&'static str, &str)]) -> Self {
752 let mut vars = Vec::new();
753 for (key, value) in pairs {
754 vars.push((*key, std::env::var(key).ok()));
755 unsafe { std::env::set_var(key, value) };
756 }
757 Self { vars }
758 }
759 }
760
761 impl Drop for EnvReset {
762 fn drop(&mut self) {
763 for (key, previous) in self.vars.drain(..) {
764 if let Some(val) = previous {
765 unsafe { std::env::set_var(key, val) };
766 } else {
767 unsafe { std::env::remove_var(key) };
768 }
769 }
770 }
771 }
772
773 #[tokio::test(flavor = "multi_thread")]
774 async fn vault_provider_does_not_panic_under_tokio() {
775 let _guard = ENV_GUARD.lock().expect("env guard");
776 let _env = EnvReset::new(&[
777 ("VAULT_ADDR", "http://127.0.0.1:9"),
778 ("VAULT_TOKEN", "test-token"),
779 ]);
780
781 let config = Arc::new(VaultProviderConfig::from_env().expect("vault config"));
782 let client = config.build_http_client().expect("http client");
783 let backend = VaultSecretsBackend::new(config, client);
784
785 let scope = Scope::new(String::from("env"), String::from("tenant"), None).expect("scope");
786 let uri = SecretUri::new(scope, "category", "name").expect("uri");
787
788 let result = backend.get(&uri, None);
789 assert!(result.is_err());
790 }
791}