mecha10-core 0.7.0

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

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

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()))
    }

    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(())
    }
}