mecha10-core 0.6.5

Mecha10 core runtime and foundational types
Documentation
//! HashiCorp Vault Secret Backend

use super::SecretBackend;
use crate::{Mecha10Error, Result};

/// Secret backend for HashiCorp Vault
///
/// Supports reading secrets from Vault's KV v2 secrets engine.
///
/// # Configuration
///
/// Set these environment variables:
/// - `VAULT_ADDR`: Vault server address (e.g., "http://localhost:8200")
/// - `VAULT_TOKEN`: Authentication token
///
/// # Example
///
/// ```rust
/// use mecha10_core::secrets::{VaultBackend, SecretBackend};
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let backend = VaultBackend::from_env("secret/mecha10").await?;
///
/// // Reads from: secret/data/mecha10/api_key
/// let api_key = backend.get("api_key").await?;
/// # Ok(())
/// # }
/// ```
pub struct VaultBackend {
    client: reqwest::Client,
    addr: String,
    token: String,
    mount_path: String,
}

impl VaultBackend {
    /// Create a new Vault backend
    ///
    /// # Arguments
    ///
    /// * `addr` - Vault server address
    /// * `token` - Authentication token
    /// * `mount_path` - Mount path for secrets (e.g., "secret/mecha10")
    pub fn new(addr: String, token: String, mount_path: String) -> Self {
        Self {
            client: reqwest::Client::new(),
            addr,
            token,
            mount_path,
        }
    }

    /// Create from environment variables
    ///
    /// Reads `VAULT_ADDR` and `VAULT_TOKEN` from environment.
    pub async fn from_env(mount_path: &str) -> Result<Self> {
        let addr = std::env::var("VAULT_ADDR")
            .map_err(|_| Mecha10Error::Configuration("VAULT_ADDR environment variable not set".to_string()))?;

        let token = std::env::var("VAULT_TOKEN")
            .map_err(|_| Mecha10Error::Configuration("VAULT_TOKEN environment variable not set".to_string()))?;

        Ok(Self::new(addr, token, mount_path.to_string()))
    }

    // duplicate: false positive — see the note on `RedisBackend::make_key` in secrets/redis.rs.
    fn make_path(&self, key: &str) -> String {
        format!("/v1/{}/data/{}", self.mount_path, key)
    }

    /// Returns an error if `response`'s status isn't a success, so `get`/`set` don't each
    /// repeat the same status check after sending their request.
    fn check_status(response: &reqwest::Response) -> Result<()> {
        if !response.status().is_success() {
            return Err(Mecha10Error::Other(format!(
                "Vault returned error status: {}",
                response.status()
            )));
        }

        Ok(())
    }
}

#[async_trait::async_trait]
impl SecretBackend for VaultBackend {
    async fn get(&self, key: &str) -> Result<String> {
        let url = format!("{}{}", self.addr, self.make_path(key));

        let response = self
            .client
            .get(&url)
            .header("X-Vault-Token", &self.token)
            .send()
            .await
            .map_err(|e| Mecha10Error::Other(format!("Failed to connect to Vault: {}", e)))?;

        Self::check_status(&response)?;

        let data: serde_json::Value = response
            .json()
            .await
            .map_err(|e| Mecha10Error::Other(format!("Failed to parse Vault response: {}", e)))?;

        let secret = data
            .get("data")
            .and_then(|d| d.get("data"))
            .and_then(|d| d.get("value"))
            .and_then(|v| v.as_str())
            .ok_or_else(|| Mecha10Error::Other(format!("Secret '{}' not found in Vault or invalid format", key)))?;

        Ok(secret.to_string())
    }

    async fn set(&self, key: &str, value: &str) -> Result<()> {
        let url = format!("{}{}", self.addr, self.make_path(key));

        let payload = serde_json::json!({
            "data": {
                "value": value
            }
        });

        let response = self
            .client
            .post(&url)
            .header("X-Vault-Token", &self.token)
            .json(&payload)
            .send()
            .await
            .map_err(|e| Mecha10Error::Other(format!("Failed to connect to Vault: {}", e)))?;

        Self::check_status(&response)?;

        Ok(())
    }
}