mecha10-core 0.8.0

Mecha10 core runtime and foundational types
Documentation
//! Secrets Management
//!
//! Secure storage and retrieval of sensitive configuration data like API keys,
//! passwords, and credentials.
//!
//! # Features
//!
//! - Multiple backend support (Environment, Vault, Redis)
//! - Type-safe secret retrieval
//! - Secret caching with TTL
//! - Automatic rotation support
//! - Integration with existing config system
//!
//! # Example
//!
//! ```rust
//! use mecha10_core::prelude::*;
//!
//! # async fn example() -> Result<()> {
//! // Initialize secrets manager
//! let secrets = SecretsManager::from_env().await?;
//!
//! // Retrieve a secret
//! let api_key = secrets.get("api_key").await?;
//!
//! // Type-safe retrieval with parsing
//! let port: u16 = secrets.get_as("database_port").await?;
//! # Ok(())
//! # }
//! ```

use crate::{Mecha10Error, Result};
use serde::{de::DeserializeOwned, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;

mod environment;
mod redis;
mod vault;

pub use environment::EnvironmentBackend;
pub use redis::RedisBackend;
pub use vault::VaultBackend;

#[async_trait::async_trait]
pub trait SecretBackend: Send + Sync {
    /// Retrieve a secret by key
    async fn get(&self, key: &str) -> Result<String>;

    /// Store a secret (if backend supports writing)
    async fn set(&self, key: &str, value: &str) -> Result<()> {
        let _ = (key, value);
        Err(Mecha10Error::Other(
            "This backend does not support writing secrets".to_string(),
        ))
    }

    /// Delete a secret (if backend supports deletion)
    async fn delete(&self, key: &str) -> Result<()> {
        let _ = key;
        Err(Mecha10Error::Other(
            "This backend does not support deleting secrets".to_string(),
        ))
    }

    /// List all secret keys (if backend supports listing)
    async fn list(&self) -> Result<Vec<String>> {
        Err(Mecha10Error::Other(
            "This backend does not support listing secrets".to_string(),
        ))
    }
}

#[derive(Debug, Clone, Serialize, serde::Deserialize)]
pub struct SecretsConfig {
    pub backend: String,

    #[serde(flatten)]
    pub config: HashMap<String, serde_json::Value>,

    #[serde(default = "default_cache_ttl")]
    pub cache_ttl_secs: u64,
}

fn default_cache_ttl() -> u64 {
    300
}

impl Default for SecretsConfig {
    fn default() -> Self {
        Self {
            backend: "environment".to_string(),
            config: HashMap::new(),
            cache_ttl_secs: default_cache_ttl(),
        }
    }
}

struct CacheEntry {
    value: String,
    expires_at: Instant,
}

pub struct SecretsManager {
    backend: Arc<dyn SecretBackend>,
    cache: Arc<RwLock<HashMap<String, CacheEntry>>>,
    cache_ttl: Duration,
}

impl SecretsManager {
    /// Create a new secrets manager with a specific backend
    pub fn new(backend: Arc<dyn SecretBackend>, cache_ttl: Duration) -> Self {
        Self {
            backend,
            cache: Arc::new(RwLock::new(HashMap::new())),
            cache_ttl,
        }
    }

    /// Create from configuration
    pub async fn from_config(config: SecretsConfig) -> Result<Self> {
        let backend: Arc<dyn SecretBackend> = match config.backend.as_str() {
            "environment" => {
                let prefix = config
                    .config
                    .get("prefix")
                    .and_then(|v| v.as_str())
                    .unwrap_or("MECHA10_SECRET");
                Arc::new(EnvironmentBackend::new(prefix))
            }
            "vault" => {
                let mount_path = config
                    .config
                    .get("mount_path")
                    .and_then(|v| v.as_str())
                    .ok_or_else(|| {
                        Mecha10Error::Configuration("Vault backend requires 'mount_path' config".to_string())
                    })?;
                Arc::new(VaultBackend::from_env(mount_path).await?)
            }
            "redis" => {
                let prefix = config
                    .config
                    .get("prefix")
                    .and_then(|v| v.as_str())
                    .unwrap_or("mecha10:secrets");
                Arc::new(RedisBackend::from_env(prefix).await?)
            }
            _ => {
                return Err(Mecha10Error::Configuration(format!(
                    "Unknown secrets backend: {}",
                    config.backend
                )));
            }
        };

        Ok(Self::new(backend, Duration::from_secs(config.cache_ttl_secs)))
    }

    /// Create from environment variables
    ///
    /// Uses environment backend by default.
    pub async fn from_env() -> Result<Self> {
        Ok(Self::new(
            Arc::new(EnvironmentBackend::default()),
            Duration::from_secs(300),
        ))
    }

    /// Get a secret by key
    ///
    /// Checks cache first, then falls back to backend.
    pub async fn get(&self, key: &str) -> Result<String> {
        {
            let cache = self.cache.read().await;
            if let Some(entry) = cache.get(key) {
                if entry.expires_at > Instant::now() {
                    return Ok(entry.value.clone());
                }
            }
        }

        let value = self.backend.get(key).await?;

        {
            let mut cache = self.cache.write().await;
            cache.insert(
                key.to_string(),
                CacheEntry {
                    value: value.clone(),
                    expires_at: Instant::now() + self.cache_ttl,
                },
            );
        }

        Ok(value)
    }

    /// Get a secret and parse it to a specific type
    ///
    /// # Example
    ///
    /// ```rust
    /// # use mecha10_core::prelude::*;
    /// # async fn example(secrets: &SecretsManager) -> Result<()> {
    /// let port: u16 = secrets.get_as("database_port").await?;
    /// let timeout: f64 = secrets.get_as("request_timeout").await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn get_as<T>(&self, key: &str) -> Result<T>
    where
        T: DeserializeOwned + std::str::FromStr,
        T::Err: std::fmt::Display,
    {
        let value = self.get(key).await?;

        if let Ok(parsed) = serde_json::from_str(&value) {
            return Ok(parsed);
        }

        value.parse().map_err(|e| {
            Mecha10Error::Configuration(format!(
                "Failed to parse secret '{}' as {}: {}",
                key,
                std::any::type_name::<T>(),
                e
            ))
        })
    }

    /// Set a secret (if backend supports it)
    pub async fn set(&self, key: &str, value: &str) -> Result<()> {
        self.backend.set(key, value).await?;

        let mut cache = self.cache.write().await;
        cache.remove(key);

        Ok(())
    }

    /// Delete a secret (if backend supports it)
    pub async fn delete(&self, key: &str) -> Result<()> {
        self.backend.delete(key).await?;

        let mut cache = self.cache.write().await;
        cache.remove(key);

        Ok(())
    }

    /// List all secret keys (if backend supports it)
    pub async fn list(&self) -> Result<Vec<String>> {
        self.backend.list().await
    }

    /// Clear the cache
    pub async fn clear_cache(&self) {
        let mut cache = self.cache.write().await;
        cache.clear();
    }
}