1pub mod env;
32pub mod file;
33pub mod keychain;
34
35use std::collections::HashMap;
36use std::sync::Arc;
37use std::time::{Duration, Instant};
38
39use async_trait::async_trait;
40use tokio::sync::RwLock;
41use zeroize::Zeroizing;
42
43use crate::secrets::{SecretDefinition, SecretProviderConfig};
44use crate::ContextError;
45
46pub use env::EnvironmentProvider;
47pub use file::FileProvider;
48pub use keychain::KeychainProvider;
49
50pub type SecretValue = Zeroizing<String>;
52
53#[async_trait]
58pub trait SecretProvider: Send + Sync {
59 async fn get_secret(
63 &self,
64 context_id: &str,
65 key: &str,
66 ) -> Result<Option<SecretValue>, ContextError>;
67
68 async fn set_secret(
70 &self,
71 context_id: &str,
72 key: &str,
73 value: &str,
74 ) -> Result<(), ContextError>;
75
76 async fn delete_secret(&self, context_id: &str, key: &str) -> Result<(), ContextError>;
78
79 async fn has_secret(&self, context_id: &str, key: &str) -> Result<bool, ContextError> {
81 Ok(self.get_secret(context_id, key).await?.is_some())
82 }
83
84 async fn list_keys(&self, context_id: &str) -> Result<Vec<String>, ContextError>;
86
87 fn name(&self) -> &'static str;
89
90 fn is_read_only(&self) -> bool {
92 false
93 }
94}
95
96pub struct SecretManager {
98 providers: HashMap<String, Arc<dyn SecretProvider>>,
100 default_provider: String,
102 cache: Arc<RwLock<SecretCache>>,
104 cache_ttl: Duration,
106}
107
108impl SecretManager {
109 pub fn new() -> Self {
111 let mut providers: HashMap<String, Arc<dyn SecretProvider>> = HashMap::new();
112 providers.insert("keychain".to_string(), Arc::new(KeychainProvider::new()));
113
114 Self {
115 providers,
116 default_provider: "keychain".to_string(),
117 cache: Arc::new(RwLock::new(SecretCache::new())),
118 cache_ttl: Duration::from_secs(300), }
120 }
121
122 pub fn with_provider(mut self, name: impl Into<String>, provider: Arc<dyn SecretProvider>) -> Self {
124 self.providers.insert(name.into(), provider);
125 self
126 }
127
128 pub fn with_default_provider(mut self, name: impl Into<String>) -> Self {
130 self.default_provider = name.into();
131 self
132 }
133
134 pub fn with_cache_ttl(mut self, ttl: Duration) -> Self {
136 self.cache_ttl = ttl;
137 self
138 }
139
140 pub fn without_cache(mut self) -> Self {
142 self.cache_ttl = Duration::ZERO;
143 self
144 }
145
146 pub fn with_provider_configs(mut self, configs: &[SecretProviderConfig]) -> Self {
148 for config in configs {
149 match config {
150 SecretProviderConfig::Keychain => {
151 self.providers
152 .insert("keychain".to_string(), Arc::new(KeychainProvider::new()));
153 }
154 SecretProviderConfig::EnvironmentVariable { prefix } => {
155 self.providers.insert(
156 "environment".to_string(),
157 Arc::new(EnvironmentProvider::new(prefix)),
158 );
159 }
160 SecretProviderConfig::File { path, format } => {
161 if let Ok(provider) = FileProvider::new(path, format.clone()) {
162 self.providers
163 .insert("file".to_string(), Arc::new(provider));
164 }
165 }
166 SecretProviderConfig::External { .. } => {
167 tracing::warn!("External secret providers not yet implemented");
169 }
170 }
171 }
172 self
173 }
174
175 pub async fn get_secret(
177 &self,
178 context_id: &str,
179 definition: &SecretDefinition,
180 ) -> Result<Option<SecretValue>, ContextError> {
181 let cache_key = format!("{}:{}", context_id, definition.key);
182
183 if !self.cache_ttl.is_zero() {
185 let cache = self.cache.read().await;
186 if let Some(cached) = cache.get(&cache_key, self.cache_ttl) {
187 tracing::debug!(
188 context_id = context_id,
189 key = definition.key,
190 "Secret cache hit"
191 );
192 return Ok(Some(cached));
193 }
194 }
195
196 let provider_name = definition
198 .provider
199 .as_deref()
200 .unwrap_or(&self.default_provider);
201
202 let provider = self
203 .providers
204 .get(provider_name)
205 .ok_or_else(|| ContextError::SecretProvider(format!(
206 "Provider '{}' not configured",
207 provider_name
208 )))?;
209
210 tracing::debug!(
211 context_id = context_id,
212 key = definition.key,
213 provider = provider_name,
214 "Fetching secret from provider"
215 );
216
217 let secret = provider.get_secret(context_id, &definition.key).await?;
218
219 if !self.cache_ttl.is_zero() {
221 if let Some(ref value) = secret {
222 let mut cache = self.cache.write().await;
223 cache.set(cache_key, value.clone());
224 }
225 }
226
227 Ok(secret)
228 }
229
230 pub async fn set_secret(
232 &self,
233 context_id: &str,
234 definition: &SecretDefinition,
235 value: &str,
236 ) -> Result<(), ContextError> {
237 let provider_name = definition
238 .provider
239 .as_deref()
240 .unwrap_or(&self.default_provider);
241
242 let provider = self
243 .providers
244 .get(provider_name)
245 .ok_or_else(|| ContextError::SecretProvider(format!(
246 "Provider '{}' not configured",
247 provider_name
248 )))?;
249
250 if provider.is_read_only() {
251 return Err(ContextError::SecretProvider(format!(
252 "Provider '{}' is read-only",
253 provider_name
254 )));
255 }
256
257 tracing::info!(
258 context_id = context_id,
259 key = definition.key,
260 provider = provider_name,
261 "Setting secret"
262 );
263
264 provider.set_secret(context_id, &definition.key, value).await?;
265
266 if !self.cache_ttl.is_zero() {
268 let cache_key = format!("{}:{}", context_id, definition.key);
269 let mut cache = self.cache.write().await;
270 cache.invalidate(&cache_key);
271 }
272
273 Ok(())
274 }
275
276 pub async fn delete_secret(
278 &self,
279 context_id: &str,
280 definition: &SecretDefinition,
281 ) -> Result<(), ContextError> {
282 let provider_name = definition
283 .provider
284 .as_deref()
285 .unwrap_or(&self.default_provider);
286
287 let provider = self
288 .providers
289 .get(provider_name)
290 .ok_or_else(|| ContextError::SecretProvider(format!(
291 "Provider '{}' not configured",
292 provider_name
293 )))?;
294
295 if provider.is_read_only() {
296 return Err(ContextError::SecretProvider(format!(
297 "Provider '{}' is read-only",
298 provider_name
299 )));
300 }
301
302 tracing::info!(
303 context_id = context_id,
304 key = definition.key,
305 provider = provider_name,
306 "Deleting secret"
307 );
308
309 provider.delete_secret(context_id, &definition.key).await?;
310
311 if !self.cache_ttl.is_zero() {
313 let cache_key = format!("{}:{}", context_id, definition.key);
314 let mut cache = self.cache.write().await;
315 cache.invalidate(&cache_key);
316 }
317
318 Ok(())
319 }
320
321 pub async fn verify_secrets(
323 &self,
324 context_id: &str,
325 definitions: &[(&str, &SecretDefinition)],
326 ) -> Result<Vec<String>, ContextError> {
327 let mut missing = Vec::new();
328
329 for (key, def) in definitions {
330 if def.required {
331 let has = self.get_secret(context_id, def).await?.is_some();
332 if !has {
333 missing.push(key.to_string());
334 }
335 }
336 }
337
338 Ok(missing)
339 }
340
341 pub async fn clear_cache(&self) {
343 let mut cache = self.cache.write().await;
344 cache.clear();
345 }
346}
347
348impl Default for SecretManager {
349 fn default() -> Self {
350 Self::new()
351 }
352}
353
354struct SecretCache {
356 entries: HashMap<String, CacheEntry>,
357}
358
359struct CacheEntry {
360 value: SecretValue,
361 cached_at: Instant,
362}
363
364impl SecretCache {
365 fn new() -> Self {
366 Self {
367 entries: HashMap::new(),
368 }
369 }
370
371 fn get(&self, key: &str, ttl: Duration) -> Option<SecretValue> {
372 self.entries.get(key).and_then(|entry| {
373 if entry.cached_at.elapsed() < ttl {
374 Some(entry.value.clone())
375 } else {
376 None
377 }
378 })
379 }
380
381 fn set(&mut self, key: String, value: SecretValue) {
382 self.entries.insert(
383 key,
384 CacheEntry {
385 value,
386 cached_at: Instant::now(),
387 },
388 );
389 }
390
391 fn invalidate(&mut self, key: &str) {
392 self.entries.remove(key);
393 }
394
395 fn clear(&mut self) {
396 self.entries.clear();
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 #[tokio::test]
405 async fn test_secret_manager_default() {
406 let manager = SecretManager::new();
407 assert!(manager.providers.contains_key("keychain"));
408 }
409
410 #[tokio::test]
411 async fn test_cache_operations() {
412 let mut cache = SecretCache::new();
413
414 cache.set("key1".to_string(), Zeroizing::new("value1".to_string()));
415
416 let result = cache.get("key1", Duration::from_secs(60));
418 assert!(result.is_some());
419 assert_eq!(&*result.unwrap(), "value1");
420
421 let result = cache.get("key2", Duration::from_secs(60));
423 assert!(result.is_none());
424
425 cache.invalidate("key1");
427 let result = cache.get("key1", Duration::from_secs(60));
428 assert!(result.is_none());
429 }
430}