pub mod env;
pub mod vault;
use std::sync::Arc;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum SecretsError {
#[error("Secret not found: {0}")]
NotFound(String),
#[error("Vault error: {0}")]
Vault(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("Authentication failed: {0}")]
AuthFailed(String),
}
pub struct SecretKey;
impl SecretKey {
pub const JWT_SECRET: &'static str = "jwt_secret";
pub const DB_USERNAME: &'static str = "db_username";
pub const DB_PASSWORD: &'static str = "db_password";
pub const RESEND_API_KEY: &'static str = "resend_api_key";
}
#[async_trait::async_trait]
pub trait SecretsBackend: Send + Sync {
async fn get_secret(&self, key: &str) -> Result<Option<String>, SecretsError>;
fn name(&self) -> &'static str;
}
pub struct SecretsManager {
backends: Vec<Arc<dyn SecretsBackend>>,
}
impl SecretsManager {
pub fn new() -> Self {
Self {
backends: Vec::new(),
}
}
pub fn with_backend(mut self, backend: Arc<dyn SecretsBackend>) -> Self {
self.backends.push(backend);
self
}
pub async fn get_secret(&self, key: &str) -> Result<String, SecretsError> {
for backend in &self.backends {
match backend.get_secret(key).await {
Ok(Some(value)) => {
tracing::debug!(secret_key = key, backend = backend.name(), "Secret loaded");
return Ok(value);
}
Ok(None) => continue,
Err(e) => {
tracing::warn!(
secret_key = key,
backend = backend.name(),
error = %e,
"Backend failed to retrieve secret"
);
continue;
}
}
}
Err(SecretsError::NotFound(key.to_string()))
}
pub async fn get_secret_optional(&self, key: &str) -> Result<Option<String>, SecretsError> {
match self.get_secret(key).await {
Ok(v) => Ok(Some(v)),
Err(SecretsError::NotFound(_)) => Ok(None),
Err(e) => Err(e),
}
}
#[allow(dead_code)]
pub fn has_backends(&self) -> bool {
!self.backends.is_empty()
}
pub fn backend_names(&self) -> Vec<&'static str> {
self.backends.iter().map(|b| b.name()).collect()
}
}
impl Default for SecretsManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockBackend {
secrets: std::collections::HashMap<String, String>,
}
impl MockBackend {
fn new(secrets: Vec<(&str, &str)>) -> Self {
Self {
secrets: secrets
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
}
}
}
#[async_trait::async_trait]
impl SecretsBackend for MockBackend {
async fn get_secret(&self, key: &str) -> Result<Option<String>, SecretsError> {
Ok(self.secrets.get(key).cloned())
}
fn name(&self) -> &'static str {
"mock"
}
}
#[tokio::test]
async fn test_secrets_manager_priority() {
let backend1 = Arc::new(MockBackend::new(vec![("key1", "value1")]));
let backend2 = Arc::new(MockBackend::new(vec![
("key1", "value2"),
("key2", "value2"),
]));
let manager = SecretsManager::new()
.with_backend(backend1)
.with_backend(backend2);
assert_eq!(manager.get_secret("key1").await.unwrap(), "value1");
assert_eq!(manager.get_secret("key2").await.unwrap(), "value2");
assert!(manager.get_secret("key3").await.is_err());
}
#[tokio::test]
async fn test_get_secret_optional() {
let backend = Arc::new(MockBackend::new(vec![("exists", "value")]));
let manager = SecretsManager::new().with_backend(backend);
assert_eq!(
manager.get_secret_optional("exists").await.unwrap(),
Some("value".to_string())
);
assert_eq!(manager.get_secret_optional("missing").await.unwrap(), None);
}
}