mecha10_core/secrets/
vault.rs

1//! HashiCorp Vault Secret Backend
2
3use super::SecretBackend;
4use crate::{Mecha10Error, Result};
5
6/// Secret backend for HashiCorp Vault
7///
8/// Supports reading secrets from Vault's KV v2 secrets engine.
9///
10/// # Configuration
11///
12/// Set these environment variables:
13/// - `VAULT_ADDR`: Vault server address (e.g., "http://localhost:8200")
14/// - `VAULT_TOKEN`: Authentication token
15///
16/// # Example
17///
18/// ```rust
19/// use mecha10::secrets::{VaultBackend, SecretBackend};
20///
21/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
22/// let backend = VaultBackend::from_env("secret/mecha10").await?;
23///
24/// // Reads from: secret/data/mecha10/api_key
25/// let api_key = backend.get("api_key").await?;
26/// # Ok(())
27/// # }
28/// ```
29pub struct VaultBackend {
30    client: reqwest::Client,
31    addr: String,
32    token: String,
33    mount_path: String,
34}
35
36impl VaultBackend {
37    /// Create a new Vault backend
38    ///
39    /// # Arguments
40    ///
41    /// * `addr` - Vault server address
42    /// * `token` - Authentication token
43    /// * `mount_path` - Mount path for secrets (e.g., "secret/mecha10")
44    pub fn new(addr: String, token: String, mount_path: String) -> Self {
45        Self {
46            client: reqwest::Client::new(),
47            addr,
48            token,
49            mount_path,
50        }
51    }
52
53    /// Create from environment variables
54    ///
55    /// Reads `VAULT_ADDR` and `VAULT_TOKEN` from environment.
56    pub async fn from_env(mount_path: &str) -> Result<Self> {
57        let addr = std::env::var("VAULT_ADDR")
58            .map_err(|_| Mecha10Error::Configuration("VAULT_ADDR environment variable not set".to_string()))?;
59
60        let token = std::env::var("VAULT_TOKEN")
61            .map_err(|_| Mecha10Error::Configuration("VAULT_TOKEN environment variable not set".to_string()))?;
62
63        Ok(Self::new(addr, token, mount_path.to_string()))
64    }
65
66    fn make_path(&self, key: &str) -> String {
67        // For KV v2, path format is: /v1/{mount}/data/{path}
68        format!("/v1/{}/data/{}", self.mount_path, key)
69    }
70}
71
72#[async_trait::async_trait]
73impl SecretBackend for VaultBackend {
74    async fn get(&self, key: &str) -> Result<String> {
75        let url = format!("{}{}", self.addr, self.make_path(key));
76
77        let response = self
78            .client
79            .get(&url)
80            .header("X-Vault-Token", &self.token)
81            .send()
82            .await
83            .map_err(|e| Mecha10Error::Other(format!("Failed to connect to Vault: {}", e)))?;
84
85        if !response.status().is_success() {
86            return Err(Mecha10Error::Other(format!(
87                "Vault returned error status: {}",
88                response.status()
89            )));
90        }
91
92        let data: serde_json::Value = response
93            .json()
94            .await
95            .map_err(|e| Mecha10Error::Other(format!("Failed to parse Vault response: {}", e)))?;
96
97        // Extract secret from KV v2 response format
98        let secret = data
99            .get("data")
100            .and_then(|d| d.get("data"))
101            .and_then(|d| d.get("value"))
102            .and_then(|v| v.as_str())
103            .ok_or_else(|| Mecha10Error::Other(format!("Secret '{}' not found in Vault or invalid format", key)))?;
104
105        Ok(secret.to_string())
106    }
107
108    async fn set(&self, key: &str, value: &str) -> Result<()> {
109        let url = format!("{}{}", self.addr, self.make_path(key));
110
111        let payload = serde_json::json!({
112            "data": {
113                "value": value
114            }
115        });
116
117        let response = self
118            .client
119            .post(&url)
120            .header("X-Vault-Token", &self.token)
121            .json(&payload)
122            .send()
123            .await
124            .map_err(|e| Mecha10Error::Other(format!("Failed to connect to Vault: {}", e)))?;
125
126        if !response.status().is_success() {
127            return Err(Mecha10Error::Other(format!(
128                "Vault returned error status: {}",
129                response.status()
130            )));
131        }
132
133        Ok(())
134    }
135}