skill_context/providers/
mod.rs

1//! Secret provider implementations.
2//!
3//! This module provides a pluggable secret provider system with multiple
4//! backends for storing and retrieving secrets.
5//!
6//! # Available Providers
7//!
8//! - [`KeychainProvider`]: Platform-native keychain (default)
9//! - [`EnvironmentProvider`]: Environment variables (useful for CI/CD)
10//! - [`FileProvider`]: File-based secrets
11//!
12//! # Example
13//!
14//! ```rust,no_run
15//! use skill_context::providers::{SecretProvider, KeychainProvider};
16//!
17//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
18//! let provider = KeychainProvider::new();
19//!
20//! // Store a secret
21//! provider.set_secret("my-context", "api-key", "secret-value").await?;
22//!
23//! // Retrieve a secret
24//! if let Some(secret) = provider.get_secret("my-context", "api-key").await? {
25//!     println!("Got secret (length: {})", secret.len());
26//! }
27//! # Ok(())
28//! # }
29//! ```
30
31pub 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
50/// A secret value that is automatically zeroed when dropped.
51pub type SecretValue = Zeroizing<String>;
52
53/// Trait for secret providers.
54///
55/// All secret values are wrapped in `Zeroizing<String>` to ensure
56/// they are cleared from memory when no longer needed.
57#[async_trait]
58pub trait SecretProvider: Send + Sync {
59    /// Get a secret value.
60    ///
61    /// Returns `None` if the secret doesn't exist.
62    async fn get_secret(
63        &self,
64        context_id: &str,
65        key: &str,
66    ) -> Result<Option<SecretValue>, ContextError>;
67
68    /// Set a secret value.
69    async fn set_secret(
70        &self,
71        context_id: &str,
72        key: &str,
73        value: &str,
74    ) -> Result<(), ContextError>;
75
76    /// Delete a secret.
77    async fn delete_secret(&self, context_id: &str, key: &str) -> Result<(), ContextError>;
78
79    /// Check if a secret exists.
80    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    /// List all secret keys for a context.
85    async fn list_keys(&self, context_id: &str) -> Result<Vec<String>, ContextError>;
86
87    /// Get the provider name.
88    fn name(&self) -> &'static str;
89
90    /// Check if this provider is read-only.
91    fn is_read_only(&self) -> bool {
92        false
93    }
94}
95
96/// Manager for routing secret operations to the appropriate provider.
97pub struct SecretManager {
98    /// Available providers by name.
99    providers: HashMap<String, Arc<dyn SecretProvider>>,
100    /// Default provider name.
101    default_provider: String,
102    /// Secret cache.
103    cache: Arc<RwLock<SecretCache>>,
104    /// Cache TTL.
105    cache_ttl: Duration,
106}
107
108impl SecretManager {
109    /// Create a new secret manager with the keychain as the default provider.
110    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), // 5 minutes default
119        }
120    }
121
122    /// Add a provider.
123    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    /// Set the default provider.
129    pub fn with_default_provider(mut self, name: impl Into<String>) -> Self {
130        self.default_provider = name.into();
131        self
132    }
133
134    /// Set the cache TTL.
135    pub fn with_cache_ttl(mut self, ttl: Duration) -> Self {
136        self.cache_ttl = ttl;
137        self
138    }
139
140    /// Disable caching.
141    pub fn without_cache(mut self) -> Self {
142        self.cache_ttl = Duration::ZERO;
143        self
144    }
145
146    /// Add providers from configuration.
147    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                    // External providers require feature flags
168                    tracing::warn!("External secret providers not yet implemented");
169                }
170            }
171        }
172        self
173    }
174
175    /// Get a secret using the appropriate provider.
176    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        // Check cache first
184        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        // Get from provider
197        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        // Update cache
220        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    /// Set a secret using the appropriate provider.
231    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        // Invalidate cache
267        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    /// Delete a secret using the appropriate provider.
277    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        // Invalidate cache
312        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    /// Check if all required secrets for a context are set.
322    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    /// Clear the secret cache.
342    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
354/// Internal cache for secret values.
355struct 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        // Should hit cache
417        let result = cache.get("key1", Duration::from_secs(60));
418        assert!(result.is_some());
419        assert_eq!(&*result.unwrap(), "value1");
420
421        // Should miss for nonexistent key
422        let result = cache.get("key2", Duration::from_secs(60));
423        assert!(result.is_none());
424
425        // Invalidate
426        cache.invalidate("key1");
427        let result = cache.get("key1", Duration::from_secs(60));
428        assert!(result.is_none());
429    }
430}