llm_shield_cloud/
secrets.rs

1//! Secret management abstractions.
2//!
3//! Provides unified trait for secret management across cloud providers:
4//! - AWS Secrets Manager
5//! - GCP Secret Manager
6//! - Azure Key Vault
7
8use crate::error::{CloudError, Result};
9use async_trait::async_trait;
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::time::{Duration, Instant};
13use tokio::sync::RwLock;
14
15/// Represents a secret value that can be stored and retrieved.
16#[derive(Clone, Debug)]
17pub struct SecretValue {
18    data: Vec<u8>,
19}
20
21impl SecretValue {
22    /// Creates a secret value from a string.
23    pub fn from_string(s: String) -> Self {
24        Self {
25            data: s.into_bytes(),
26        }
27    }
28
29    /// Creates a secret value from bytes.
30    pub fn from_bytes(data: Vec<u8>) -> Self {
31        Self { data }
32    }
33
34    /// Returns the secret value as a string.
35    ///
36    /// # Panics
37    ///
38    /// Panics if the data is not valid UTF-8.
39    pub fn as_string(&self) -> &str {
40        std::str::from_utf8(&self.data).expect("Secret value is not valid UTF-8")
41    }
42
43    /// Returns the secret value as bytes.
44    pub fn as_bytes(&self) -> &[u8] {
45        &self.data
46    }
47
48    /// Returns the length of the secret in bytes.
49    pub fn len(&self) -> usize {
50        self.data.len()
51    }
52
53    /// Checks if the secret is empty.
54    pub fn is_empty(&self) -> bool {
55        self.data.is_empty()
56    }
57}
58
59/// Metadata about a secret.
60#[derive(Debug, Clone)]
61pub struct SecretMetadata {
62    /// The name/ID of the secret.
63    pub name: String,
64
65    /// When the secret was created.
66    pub created_at: chrono::DateTime<chrono::Utc>,
67
68    /// When the secret was last updated.
69    pub updated_at: chrono::DateTime<chrono::Utc>,
70
71    /// Optional tags/labels for the secret.
72    pub tags: HashMap<String, String>,
73
74    /// Secret version (if supported by provider).
75    pub version: Option<String>,
76}
77
78/// Unified trait for cloud secret management.
79///
80/// This trait provides a consistent interface for managing secrets across
81/// different cloud providers (AWS, GCP, Azure).
82#[async_trait]
83pub trait CloudSecretManager: Send + Sync {
84    /// Fetches a secret by name.
85    ///
86    /// # Arguments
87    ///
88    /// * `name` - The name/ID of the secret to fetch
89    ///
90    /// # Returns
91    ///
92    /// Returns the secret value if found.
93    ///
94    /// # Errors
95    ///
96    /// Returns `CloudError::SecretNotFound` if the secret doesn't exist.
97    /// Returns `CloudError::SecretFetch` if the fetch operation fails.
98    async fn get_secret(&self, name: &str) -> Result<SecretValue>;
99
100    /// Lists all secret names.
101    ///
102    /// # Returns
103    ///
104    /// Returns a vector of secret names/IDs.
105    ///
106    /// # Errors
107    ///
108    /// Returns `CloudError::SecretList` if the list operation fails.
109    async fn list_secrets(&self) -> Result<Vec<String>>;
110
111    /// Creates a new secret.
112    ///
113    /// # Arguments
114    ///
115    /// * `name` - The name/ID for the new secret
116    /// * `value` - The secret value to store
117    ///
118    /// # Errors
119    ///
120    /// Returns `CloudError::SecretCreate` if the create operation fails.
121    async fn create_secret(&self, name: &str, value: &SecretValue) -> Result<()>;
122
123    /// Updates an existing secret.
124    ///
125    /// # Arguments
126    ///
127    /// * `name` - The name/ID of the secret to update
128    /// * `value` - The new secret value
129    ///
130    /// # Errors
131    ///
132    /// Returns `CloudError::SecretUpdate` if the update operation fails.
133    async fn update_secret(&self, name: &str, value: &SecretValue) -> Result<()>;
134
135    /// Deletes a secret.
136    ///
137    /// # Arguments
138    ///
139    /// * `name` - The name/ID of the secret to delete
140    ///
141    /// # Errors
142    ///
143    /// Returns `CloudError::SecretDelete` if the delete operation fails.
144    async fn delete_secret(&self, name: &str) -> Result<()>;
145
146    /// Rotates a secret (creates a new version).
147    ///
148    /// Default implementation calls `update_secret`.
149    ///
150    /// # Arguments
151    ///
152    /// * `name` - The name/ID of the secret to rotate
153    /// * `new_value` - The new secret value
154    ///
155    /// # Errors
156    ///
157    /// Returns `CloudError::SecretUpdate` if the rotation fails.
158    async fn rotate_secret(&self, name: &str, new_value: &SecretValue) -> Result<()> {
159        self.update_secret(name, new_value).await
160    }
161
162    /// Gets secret metadata without fetching the value.
163    ///
164    /// Default implementation fetches the secret and discards the value.
165    /// Providers should override this for efficiency.
166    ///
167    /// # Arguments
168    ///
169    /// * `name` - The name/ID of the secret
170    ///
171    /// # Errors
172    ///
173    /// Returns `CloudError::SecretFetch` if the operation fails.
174    async fn get_secret_metadata(&self, name: &str) -> Result<SecretMetadata> {
175        // Default implementation - providers should override for efficiency
176        let _ = self.get_secret(name).await?;
177
178        Ok(SecretMetadata {
179            name: name.to_string(),
180            created_at: chrono::Utc::now(),
181            updated_at: chrono::Utc::now(),
182            tags: HashMap::new(),
183            version: None,
184        })
185    }
186}
187
188/// Cached secret with expiration.
189#[derive(Clone, Debug)]
190struct CachedSecret {
191    value: SecretValue,
192    cached_at: Instant,
193    ttl: Duration,
194}
195
196impl CachedSecret {
197    /// Checks if the cached secret has expired.
198    fn is_expired(&self) -> bool {
199        self.cached_at.elapsed() > self.ttl
200    }
201}
202
203/// In-memory cache for secrets with TTL.
204///
205/// This cache reduces the number of API calls to cloud secret managers.
206pub struct SecretCache {
207    cache: Arc<RwLock<HashMap<String, CachedSecret>>>,
208    default_ttl: Duration,
209}
210
211impl SecretCache {
212    /// Creates a new secret cache with the specified TTL.
213    ///
214    /// # Arguments
215    ///
216    /// * `ttl_seconds` - Default time-to-live for cached secrets in seconds
217    pub fn new(ttl_seconds: u64) -> Self {
218        Self {
219            cache: Arc::new(RwLock::new(HashMap::new())),
220            default_ttl: Duration::from_secs(ttl_seconds),
221        }
222    }
223
224    /// Gets a secret from the cache if it exists and hasn't expired.
225    ///
226    /// # Arguments
227    ///
228    /// * `key` - The secret name/key
229    ///
230    /// # Returns
231    ///
232    /// Returns `Some(SecretValue)` if the secret is in cache and not expired,
233    /// otherwise returns `None`.
234    pub async fn get(&self, key: &str) -> Option<SecretValue> {
235        let cache = self.cache.read().await;
236
237        if let Some(cached) = cache.get(key) {
238            if !cached.is_expired() {
239                return Some(cached.value.clone());
240            }
241        }
242
243        None
244    }
245
246    /// Stores a secret in the cache.
247    ///
248    /// # Arguments
249    ///
250    /// * `key` - The secret name/key
251    /// * `value` - The secret value to cache
252    pub async fn set(&self, key: String, value: SecretValue) {
253        let mut cache = self.cache.write().await;
254        cache.insert(
255            key,
256            CachedSecret {
257                value,
258                cached_at: Instant::now(),
259                ttl: self.default_ttl,
260            },
261        );
262    }
263
264    /// Invalidates a specific secret in the cache.
265    ///
266    /// # Arguments
267    ///
268    /// * `key` - The secret name/key to invalidate
269    pub async fn invalidate(&self, key: &str) {
270        let mut cache = self.cache.write().await;
271        cache.remove(key);
272    }
273
274    /// Clears all secrets from the cache.
275    pub async fn clear(&self) {
276        let mut cache = self.cache.write().await;
277        cache.clear();
278    }
279
280    /// Returns the number of secrets currently in the cache.
281    pub async fn len(&self) -> usize {
282        let cache = self.cache.read().await;
283        cache.len()
284    }
285
286    /// Checks if the cache is empty.
287    pub async fn is_empty(&self) -> bool {
288        let cache = self.cache.read().await;
289        cache.is_empty()
290    }
291
292    /// Removes expired secrets from the cache.
293    pub async fn cleanup_expired(&self) {
294        let mut cache = self.cache.write().await;
295        cache.retain(|_, cached| !cached.is_expired());
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn test_secret_value_from_string() {
305        let secret = SecretValue::from_string("test-secret".to_string());
306        assert_eq!(secret.as_string(), "test-secret");
307        assert_eq!(secret.len(), 11);
308        assert!(!secret.is_empty());
309    }
310
311    #[test]
312    fn test_secret_value_from_bytes() {
313        let data = vec![1, 2, 3, 4];
314        let secret = SecretValue::from_bytes(data.clone());
315        assert_eq!(secret.as_bytes(), &data[..]);
316        assert_eq!(secret.len(), 4);
317    }
318
319    #[test]
320    fn test_secret_value_empty() {
321        let secret = SecretValue::from_bytes(vec![]);
322        assert!(secret.is_empty());
323        assert_eq!(secret.len(), 0);
324    }
325
326    #[tokio::test]
327    async fn test_secret_cache_basic() {
328        let cache = SecretCache::new(300);
329        let secret = SecretValue::from_string("cached-value".to_string());
330
331        // Initially empty
332        assert!(cache.is_empty().await);
333        assert_eq!(cache.len().await, 0);
334
335        // Set and get
336        cache.set("test-key".to_string(), secret.clone()).await;
337        assert_eq!(cache.len().await, 1);
338
339        let retrieved = cache.get("test-key").await;
340        assert!(retrieved.is_some());
341        assert_eq!(retrieved.unwrap().as_string(), "cached-value");
342
343        // Invalidate
344        cache.invalidate("test-key").await;
345        assert!(cache.get("test-key").await.is_none());
346    }
347
348    #[tokio::test]
349    async fn test_secret_cache_expiration() {
350        let cache = SecretCache::new(1); // 1 second TTL
351        let secret = SecretValue::from_string("expires-soon".to_string());
352
353        cache.set("expiring-key".to_string(), secret).await;
354
355        // Should be available immediately
356        assert!(cache.get("expiring-key").await.is_some());
357
358        // Wait for expiration
359        tokio::time::sleep(Duration::from_secs(2)).await;
360
361        // Should be expired
362        assert!(cache.get("expiring-key").await.is_none());
363    }
364
365    #[tokio::test]
366    async fn test_secret_cache_clear() {
367        let cache = SecretCache::new(300);
368
369        cache.set("key1".to_string(), SecretValue::from_string("val1".to_string())).await;
370        cache.set("key2".to_string(), SecretValue::from_string("val2".to_string())).await;
371
372        assert_eq!(cache.len().await, 2);
373
374        cache.clear().await;
375
376        assert_eq!(cache.len().await, 0);
377        assert!(cache.is_empty().await);
378    }
379
380    #[tokio::test]
381    async fn test_secret_cache_cleanup_expired() {
382        let cache = SecretCache::new(1); // 1 second TTL
383
384        cache.set("short".to_string(), SecretValue::from_string("expires".to_string())).await;
385
386        // Wait for expiration
387        tokio::time::sleep(Duration::from_secs(2)).await;
388
389        // Add a fresh entry
390        cache.set("fresh".to_string(), SecretValue::from_string("current".to_string())).await;
391
392        // Should have 2 entries (one expired)
393        assert_eq!(cache.len().await, 2);
394
395        // Cleanup expired
396        cache.cleanup_expired().await;
397
398        // Should only have the fresh entry
399        assert_eq!(cache.len().await, 1);
400        assert!(cache.get("fresh").await.is_some());
401        assert!(cache.get("short").await.is_none());
402    }
403}