Skip to main content

secrets_provider_vault_kv/
lib.rs

1//! HashiCorp Vault KV v2 backend using the live Vault HTTP API.
2//!
3//! Each secret is persisted under a KV v2 mount in a directory structure that
4//! mirrors the Greentic scope (`env/tenant/[team]/category/name`). Secret
5//! records are serialized to JSON and stored in the `data` field as a base64
6//! string. The provider also integrates with Vault Transit to wrap and unwrap
7//! data encryption keys.
8
9use 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
38/// Components returned to the broker wiring.
39pub struct BackendComponents {
40    pub backend: Box<dyn SecretsBackend>,
41    pub key_provider: Box<dyn KeyProvider>,
42}
43
44/// Construct the Vault backend and transit key provider from environment configuration.
45pub 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 understands numeric keys
634    #[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}