llm_shield_cloud/
secrets.rs1use 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#[derive(Clone, Debug)]
17pub struct SecretValue {
18 data: Vec<u8>,
19}
20
21impl SecretValue {
22 pub fn from_string(s: String) -> Self {
24 Self {
25 data: s.into_bytes(),
26 }
27 }
28
29 pub fn from_bytes(data: Vec<u8>) -> Self {
31 Self { data }
32 }
33
34 pub fn as_string(&self) -> &str {
40 std::str::from_utf8(&self.data).expect("Secret value is not valid UTF-8")
41 }
42
43 pub fn as_bytes(&self) -> &[u8] {
45 &self.data
46 }
47
48 pub fn len(&self) -> usize {
50 self.data.len()
51 }
52
53 pub fn is_empty(&self) -> bool {
55 self.data.is_empty()
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct SecretMetadata {
62 pub name: String,
64
65 pub created_at: chrono::DateTime<chrono::Utc>,
67
68 pub updated_at: chrono::DateTime<chrono::Utc>,
70
71 pub tags: HashMap<String, String>,
73
74 pub version: Option<String>,
76}
77
78#[async_trait]
83pub trait CloudSecretManager: Send + Sync {
84 async fn get_secret(&self, name: &str) -> Result<SecretValue>;
99
100 async fn list_secrets(&self) -> Result<Vec<String>>;
110
111 async fn create_secret(&self, name: &str, value: &SecretValue) -> Result<()>;
122
123 async fn update_secret(&self, name: &str, value: &SecretValue) -> Result<()>;
134
135 async fn delete_secret(&self, name: &str) -> Result<()>;
145
146 async fn rotate_secret(&self, name: &str, new_value: &SecretValue) -> Result<()> {
159 self.update_secret(name, new_value).await
160 }
161
162 async fn get_secret_metadata(&self, name: &str) -> Result<SecretMetadata> {
175 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#[derive(Clone, Debug)]
190struct CachedSecret {
191 value: SecretValue,
192 cached_at: Instant,
193 ttl: Duration,
194}
195
196impl CachedSecret {
197 fn is_expired(&self) -> bool {
199 self.cached_at.elapsed() > self.ttl
200 }
201}
202
203pub struct SecretCache {
207 cache: Arc<RwLock<HashMap<String, CachedSecret>>>,
208 default_ttl: Duration,
209}
210
211impl SecretCache {
212 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 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 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 pub async fn invalidate(&self, key: &str) {
270 let mut cache = self.cache.write().await;
271 cache.remove(key);
272 }
273
274 pub async fn clear(&self) {
276 let mut cache = self.cache.write().await;
277 cache.clear();
278 }
279
280 pub async fn len(&self) -> usize {
282 let cache = self.cache.read().await;
283 cache.len()
284 }
285
286 pub async fn is_empty(&self) -> bool {
288 let cache = self.cache.read().await;
289 cache.is_empty()
290 }
291
292 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 assert!(cache.is_empty().await);
333 assert_eq!(cache.len().await, 0);
334
335 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 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); let secret = SecretValue::from_string("expires-soon".to_string());
352
353 cache.set("expiring-key".to_string(), secret).await;
354
355 assert!(cache.get("expiring-key").await.is_some());
357
358 tokio::time::sleep(Duration::from_secs(2)).await;
360
361 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); cache.set("short".to_string(), SecretValue::from_string("expires".to_string())).await;
385
386 tokio::time::sleep(Duration::from_secs(2)).await;
388
389 cache.set("fresh".to_string(), SecretValue::from_string("current".to_string())).await;
391
392 assert_eq!(cache.len().await, 2);
394
395 cache.cleanup_expired().await;
397
398 assert_eq!(cache.len().await, 1);
400 assert!(cache.get("fresh").await.is_some());
401 assert!(cache.get("short").await.is_none());
402 }
403}