pub mod env;
pub mod file;
pub mod keychain;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use async_trait::async_trait;
use tokio::sync::RwLock;
use zeroize::Zeroizing;
use crate::secrets::{SecretDefinition, SecretProviderConfig};
use crate::ContextError;
pub use env::EnvironmentProvider;
pub use file::FileProvider;
pub use keychain::KeychainProvider;
pub type SecretValue = Zeroizing<String>;
#[async_trait]
pub trait SecretProvider: Send + Sync {
async fn get_secret(
&self,
context_id: &str,
key: &str,
) -> Result<Option<SecretValue>, ContextError>;
async fn set_secret(
&self,
context_id: &str,
key: &str,
value: &str,
) -> Result<(), ContextError>;
async fn delete_secret(&self, context_id: &str, key: &str) -> Result<(), ContextError>;
async fn has_secret(&self, context_id: &str, key: &str) -> Result<bool, ContextError> {
Ok(self.get_secret(context_id, key).await?.is_some())
}
async fn list_keys(&self, context_id: &str) -> Result<Vec<String>, ContextError>;
fn name(&self) -> &'static str;
fn is_read_only(&self) -> bool {
false
}
}
pub struct SecretManager {
providers: HashMap<String, Arc<dyn SecretProvider>>,
default_provider: String,
cache: Arc<RwLock<SecretCache>>,
cache_ttl: Duration,
}
impl SecretManager {
pub fn new() -> Self {
let mut providers: HashMap<String, Arc<dyn SecretProvider>> = HashMap::new();
providers.insert("keychain".to_string(), Arc::new(KeychainProvider::new()));
Self {
providers,
default_provider: "keychain".to_string(),
cache: Arc::new(RwLock::new(SecretCache::new())),
cache_ttl: Duration::from_secs(300), }
}
pub fn with_provider(mut self, name: impl Into<String>, provider: Arc<dyn SecretProvider>) -> Self {
self.providers.insert(name.into(), provider);
self
}
pub fn with_default_provider(mut self, name: impl Into<String>) -> Self {
self.default_provider = name.into();
self
}
pub fn with_cache_ttl(mut self, ttl: Duration) -> Self {
self.cache_ttl = ttl;
self
}
pub fn without_cache(mut self) -> Self {
self.cache_ttl = Duration::ZERO;
self
}
pub fn with_provider_configs(mut self, configs: &[SecretProviderConfig]) -> Self {
for config in configs {
match config {
SecretProviderConfig::Keychain => {
self.providers
.insert("keychain".to_string(), Arc::new(KeychainProvider::new()));
}
SecretProviderConfig::EnvironmentVariable { prefix } => {
self.providers.insert(
"environment".to_string(),
Arc::new(EnvironmentProvider::new(prefix)),
);
}
SecretProviderConfig::File { path, format } => {
if let Ok(provider) = FileProvider::new(path, format.clone()) {
self.providers
.insert("file".to_string(), Arc::new(provider));
}
}
SecretProviderConfig::External { .. } => {
tracing::warn!("External secret providers not yet implemented");
}
}
}
self
}
pub async fn get_secret(
&self,
context_id: &str,
definition: &SecretDefinition,
) -> Result<Option<SecretValue>, ContextError> {
let cache_key = format!("{}:{}", context_id, definition.key);
if !self.cache_ttl.is_zero() {
let cache = self.cache.read().await;
if let Some(cached) = cache.get(&cache_key, self.cache_ttl) {
tracing::debug!(
context_id = context_id,
key = definition.key,
"Secret cache hit"
);
return Ok(Some(cached));
}
}
let provider_name = definition
.provider
.as_deref()
.unwrap_or(&self.default_provider);
let provider = self
.providers
.get(provider_name)
.ok_or_else(|| ContextError::SecretProvider(format!(
"Provider '{}' not configured",
provider_name
)))?;
tracing::debug!(
context_id = context_id,
key = definition.key,
provider = provider_name,
"Fetching secret from provider"
);
let secret = provider.get_secret(context_id, &definition.key).await?;
if !self.cache_ttl.is_zero() {
if let Some(ref value) = secret {
let mut cache = self.cache.write().await;
cache.set(cache_key, value.clone());
}
}
Ok(secret)
}
pub async fn set_secret(
&self,
context_id: &str,
definition: &SecretDefinition,
value: &str,
) -> Result<(), ContextError> {
let provider_name = definition
.provider
.as_deref()
.unwrap_or(&self.default_provider);
let provider = self
.providers
.get(provider_name)
.ok_or_else(|| ContextError::SecretProvider(format!(
"Provider '{}' not configured",
provider_name
)))?;
if provider.is_read_only() {
return Err(ContextError::SecretProvider(format!(
"Provider '{}' is read-only",
provider_name
)));
}
tracing::info!(
context_id = context_id,
key = definition.key,
provider = provider_name,
"Setting secret"
);
provider.set_secret(context_id, &definition.key, value).await?;
if !self.cache_ttl.is_zero() {
let cache_key = format!("{}:{}", context_id, definition.key);
let mut cache = self.cache.write().await;
cache.invalidate(&cache_key);
}
Ok(())
}
pub async fn delete_secret(
&self,
context_id: &str,
definition: &SecretDefinition,
) -> Result<(), ContextError> {
let provider_name = definition
.provider
.as_deref()
.unwrap_or(&self.default_provider);
let provider = self
.providers
.get(provider_name)
.ok_or_else(|| ContextError::SecretProvider(format!(
"Provider '{}' not configured",
provider_name
)))?;
if provider.is_read_only() {
return Err(ContextError::SecretProvider(format!(
"Provider '{}' is read-only",
provider_name
)));
}
tracing::info!(
context_id = context_id,
key = definition.key,
provider = provider_name,
"Deleting secret"
);
provider.delete_secret(context_id, &definition.key).await?;
if !self.cache_ttl.is_zero() {
let cache_key = format!("{}:{}", context_id, definition.key);
let mut cache = self.cache.write().await;
cache.invalidate(&cache_key);
}
Ok(())
}
pub async fn verify_secrets(
&self,
context_id: &str,
definitions: &[(&str, &SecretDefinition)],
) -> Result<Vec<String>, ContextError> {
let mut missing = Vec::new();
for (key, def) in definitions {
if def.required {
let has = self.get_secret(context_id, def).await?.is_some();
if !has {
missing.push(key.to_string());
}
}
}
Ok(missing)
}
pub async fn clear_cache(&self) {
let mut cache = self.cache.write().await;
cache.clear();
}
}
impl Default for SecretManager {
fn default() -> Self {
Self::new()
}
}
struct SecretCache {
entries: HashMap<String, CacheEntry>,
}
struct CacheEntry {
value: SecretValue,
cached_at: Instant,
}
impl SecretCache {
fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
fn get(&self, key: &str, ttl: Duration) -> Option<SecretValue> {
self.entries.get(key).and_then(|entry| {
if entry.cached_at.elapsed() < ttl {
Some(entry.value.clone())
} else {
None
}
})
}
fn set(&mut self, key: String, value: SecretValue) {
self.entries.insert(
key,
CacheEntry {
value,
cached_at: Instant::now(),
},
);
}
fn invalidate(&mut self, key: &str) {
self.entries.remove(key);
}
fn clear(&mut self) {
self.entries.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_secret_manager_default() {
let manager = SecretManager::new();
assert!(manager.providers.contains_key("keychain"));
}
#[tokio::test]
async fn test_cache_operations() {
let mut cache = SecretCache::new();
cache.set("key1".to_string(), Zeroizing::new("value1".to_string()));
let result = cache.get("key1", Duration::from_secs(60));
assert!(result.is_some());
assert_eq!(&*result.unwrap(), "value1");
let result = cache.get("key2", Duration::from_secs(60));
assert!(result.is_none());
cache.invalidate("key1");
let result = cache.get("key1", Duration::from_secs(60));
assert!(result.is_none());
}
}