pub mod errors;
pub mod events;
pub mod metrics;
pub mod providers;
pub mod tracing;
use async_trait::async_trait;
use secrecy::{ExposeSecret, SecretString};
use std::sync::Arc;
use tokio::sync::broadcast;
pub use errors::SecretError;
pub use events::SecretEvent;
pub use providers::{
chained_provider::ChainedProvider,
env_provider::EnvProvider,
file_provider::{FileFormat, FileProvider},
memory_provider::MemoryProvider,
};
#[async_trait]
pub trait SecretProvider: Send + Sync + std::fmt::Debug {
async fn get(&self, key: &str) -> Result<SecretString, SecretError>;
async fn set(&self, key: &str, value: SecretString) -> Result<(), SecretError>;
async fn keys(&self) -> Result<Vec<String>, SecretError>;
async fn watch(&self, tx: broadcast::Sender<SecretEvent>) -> Result<(), SecretError>;
}
pub async fn redact_line(line: &str, provider: &dyn SecretProvider) -> String {
let keys = match provider.keys().await {
Ok(k) => k,
Err(_) => return line.to_string(),
};
let mut patterns = Vec::new();
for key in keys {
if let Ok(secret) = provider.get(&key).await {
patterns.push(secret.expose_secret().to_owned());
}
}
if patterns.is_empty() {
return line.to_string();
}
match aho_corasick::AhoCorasick::new(&patterns) {
Ok(ac) => ac.replace_all(
line,
&std::iter::repeat_n("[REDACTED]", patterns.len()).collect::<Vec<_>>(),
),
Err(_) => line.to_string(),
}
}
pub async fn init() -> Result<Arc<dyn SecretProvider>, SecretError> {
use std::env;
let chain_raw = env::var("SECRET_PROVIDER_CHAIN").unwrap_or_else(|_| "env".to_string());
let chain_parts: Vec<&str> = chain_raw
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
let mut providers: Vec<Arc<dyn SecretProvider>> = Vec::new();
for part in chain_parts {
match part {
"env" => {
providers.push(Arc::new(EnvProvider::new()));
}
"file" => {
let path = env::var("SECRET_FILE_PATH").map_err(|_| {
SecretError::Configuration(
"SECRET_FILE_PATH env var not set for file provider".to_string(),
)
})?;
let format = match env::var("SECRET_FILE_FORMAT")
.unwrap_or_else(|_| "toml".to_string())
.to_lowercase()
.as_str()
{
"toml" => FileFormat::Toml,
other => {
return Err(SecretError::Configuration(format!(
"Unsupported file format '{}'. Only 'toml' is supported",
other
)));
}
};
let fp = FileProvider::new(path, format).await?;
providers.push(Arc::new(fp));
}
other => {
return Err(SecretError::Configuration(format!(
"Unknown provider '{}' in SECRET_PROVIDER_CHAIN",
other
)));
}
}
}
if providers.is_empty() {
return Err(SecretError::Configuration(
"No providers configured".to_string(),
));
}
match providers.as_slice() {
[single] => Ok(single.clone()),
_ => Ok(Arc::new(ChainedProvider::new(providers))),
}
}