use crate::secret_provider::{
SecretError, SecretEvent, SecretProvider, tracing as provider_tracing,
};
use async_trait::async_trait;
use secrecy::SecretString;
use std::env;
use tokio::sync::broadcast;
use tracing::{debug, instrument};
#[derive(Debug, Default, Clone)]
pub struct EnvProvider;
impl EnvProvider {
pub fn new() -> Self {
Self {}
}
}
#[async_trait]
impl SecretProvider for EnvProvider {
#[instrument(
name = "env_provider.get",
skip_all,
fields(
secret_op = "get",
secret_key_hash = tracing::field::Empty,
outcome = tracing::field::Empty
)
)]
async fn get(&self, key: &str) -> Result<SecretString, SecretError> {
provider_tracing::add_hashed_key(key);
#[cfg(feature = "metrics")]
let _timer = crate::secret_provider::metrics::OpTimer::new("env", "get");
match env::var(key) {
Ok(value) => {
provider_tracing::record_success();
#[cfg(feature = "metrics")]
crate::secret_provider::metrics::record_operation("env", "get", "success");
Ok(SecretString::new(value.into()))
}
Err(env::VarError::NotPresent) => {
let err = SecretError::NotFound(key.to_string());
provider_tracing::record_error(&err);
#[cfg(feature = "metrics")]
crate::secret_provider::metrics::record_operation("env", "get", "error");
Err(err)
}
Err(e) => {
let err = SecretError::Backend(
anyhow::Error::new(e)
.context(format!("Failed to read environment variable '{}'", key)),
);
provider_tracing::record_error(&err);
#[cfg(feature = "metrics")]
crate::secret_provider::metrics::record_operation("env", "get", "error");
Err(err)
}
}
}
#[instrument(
name = "env_provider.set",
skip_all,
fields(
secret_op = "set",
secret_key_hash = tracing::field::Empty,
outcome = tracing::field::Empty
)
)]
async fn set(&self, key: &str, _value: SecretString) -> Result<(), SecretError> {
provider_tracing::add_hashed_key(key);
#[cfg(feature = "metrics")]
let _timer = crate::secret_provider::metrics::OpTimer::new("env", "set");
debug!(
"'set' operation is not supported by EnvProvider for key: {}",
key
);
let err = SecretError::UnsupportedOperation(
"Setting secrets via environment variables is not supported".to_string(),
);
provider_tracing::record_error(&err);
#[cfg(feature = "metrics")]
crate::secret_provider::metrics::record_operation("env", "set", "error");
Err(err)
}
#[instrument(
name = "env_provider.keys",
skip_all,
fields(
secret_op = "keys",
outcome = tracing::field::Empty
)
)]
async fn keys(&self) -> Result<Vec<String>, SecretError> {
#[cfg(feature = "metrics")]
let _timer = crate::secret_provider::metrics::OpTimer::new("env", "keys");
debug!(
"'keys' operation on EnvProvider returns an empty list for safety/performance reasons."
);
provider_tracing::record_success();
#[cfg(feature = "metrics")]
crate::secret_provider::metrics::record_operation("env", "keys", "success");
Ok(Vec::new())
}
#[instrument(
name = "env_provider.watch",
skip_all,
fields(
secret_op = "watch",
outcome = tracing::field::Empty
)
)]
async fn watch(&self, _tx: broadcast::Sender<SecretEvent>) -> Result<(), SecretError> {
#[cfg(feature = "metrics")]
let _timer = crate::secret_provider::metrics::OpTimer::new("env", "watch");
debug!("'watch' operation is a no-op for EnvProvider.");
provider_tracing::record_success();
#[cfg(feature = "metrics")]
crate::secret_provider::metrics::record_operation("env", "watch", "success");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Result;
use secrecy::ExposeSecret;
use std::env;
const TEST_ENV_VAR: &str = "SECRET_PROVIDER_TEST_VAR";
const TEST_SECRET_VALUE: &str = "test_secret_value_123";
#[tokio::test]
async fn env_provider_get_success() -> Result<()> {
unsafe {
env::set_var(TEST_ENV_VAR, TEST_SECRET_VALUE);
}
let provider = EnvProvider::new();
let secret = provider.get(TEST_ENV_VAR).await?;
assert_eq!(secret.expose_secret(), TEST_SECRET_VALUE);
unsafe {
env::remove_var(TEST_ENV_VAR);
} Ok(())
}
#[tokio::test]
async fn env_provider_get_not_found() -> Result<()> {
let provider = EnvProvider::new();
let result = provider.get("NON_EXISTENT_VAR_FOR_TESTING").await;
assert!(matches!(result, Err(SecretError::NotFound(_))));
Ok(())
}
#[tokio::test]
async fn env_provider_set_unsupported() -> Result<()> {
let provider = EnvProvider::new();
let result = provider
.set("ANY_KEY", SecretString::new("any_value".into()))
.await;
assert!(matches!(result, Err(SecretError::UnsupportedOperation(_))));
Ok(())
}
#[tokio::test]
async fn env_provider_keys_empty() -> Result<()> {
let provider = EnvProvider::new();
let keys = provider.keys().await?;
assert!(keys.is_empty());
Ok(())
}
#[tokio::test]
async fn env_provider_watch_noop() -> Result<()> {
let provider = EnvProvider::new();
let (tx, _) = broadcast::channel(1);
let result = provider.watch(tx).await;
assert!(result.is_ok());
Ok(())
}
}