use async_trait::async_trait;
use zeroize::Zeroizing;
use super::{SecretProvider, SecretValue};
use crate::ContextError;
pub struct EnvironmentProvider {
prefix: String,
}
impl EnvironmentProvider {
pub fn new(prefix: impl Into<String>) -> Self {
Self {
prefix: prefix.into(),
}
}
pub fn without_prefix() -> Self {
Self {
prefix: String::new(),
}
}
fn build_env_var(&self, context_id: &str, key: &str) -> String {
let context_part = context_id.to_uppercase().replace('-', "_").replace('.', "_");
let key_part = key.to_uppercase().replace('-', "_").replace('.', "_");
format!("{}{}__{}", self.prefix, context_part, key_part)
}
}
#[async_trait]
impl SecretProvider for EnvironmentProvider {
async fn get_secret(
&self,
context_id: &str,
key: &str,
) -> Result<Option<SecretValue>, ContextError> {
let env_var = self.build_env_var(context_id, key);
match std::env::var(&env_var) {
Ok(value) => {
tracing::debug!(
context_id = context_id,
key = key,
env_var = env_var,
"Retrieved secret from environment"
);
Ok(Some(Zeroizing::new(value)))
}
Err(std::env::VarError::NotPresent) => Ok(None),
Err(std::env::VarError::NotUnicode(_)) => {
tracing::warn!(
context_id = context_id,
key = key,
env_var = env_var,
"Environment variable contains invalid UTF-8"
);
Err(ContextError::SecretProvider(format!(
"Environment variable '{}' contains invalid UTF-8",
env_var
)))
}
}
}
async fn set_secret(
&self,
context_id: &str,
key: &str,
_value: &str,
) -> Result<(), ContextError> {
let env_var = self.build_env_var(context_id, key);
tracing::warn!(
context_id = context_id,
key = key,
env_var = env_var,
"Attempted to set secret via environment provider (read-only)"
);
Err(ContextError::SecretProvider(
"Environment provider is read-only. Cannot set secrets.".to_string(),
))
}
async fn delete_secret(&self, context_id: &str, key: &str) -> Result<(), ContextError> {
let env_var = self.build_env_var(context_id, key);
tracing::warn!(
context_id = context_id,
key = key,
env_var = env_var,
"Attempted to delete secret via environment provider (read-only)"
);
Err(ContextError::SecretProvider(
"Environment provider is read-only. Cannot delete secrets.".to_string(),
))
}
async fn list_keys(&self, context_id: &str) -> Result<Vec<String>, ContextError> {
let context_prefix = format!(
"{}{}__",
self.prefix,
context_id.to_uppercase().replace('-', "_").replace('.', "_")
);
let keys: Vec<String> = std::env::vars()
.filter_map(|(k, _)| {
if k.starts_with(&context_prefix) {
Some(
k[context_prefix.len()..]
.to_lowercase()
.replace('_', "-"),
)
} else {
None
}
})
.collect();
Ok(keys)
}
fn name(&self) -> &'static str {
"environment"
}
fn is_read_only(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_env_var_naming() {
let provider = EnvironmentProvider::new("SECRET_");
assert_eq!(
provider.build_env_var("my-context", "api-key"),
"SECRET_MY_CONTEXT__API_KEY"
);
assert_eq!(
provider.build_env_var("production.api", "database.password"),
"SECRET_PRODUCTION_API__DATABASE_PASSWORD"
);
}
#[test]
fn test_no_prefix() {
let provider = EnvironmentProvider::without_prefix();
assert_eq!(
provider.build_env_var("context", "key"),
"CONTEXT__KEY"
);
}
#[tokio::test]
async fn test_get_from_env() {
let provider = EnvironmentProvider::new("TEST_SECRET_");
std::env::set_var("TEST_SECRET_MY_CTX__MY_KEY", "my-secret-value");
let result = provider.get_secret("my-ctx", "my-key").await.unwrap();
assert!(result.is_some());
assert_eq!(&*result.unwrap(), "my-secret-value");
std::env::remove_var("TEST_SECRET_MY_CTX__MY_KEY");
}
#[tokio::test]
async fn test_get_nonexistent() {
let provider = EnvironmentProvider::new("NONEXISTENT_PREFIX_");
let result = provider
.get_secret("context", "key")
.await
.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn test_set_is_read_only() {
let provider = EnvironmentProvider::new("TEST_");
let result = provider.set_secret("ctx", "key", "value").await;
assert!(result.is_err());
assert!(provider.is_read_only());
}
#[tokio::test]
async fn test_delete_is_read_only() {
let provider = EnvironmentProvider::new("TEST_");
let result = provider.delete_secret("ctx", "key").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_list_keys() {
let provider = EnvironmentProvider::new("TEST_LIST_");
std::env::set_var("TEST_LIST_MY_CTX__KEY_ONE", "value1");
std::env::set_var("TEST_LIST_MY_CTX__KEY_TWO", "value2");
std::env::set_var("TEST_LIST_OTHER_CTX__KEY", "value3");
let keys = provider.list_keys("my-ctx").await.unwrap();
assert!(keys.contains(&"key-one".to_string()));
assert!(keys.contains(&"key-two".to_string()));
assert!(!keys.contains(&"key".to_string()));
std::env::remove_var("TEST_LIST_MY_CTX__KEY_ONE");
std::env::remove_var("TEST_LIST_MY_CTX__KEY_TWO");
std::env::remove_var("TEST_LIST_OTHER_CTX__KEY");
}
}