use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecretsConfig {
#[serde(flatten)]
pub backend: SecretsBackend,
#[serde(default)]
pub common: CommonSecretsConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum SecretsBackend {
Vault(VaultConfig),
File(FileConfig),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommonSecretsConfig {
#[serde(default = "default_timeout")]
pub timeout_seconds: u64,
#[serde(default = "default_max_retries")]
pub max_retries: u32,
#[serde(default = "default_enable_cache")]
pub enable_cache: bool,
#[serde(default = "default_cache_ttl")]
pub cache_ttl_seconds: u64,
pub audit: Option<super::auditing::AuditConfig>,
}
impl Default for CommonSecretsConfig {
fn default() -> Self {
Self {
timeout_seconds: default_timeout(),
max_retries: default_max_retries(),
enable_cache: default_enable_cache(),
cache_ttl_seconds: default_cache_ttl(),
audit: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultConfig {
pub url: String,
pub auth: VaultAuthConfig,
pub namespace: Option<String>,
#[serde(default = "default_vault_mount")]
pub mount_path: String,
#[serde(default = "default_vault_api_version")]
pub api_version: String,
#[serde(default)]
pub tls: VaultTlsConfig,
#[serde(default)]
pub connection: VaultConnectionConfig,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "method", rename_all = "lowercase")]
pub enum VaultAuthConfig {
Token {
token: String,
},
AppRole {
role_id: String,
secret_id: String,
#[serde(default = "default_approle_mount")]
mount_path: String,
},
Kubernetes {
#[serde(default = "default_k8s_token_path")]
token_path: String,
role: String,
#[serde(default = "default_k8s_mount")]
mount_path: String,
},
Aws {
region: String,
role: String,
#[serde(default = "default_aws_mount")]
mount_path: String,
},
}
impl std::fmt::Debug for VaultAuthConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VaultAuthConfig::Token { .. } => f
.debug_struct("VaultAuthConfig::Token")
.field("token", &"[REDACTED]")
.finish(),
VaultAuthConfig::AppRole {
role_id,
mount_path,
..
} => f
.debug_struct("VaultAuthConfig::AppRole")
.field("role_id", role_id)
.field("secret_id", &"[REDACTED]")
.field("mount_path", mount_path)
.finish(),
VaultAuthConfig::Kubernetes {
token_path,
role,
mount_path,
} => f
.debug_struct("VaultAuthConfig::Kubernetes")
.field("token_path", token_path)
.field("role", role)
.field("mount_path", mount_path)
.finish(),
VaultAuthConfig::Aws {
region,
role,
mount_path,
} => f
.debug_struct("VaultAuthConfig::Aws")
.field("region", region)
.field("role", role)
.field("mount_path", mount_path)
.finish(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct VaultTlsConfig {
#[serde(default)]
pub skip_verify: bool,
pub ca_cert: Option<PathBuf>,
pub client_cert: Option<PathBuf>,
pub client_key: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultConnectionConfig {
#[serde(default = "default_max_connections")]
pub max_connections: usize,
#[serde(default = "default_connection_timeout")]
pub connection_timeout_seconds: u64,
#[serde(default = "default_request_timeout")]
pub request_timeout_seconds: u64,
}
impl Default for VaultConnectionConfig {
fn default() -> Self {
Self {
max_connections: default_max_connections(),
connection_timeout_seconds: default_connection_timeout(),
request_timeout_seconds: default_request_timeout(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileConfig {
pub path: PathBuf,
#[serde(default = "default_file_format")]
pub format: FileFormat,
#[serde(default)]
pub encryption: FileEncryptionConfig,
pub permissions: Option<u32>,
#[serde(default)]
pub watch_for_changes: bool,
#[serde(default)]
pub backup: FileBackupConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FileFormat {
Json,
Yaml,
Toml,
Env,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileEncryptionConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_encryption_algorithm")]
pub algorithm: String,
#[serde(default = "default_kdf")]
pub kdf: String,
#[serde(default)]
pub key: FileKeyConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileKeyConfig {
#[serde(default = "default_key_provider")]
pub provider: String,
pub env_var: Option<String>,
pub service: Option<String>,
pub account: Option<String>,
pub file_path: Option<PathBuf>,
}
impl Default for FileKeyConfig {
fn default() -> Self {
Self {
provider: default_key_provider(),
env_var: None,
service: None,
account: None,
file_path: None,
}
}
}
impl Default for FileEncryptionConfig {
fn default() -> Self {
Self {
enabled: false,
algorithm: default_encryption_algorithm(),
kdf: default_kdf(),
key: FileKeyConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileBackupConfig {
#[serde(default)]
pub enabled: bool,
pub backup_dir: Option<PathBuf>,
#[serde(default = "default_max_backups")]
pub max_backups: usize,
#[serde(default = "default_backup_before_write")]
pub backup_before_write: bool,
}
impl Default for FileBackupConfig {
fn default() -> Self {
Self {
enabled: false,
backup_dir: None,
max_backups: default_max_backups(),
backup_before_write: default_backup_before_write(),
}
}
}
fn default_timeout() -> u64 {
30
}
fn default_max_retries() -> u32 {
3
}
fn default_enable_cache() -> bool {
true
}
fn default_cache_ttl() -> u64 {
300
}
fn default_vault_mount() -> String {
"secret".to_string()
}
fn default_vault_api_version() -> String {
"v2".to_string()
}
fn default_approle_mount() -> String {
"approle".to_string()
}
fn default_k8s_token_path() -> String {
"/var/run/secrets/kubernetes.io/serviceaccount/token".to_string()
}
fn default_k8s_mount() -> String {
"kubernetes".to_string()
}
fn default_aws_mount() -> String {
"aws".to_string()
}
fn default_max_connections() -> usize {
10
}
fn default_connection_timeout() -> u64 {
10
}
fn default_request_timeout() -> u64 {
30
}
fn default_file_format() -> FileFormat {
FileFormat::Json
}
fn default_encryption_algorithm() -> String {
"AES-256-GCM".to_string()
}
fn default_kdf() -> String {
"PBKDF2".to_string()
}
fn default_key_provider() -> String {
"env".to_string()
}
fn default_max_backups() -> usize {
5
}
fn default_backup_before_write() -> bool {
true
}
impl SecretsConfig {
pub fn vault_with_token(url: String, token: String) -> Self {
Self {
backend: SecretsBackend::Vault(VaultConfig {
url,
auth: VaultAuthConfig::Token { token },
namespace: None,
mount_path: default_vault_mount(),
api_version: default_vault_api_version(),
tls: VaultTlsConfig::default(),
connection: VaultConnectionConfig::default(),
}),
common: CommonSecretsConfig::default(),
}
}
pub fn file_json(path: PathBuf) -> Self {
Self {
backend: SecretsBackend::File(FileConfig {
path,
format: FileFormat::Json,
encryption: FileEncryptionConfig::default(),
permissions: Some(0o600),
watch_for_changes: false,
backup: FileBackupConfig::default(),
}),
common: CommonSecretsConfig::default(),
}
}
pub fn backend_type(&self) -> &'static str {
match &self.backend {
SecretsBackend::Vault(_) => "vault",
SecretsBackend::File(_) => "file",
}
}
pub fn timeout(&self) -> Duration {
Duration::from_secs(self.common.timeout_seconds)
}
pub fn cache_ttl(&self) -> Duration {
Duration::from_secs(self.common.cache_ttl_seconds)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vault_config_creation() {
let config = SecretsConfig::vault_with_token(
"https://vault.example.com".to_string(),
"hvs.token123".to_string(),
);
assert_eq!(config.backend_type(), "vault");
if let SecretsBackend::Vault(vault_config) = &config.backend {
assert_eq!(vault_config.url, "https://vault.example.com");
if let VaultAuthConfig::Token { token } = &vault_config.auth {
assert_eq!(token, "hvs.token123");
} else {
panic!("Expected token auth");
}
} else {
panic!("Expected vault backend");
}
}
#[test]
fn test_file_config_creation() {
let path = PathBuf::from("/etc/secrets/app.json");
let config = SecretsConfig::file_json(path.clone());
assert_eq!(config.backend_type(), "file");
if let SecretsBackend::File(file_config) = &config.backend {
assert_eq!(file_config.path, path);
assert!(matches!(file_config.format, FileFormat::Json));
} else {
panic!("Expected file backend");
}
}
#[test]
fn test_common_config_defaults() {
let config = CommonSecretsConfig::default();
assert_eq!(config.timeout_seconds, 30);
assert_eq!(config.max_retries, 3);
assert!(config.enable_cache);
assert_eq!(config.cache_ttl_seconds, 300);
}
#[test]
fn test_timeout_conversion() {
let config = SecretsConfig::file_json(PathBuf::from("/test"));
assert_eq!(config.timeout(), Duration::from_secs(30));
assert_eq!(config.cache_ttl(), Duration::from_secs(300));
}
}