mecha10_core/secrets/
mod.rs

1//! Secrets Management
2//!
3//! Secure storage and retrieval of sensitive configuration data like API keys,
4//! passwords, and credentials.
5//!
6//! # Features
7//!
8//! - Multiple backend support (Environment, Vault, Redis)
9//! - Type-safe secret retrieval
10//! - Secret caching with TTL
11//! - Automatic rotation support
12//! - Integration with existing config system
13//!
14//! # Example
15//!
16//! ```rust
17//! use mecha10::prelude::*;
18//!
19//! # async fn example() -> Result<()> {
20//! // Initialize secrets manager
21//! let secrets = SecretsManager::from_env().await?;
22//!
23//! // Retrieve a secret
24//! let api_key = secrets.get("api_key").await?;
25//!
26//! // Type-safe retrieval with parsing
27//! let port: u16 = secrets.get_as("database_port").await?;
28//! # Ok(())
29//! # }
30//! ```
31
32use crate::{Mecha10Error, Result};
33use serde::{de::DeserializeOwned, Serialize};
34use std::collections::HashMap;
35use std::sync::Arc;
36use std::time::{Duration, Instant};
37use tokio::sync::RwLock;
38
39// Module exports
40mod environment;
41mod redis;
42mod vault;
43
44pub use environment::EnvironmentBackend;
45pub use redis::RedisBackend;
46pub use vault::VaultBackend;
47
48// ============================================================================
49// Secret Backend Trait
50// ============================================================================
51
52/// Backend for retrieving secrets
53#[async_trait::async_trait]
54pub trait SecretBackend: Send + Sync {
55    /// Retrieve a secret by key
56    async fn get(&self, key: &str) -> Result<String>;
57
58    /// Store a secret (if backend supports writing)
59    async fn set(&self, key: &str, value: &str) -> Result<()> {
60        let _ = (key, value);
61        Err(Mecha10Error::Other(
62            "This backend does not support writing secrets".to_string(),
63        ))
64    }
65
66    /// Delete a secret (if backend supports deletion)
67    async fn delete(&self, key: &str) -> Result<()> {
68        let _ = key;
69        Err(Mecha10Error::Other(
70            "This backend does not support deleting secrets".to_string(),
71        ))
72    }
73
74    /// List all secret keys (if backend supports listing)
75    async fn list(&self) -> Result<Vec<String>> {
76        Err(Mecha10Error::Other(
77            "This backend does not support listing secrets".to_string(),
78        ))
79    }
80}
81
82// ============================================================================
83// Secrets Configuration
84// ============================================================================
85
86/// Configuration for secrets management
87#[derive(Debug, Clone, Serialize, serde::Deserialize)]
88pub struct SecretsConfig {
89    /// Backend type to use
90    pub backend: String,
91
92    /// Backend-specific configuration
93    #[serde(flatten)]
94    pub config: HashMap<String, serde_json::Value>,
95
96    /// Cache TTL in seconds (default: 300)
97    #[serde(default = "default_cache_ttl")]
98    pub cache_ttl_secs: u64,
99}
100
101fn default_cache_ttl() -> u64 {
102    300 // 5 minutes
103}
104
105impl Default for SecretsConfig {
106    fn default() -> Self {
107        Self {
108            backend: "environment".to_string(),
109            config: HashMap::new(),
110            cache_ttl_secs: default_cache_ttl(),
111        }
112    }
113}
114
115// ============================================================================
116// Secrets Manager
117// ============================================================================
118
119/// Entry in the secret cache
120struct CacheEntry {
121    value: String,
122    expires_at: Instant,
123}
124
125/// Main secrets manager that handles caching and backend abstraction
126pub struct SecretsManager {
127    backend: Arc<dyn SecretBackend>,
128    cache: Arc<RwLock<HashMap<String, CacheEntry>>>,
129    cache_ttl: Duration,
130}
131
132impl SecretsManager {
133    /// Create a new secrets manager with a specific backend
134    pub fn new(backend: Arc<dyn SecretBackend>, cache_ttl: Duration) -> Self {
135        Self {
136            backend,
137            cache: Arc::new(RwLock::new(HashMap::new())),
138            cache_ttl,
139        }
140    }
141
142    /// Create from configuration
143    pub async fn from_config(config: SecretsConfig) -> Result<Self> {
144        let backend: Arc<dyn SecretBackend> = match config.backend.as_str() {
145            "environment" => {
146                let prefix = config
147                    .config
148                    .get("prefix")
149                    .and_then(|v| v.as_str())
150                    .unwrap_or("MECHA10_SECRET");
151                Arc::new(EnvironmentBackend::new(prefix))
152            }
153            "vault" => {
154                let mount_path = config
155                    .config
156                    .get("mount_path")
157                    .and_then(|v| v.as_str())
158                    .ok_or_else(|| {
159                        Mecha10Error::Configuration("Vault backend requires 'mount_path' config".to_string())
160                    })?;
161                Arc::new(VaultBackend::from_env(mount_path).await?)
162            }
163            "redis" => {
164                let prefix = config
165                    .config
166                    .get("prefix")
167                    .and_then(|v| v.as_str())
168                    .unwrap_or("mecha10:secrets");
169                Arc::new(RedisBackend::from_env(prefix).await?)
170            }
171            _ => {
172                return Err(Mecha10Error::Configuration(format!(
173                    "Unknown secrets backend: {}",
174                    config.backend
175                )));
176            }
177        };
178
179        Ok(Self::new(backend, Duration::from_secs(config.cache_ttl_secs)))
180    }
181
182    /// Create from environment variables
183    ///
184    /// Uses environment backend by default.
185    pub async fn from_env() -> Result<Self> {
186        Ok(Self::new(
187            Arc::new(EnvironmentBackend::default()),
188            Duration::from_secs(300),
189        ))
190    }
191
192    /// Get a secret by key
193    ///
194    /// Checks cache first, then falls back to backend.
195    pub async fn get(&self, key: &str) -> Result<String> {
196        // Check cache
197        {
198            let cache = self.cache.read().await;
199            if let Some(entry) = cache.get(key) {
200                if entry.expires_at > Instant::now() {
201                    return Ok(entry.value.clone());
202                }
203            }
204        }
205
206        // Cache miss or expired, fetch from backend
207        let value = self.backend.get(key).await?;
208
209        // Update cache
210        {
211            let mut cache = self.cache.write().await;
212            cache.insert(
213                key.to_string(),
214                CacheEntry {
215                    value: value.clone(),
216                    expires_at: Instant::now() + self.cache_ttl,
217                },
218            );
219        }
220
221        Ok(value)
222    }
223
224    /// Get a secret and parse it to a specific type
225    ///
226    /// # Example
227    ///
228    /// ```rust
229    /// # use mecha10::prelude::*;
230    /// # async fn example(secrets: &SecretsManager) -> Result<()> {
231    /// let port: u16 = secrets.get_as("database_port").await?;
232    /// let timeout: f64 = secrets.get_as("request_timeout").await?;
233    /// # Ok(())
234    /// # }
235    /// ```
236    pub async fn get_as<T>(&self, key: &str) -> Result<T>
237    where
238        T: DeserializeOwned + std::str::FromStr,
239        T::Err: std::fmt::Display,
240    {
241        let value = self.get(key).await?;
242
243        // Try parsing as JSON first (for complex types)
244        if let Ok(parsed) = serde_json::from_str(&value) {
245            return Ok(parsed);
246        }
247
248        // Fall back to string parsing
249        value.parse().map_err(|e| {
250            Mecha10Error::Configuration(format!(
251                "Failed to parse secret '{}' as {}: {}",
252                key,
253                std::any::type_name::<T>(),
254                e
255            ))
256        })
257    }
258
259    /// Set a secret (if backend supports it)
260    pub async fn set(&self, key: &str, value: &str) -> Result<()> {
261        self.backend.set(key, value).await?;
262
263        // Invalidate cache entry
264        let mut cache = self.cache.write().await;
265        cache.remove(key);
266
267        Ok(())
268    }
269
270    /// Delete a secret (if backend supports it)
271    pub async fn delete(&self, key: &str) -> Result<()> {
272        self.backend.delete(key).await?;
273
274        // Invalidate cache entry
275        let mut cache = self.cache.write().await;
276        cache.remove(key);
277
278        Ok(())
279    }
280
281    /// List all secret keys (if backend supports it)
282    pub async fn list(&self) -> Result<Vec<String>> {
283        self.backend.list().await
284    }
285
286    /// Clear the cache
287    pub async fn clear_cache(&self) {
288        let mut cache = self.cache.write().await;
289        cache.clear();
290    }
291
292    /// Invalidate a specific cache entry
293    pub async fn invalidate(&self, key: &str) {
294        let mut cache = self.cache.write().await;
295        cache.remove(key);
296    }
297}