use crate::error::{CloudError, Result};
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
#[derive(Clone, Debug)]
pub struct SecretValue {
data: Vec<u8>,
}
impl SecretValue {
pub fn from_string(s: String) -> Self {
Self {
data: s.into_bytes(),
}
}
pub fn from_bytes(data: Vec<u8>) -> Self {
Self { data }
}
pub fn as_string(&self) -> &str {
std::str::from_utf8(&self.data).expect("Secret value is not valid UTF-8")
}
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct SecretMetadata {
pub name: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub tags: HashMap<String, String>,
pub version: Option<String>,
}
#[async_trait]
pub trait CloudSecretManager: Send + Sync {
async fn get_secret(&self, name: &str) -> Result<SecretValue>;
async fn list_secrets(&self) -> Result<Vec<String>>;
async fn create_secret(&self, name: &str, value: &SecretValue) -> Result<()>;
async fn update_secret(&self, name: &str, value: &SecretValue) -> Result<()>;
async fn delete_secret(&self, name: &str) -> Result<()>;
async fn rotate_secret(&self, name: &str, new_value: &SecretValue) -> Result<()> {
self.update_secret(name, new_value).await
}
async fn get_secret_metadata(&self, name: &str) -> Result<SecretMetadata> {
let _ = self.get_secret(name).await?;
Ok(SecretMetadata {
name: name.to_string(),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
tags: HashMap::new(),
version: None,
})
}
}
#[derive(Clone, Debug)]
struct CachedSecret {
value: SecretValue,
cached_at: Instant,
ttl: Duration,
}
impl CachedSecret {
fn is_expired(&self) -> bool {
self.cached_at.elapsed() > self.ttl
}
}
pub struct SecretCache {
cache: Arc<RwLock<HashMap<String, CachedSecret>>>,
default_ttl: Duration,
}
impl SecretCache {
pub fn new(ttl_seconds: u64) -> Self {
Self {
cache: Arc::new(RwLock::new(HashMap::new())),
default_ttl: Duration::from_secs(ttl_seconds),
}
}
pub async fn get(&self, key: &str) -> Option<SecretValue> {
let cache = self.cache.read().await;
if let Some(cached) = cache.get(key) {
if !cached.is_expired() {
return Some(cached.value.clone());
}
}
None
}
pub async fn set(&self, key: String, value: SecretValue) {
let mut cache = self.cache.write().await;
cache.insert(
key,
CachedSecret {
value,
cached_at: Instant::now(),
ttl: self.default_ttl,
},
);
}
pub async fn invalidate(&self, key: &str) {
let mut cache = self.cache.write().await;
cache.remove(key);
}
pub async fn clear(&self) {
let mut cache = self.cache.write().await;
cache.clear();
}
pub async fn len(&self) -> usize {
let cache = self.cache.read().await;
cache.len()
}
pub async fn is_empty(&self) -> bool {
let cache = self.cache.read().await;
cache.is_empty()
}
pub async fn cleanup_expired(&self) {
let mut cache = self.cache.write().await;
cache.retain(|_, cached| !cached.is_expired());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_secret_value_from_string() {
let secret = SecretValue::from_string("test-secret".to_string());
assert_eq!(secret.as_string(), "test-secret");
assert_eq!(secret.len(), 11);
assert!(!secret.is_empty());
}
#[test]
fn test_secret_value_from_bytes() {
let data = vec![1, 2, 3, 4];
let secret = SecretValue::from_bytes(data.clone());
assert_eq!(secret.as_bytes(), &data[..]);
assert_eq!(secret.len(), 4);
}
#[test]
fn test_secret_value_empty() {
let secret = SecretValue::from_bytes(vec![]);
assert!(secret.is_empty());
assert_eq!(secret.len(), 0);
}
#[tokio::test]
async fn test_secret_cache_basic() {
let cache = SecretCache::new(300);
let secret = SecretValue::from_string("cached-value".to_string());
assert!(cache.is_empty().await);
assert_eq!(cache.len().await, 0);
cache.set("test-key".to_string(), secret.clone()).await;
assert_eq!(cache.len().await, 1);
let retrieved = cache.get("test-key").await;
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().as_string(), "cached-value");
cache.invalidate("test-key").await;
assert!(cache.get("test-key").await.is_none());
}
#[tokio::test]
async fn test_secret_cache_expiration() {
let cache = SecretCache::new(1); let secret = SecretValue::from_string("expires-soon".to_string());
cache.set("expiring-key".to_string(), secret).await;
assert!(cache.get("expiring-key").await.is_some());
tokio::time::sleep(Duration::from_secs(2)).await;
assert!(cache.get("expiring-key").await.is_none());
}
#[tokio::test]
async fn test_secret_cache_clear() {
let cache = SecretCache::new(300);
cache.set("key1".to_string(), SecretValue::from_string("val1".to_string())).await;
cache.set("key2".to_string(), SecretValue::from_string("val2".to_string())).await;
assert_eq!(cache.len().await, 2);
cache.clear().await;
assert_eq!(cache.len().await, 0);
assert!(cache.is_empty().await);
}
#[tokio::test]
async fn test_secret_cache_cleanup_expired() {
let cache = SecretCache::new(1);
cache.set("short".to_string(), SecretValue::from_string("expires".to_string())).await;
tokio::time::sleep(Duration::from_secs(2)).await;
cache.set("fresh".to_string(), SecretValue::from_string("current".to_string())).await;
assert_eq!(cache.len().await, 2);
cache.cleanup_expired().await;
assert_eq!(cache.len().await, 1);
assert!(cache.get("fresh").await.is_some());
assert!(cache.get("short").await.is_none());
}
}