use async_trait::async_trait;
use std::env;
use crate::core::traits::secret_manager::{
ListSecretsOptions, ListSecretsResult, SecretError, SecretManager, SecretMetadata, SecretResult,
};
#[derive(Debug, Clone, Default)]
pub struct EnvSecretManager {
prefix: Option<String>,
}
impl EnvSecretManager {
pub fn new() -> Self {
Self { prefix: None }
}
pub fn with_prefix(prefix: impl Into<String>) -> Self {
Self {
prefix: Some(prefix.into()),
}
}
fn get_env_name(&self, name: &str) -> String {
match &self.prefix {
Some(prefix) => format!("{}{}", prefix, name),
None => name.to_string(),
}
}
}
#[async_trait]
impl SecretManager for EnvSecretManager {
fn name(&self) -> &'static str {
"env"
}
async fn read_secret(&self, name: &str) -> SecretResult<Option<String>> {
let env_name = self.get_env_name(name);
match env::var(&env_name) {
Ok(value) => Ok(Some(value)),
Err(env::VarError::NotPresent) => Ok(None),
Err(env::VarError::NotUnicode(_)) => Err(SecretError::invalid_format(format!(
"Environment variable {} contains invalid UTF-8",
env_name
))),
}
}
async fn write_secret(&self, _name: &str, _value: &str) -> SecretResult<()> {
Err(SecretError::other(
"EnvSecretManager does not support write at runtime; use a persistent secret backend",
))
}
async fn delete_secret(&self, _name: &str) -> SecretResult<()> {
Err(SecretError::other(
"EnvSecretManager does not support delete at runtime; use a persistent secret backend",
))
}
async fn list_secrets(&self, options: &ListSecretsOptions) -> SecretResult<ListSecretsResult> {
let mut secrets = Vec::new();
for (key, _) in env::vars() {
let matches_manager_prefix = match &self.prefix {
Some(prefix) => key.starts_with(prefix),
None => true,
};
if !matches_manager_prefix {
continue;
}
let matches_filter_prefix = match &options.prefix {
Some(filter_prefix) => {
let key_without_manager_prefix = match &self.prefix {
Some(prefix) => key.strip_prefix(prefix).unwrap_or(&key),
None => &key,
};
key_without_manager_prefix.starts_with(filter_prefix)
}
None => true,
};
if !matches_filter_prefix {
continue;
}
let secret_name = match &self.prefix {
Some(prefix) => key.strip_prefix(prefix).unwrap_or(&key).to_string(),
None => key,
};
secrets.push(SecretMetadata::new(secret_name));
if let Some(max) = options.max_results
&& secrets.len() >= max
{
break;
}
}
Ok(ListSecretsResult {
secrets,
next_token: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
unsafe fn set_test_env(key: &str, value: &str) {
unsafe { env::set_var(key, value) };
}
unsafe fn remove_test_env(key: &str) {
unsafe { env::remove_var(key) };
}
#[tokio::test]
async fn test_read_existing_secret() {
let manager = EnvSecretManager::new();
unsafe { set_test_env("TEST_SECRET_READ", "test_value") };
let result = manager.read_secret("TEST_SECRET_READ").await.unwrap();
assert_eq!(result, Some("test_value".to_string()));
unsafe { remove_test_env("TEST_SECRET_READ") };
}
#[tokio::test]
async fn test_read_nonexistent_secret() {
let manager = EnvSecretManager::new();
let result = manager
.read_secret("NONEXISTENT_SECRET_12345")
.await
.unwrap();
assert_eq!(result, None);
}
#[tokio::test]
async fn test_write_secret_returns_error() {
let manager = EnvSecretManager::new();
let result = manager
.write_secret("TEST_SECRET_WRITE", "written_value")
.await;
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("does not support write"));
}
#[tokio::test]
async fn test_delete_secret_returns_error() {
let manager = EnvSecretManager::new();
let result = manager.delete_secret("TEST_SECRET_DELETE").await;
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("does not support delete"));
}
#[tokio::test]
async fn test_with_prefix() {
let manager = EnvSecretManager::with_prefix("LITELLM_");
unsafe { set_test_env("LITELLM_API_KEY", "prefixed_value") };
let result = manager.read_secret("API_KEY").await.unwrap();
assert_eq!(result, Some("prefixed_value".to_string()));
unsafe { remove_test_env("LITELLM_API_KEY") };
}
#[tokio::test]
async fn test_exists() {
let manager = EnvSecretManager::new();
unsafe { set_test_env("TEST_SECRET_EXISTS", "exists") };
assert!(manager.exists("TEST_SECRET_EXISTS").await.unwrap());
assert!(!manager.exists("NONEXISTENT_SECRET_67890").await.unwrap());
unsafe { remove_test_env("TEST_SECRET_EXISTS") };
}
#[tokio::test]
async fn test_list_secrets_with_prefix() {
let manager = EnvSecretManager::with_prefix("TEST_LIST_");
unsafe {
set_test_env("TEST_LIST_SECRET1", "value1");
set_test_env("TEST_LIST_SECRET2", "value2");
}
let result = manager
.list_secrets(&ListSecretsOptions::new())
.await
.unwrap();
assert!(result.secrets.len() >= 2);
let names: Vec<_> = result.secrets.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"SECRET1"));
assert!(names.contains(&"SECRET2"));
unsafe {
remove_test_env("TEST_LIST_SECRET1");
remove_test_env("TEST_LIST_SECRET2");
}
}
#[tokio::test]
async fn test_name() {
let manager = EnvSecretManager::new();
assert_eq!(manager.name(), "env");
}
}