use async_trait::async_trait;
use cellos_core::ports::SecretBroker;
use cellos_core::{CellosError, SecretView};
pub struct EnvSecretBroker;
impl EnvSecretBroker {
pub fn new() -> Self {
Self
}
fn env_var_name(key: &str) -> String {
format!("CELLOS_SECRET_{}", key.to_uppercase().replace('-', "_"))
}
fn validate_key(key: &str) -> Result<(), CellosError> {
if key.is_empty() {
return Err(CellosError::SecretBroker(
"secret key must not be empty".into(),
));
}
if key.as_bytes().contains(&0) {
return Err(CellosError::SecretBroker(
"secret key must not contain NUL byte".into(),
));
}
if key.contains('=') {
return Err(CellosError::SecretBroker(
"secret key must not contain '=' character".into(),
));
}
Ok(())
}
}
impl Default for EnvSecretBroker {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl SecretBroker for EnvSecretBroker {
async fn resolve(
&self,
key: &str,
_cell_id: &str,
_ttl_seconds: u64,
) -> Result<SecretView, CellosError> {
Self::validate_key(key)?;
let var = Self::env_var_name(key);
let value = std::env::var(&var).map_err(|_| {
CellosError::SecretBroker(format!(
"env var {var} not set (required for secret key {key:?})"
))
})?;
Ok(SecretView {
key: key.to_string(),
value: zeroize::Zeroizing::new(value),
})
}
async fn revoke_for_cell(&self, _cell_id: &str) -> Result<(), CellosError> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn resolves_present_key() {
std::env::set_var("CELLOS_SECRET_TEST_TOKEN", "secret-value");
let broker = EnvSecretBroker::new();
let view = broker.resolve("TEST_TOKEN", "cell-1", 60).await.unwrap();
assert_eq!(view.key, "TEST_TOKEN");
assert_eq!(view.value.as_str(), "secret-value");
}
#[tokio::test]
async fn errors_on_missing_key() {
std::env::remove_var("CELLOS_SECRET_MISSING_KEY_XYZ");
let broker = EnvSecretBroker::new();
let err = broker
.resolve("MISSING_KEY_XYZ", "cell-1", 60)
.await
.unwrap_err();
assert!(err.to_string().contains("CELLOS_SECRET_MISSING_KEY_XYZ"));
}
#[tokio::test]
async fn revoke_is_noop() {
let broker = EnvSecretBroker::new();
broker.revoke_for_cell("any-cell").await.unwrap();
}
#[tokio::test]
async fn normalizes_hyphenated_key() {
std::env::set_var("CELLOS_SECRET_MY_API_KEY", "tok");
let broker = EnvSecretBroker::new();
let view = broker.resolve("my-api-key", "cell-1", 60).await.unwrap();
assert_eq!(view.value.as_str(), "tok");
}
}