mecha10_core/secrets/
mod.rs1use 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
39mod environment;
41mod redis;
42mod vault;
43
44pub use environment::EnvironmentBackend;
45pub use redis::RedisBackend;
46pub use vault::VaultBackend;
47
48#[async_trait::async_trait]
54pub trait SecretBackend: Send + Sync {
55 async fn get(&self, key: &str) -> Result<String>;
57
58 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 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 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#[derive(Debug, Clone, Serialize, serde::Deserialize)]
88pub struct SecretsConfig {
89 pub backend: String,
91
92 #[serde(flatten)]
94 pub config: HashMap<String, serde_json::Value>,
95
96 #[serde(default = "default_cache_ttl")]
98 pub cache_ttl_secs: u64,
99}
100
101fn default_cache_ttl() -> u64 {
102 300 }
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
115struct CacheEntry {
121 value: String,
122 expires_at: Instant,
123}
124
125pub struct SecretsManager {
127 backend: Arc<dyn SecretBackend>,
128 cache: Arc<RwLock<HashMap<String, CacheEntry>>>,
129 cache_ttl: Duration,
130}
131
132impl SecretsManager {
133 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 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 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 pub async fn get(&self, key: &str) -> Result<String> {
196 {
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 let value = self.backend.get(key).await?;
208
209 {
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 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 if let Ok(parsed) = serde_json::from_str(&value) {
245 return Ok(parsed);
246 }
247
248 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 pub async fn set(&self, key: &str, value: &str) -> Result<()> {
261 self.backend.set(key, value).await?;
262
263 let mut cache = self.cache.write().await;
265 cache.remove(key);
266
267 Ok(())
268 }
269
270 pub async fn delete(&self, key: &str) -> Result<()> {
272 self.backend.delete(key).await?;
273
274 let mut cache = self.cache.write().await;
276 cache.remove(key);
277
278 Ok(())
279 }
280
281 pub async fn list(&self) -> Result<Vec<String>> {
283 self.backend.list().await
284 }
285
286 pub async fn clear_cache(&self) {
288 let mut cache = self.cache.write().await;
289 cache.clear();
290 }
291
292 pub async fn invalidate(&self, key: &str) {
294 let mut cache = self.cache.write().await;
295 cache.remove(key);
296 }
297}