463 lines
16 KiB
Rust
463 lines
16 KiB
Rust
//! Configuration management for Veza Rust services
|
|
//!
|
|
//! This module provides centralized configuration management with validation
|
|
//! and environment variable support.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use std::env;
|
|
use tracing::info;
|
|
|
|
/// Main configuration trait for Veza services
|
|
pub trait VezaConfig: Default + Clone + Serialize {
|
|
/// Load configuration from environment variables
|
|
fn from_env() -> crate::VezaResult<Self> {
|
|
let mut config = Self::default();
|
|
config.load_from_env()?;
|
|
config.validate()?;
|
|
Ok(config)
|
|
}
|
|
|
|
/// Load configuration from environment variables
|
|
fn load_from_env(&mut self) -> crate::VezaResult<()>;
|
|
|
|
/// Validate configuration
|
|
fn validate(&self) -> crate::VezaResult<()>;
|
|
|
|
/// Get configuration as JSON for debugging
|
|
fn to_json(&self) -> crate::VezaResult<String> {
|
|
Ok(serde_json::to_string_pretty(self)?)
|
|
}
|
|
}
|
|
|
|
/// Database configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DatabaseConfig {
|
|
pub url: String,
|
|
pub max_connections: u32,
|
|
pub min_connections: u32,
|
|
pub connect_timeout: u64,
|
|
pub idle_timeout: u64,
|
|
pub acquire_timeout: u64,
|
|
}
|
|
|
|
impl Default for DatabaseConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
url: "postgresql://veza:password@localhost:5432/veza_db".to_string(),
|
|
max_connections: 20,
|
|
min_connections: 5,
|
|
connect_timeout: 5,
|
|
idle_timeout: 600,
|
|
acquire_timeout: 10,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl VezaConfig for DatabaseConfig {
|
|
fn load_from_env(&mut self) -> crate::VezaResult<()> {
|
|
if let Ok(url) = env::var("DATABASE_URL") {
|
|
self.url = url;
|
|
}
|
|
if let Ok(max_conn) = env::var("DATABASE_MAX_CONNECTIONS") {
|
|
self.max_connections = max_conn.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid DATABASE_MAX_CONNECTIONS".to_string()))?;
|
|
}
|
|
if let Ok(min_conn) = env::var("DATABASE_MIN_CONNECTIONS") {
|
|
self.min_connections = min_conn.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid DATABASE_MIN_CONNECTIONS".to_string()))?;
|
|
}
|
|
if let Ok(timeout) = env::var("DATABASE_CONNECT_TIMEOUT") {
|
|
self.connect_timeout = timeout.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid DATABASE_CONNECT_TIMEOUT".to_string()))?;
|
|
}
|
|
if let Ok(timeout) = env::var("DATABASE_IDLE_TIMEOUT") {
|
|
self.idle_timeout = timeout.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid DATABASE_IDLE_TIMEOUT".to_string()))?;
|
|
}
|
|
if let Ok(timeout) = env::var("DATABASE_ACQUIRE_TIMEOUT") {
|
|
self.acquire_timeout = timeout.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid DATABASE_ACQUIRE_TIMEOUT".to_string()))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate(&self) -> crate::VezaResult<()> {
|
|
if self.url.is_empty() {
|
|
return Err(crate::VezaError::Config("Database URL cannot be empty".to_string()));
|
|
}
|
|
if self.max_connections < self.min_connections {
|
|
return Err(crate::VezaError::Config("Max connections must be >= min connections".to_string()));
|
|
}
|
|
if self.max_connections == 0 {
|
|
return Err(crate::VezaError::Config("Max connections must be > 0".to_string()));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Redis configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RedisConfig {
|
|
pub url: String,
|
|
pub max_connections: u32,
|
|
pub connection_timeout: u64,
|
|
pub command_timeout: u64,
|
|
pub retry_attempts: u32,
|
|
}
|
|
|
|
impl Default for RedisConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
url: "redis://localhost:6379".to_string(),
|
|
max_connections: 10,
|
|
connection_timeout: 5,
|
|
command_timeout: 3,
|
|
retry_attempts: 3,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl VezaConfig for RedisConfig {
|
|
fn load_from_env(&mut self) -> crate::VezaResult<()> {
|
|
if let Ok(url) = env::var("REDIS_URL") {
|
|
self.url = url;
|
|
}
|
|
if let Ok(max_conn) = env::var("REDIS_MAX_CONNECTIONS") {
|
|
self.max_connections = max_conn.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid REDIS_MAX_CONNECTIONS".to_string()))?;
|
|
}
|
|
if let Ok(timeout) = env::var("REDIS_CONNECTION_TIMEOUT") {
|
|
self.connection_timeout = timeout.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid REDIS_CONNECTION_TIMEOUT".to_string()))?;
|
|
}
|
|
if let Ok(timeout) = env::var("REDIS_COMMAND_TIMEOUT") {
|
|
self.command_timeout = timeout.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid REDIS_COMMAND_TIMEOUT".to_string()))?;
|
|
}
|
|
if let Ok(retries) = env::var("REDIS_RETRY_ATTEMPTS") {
|
|
self.retry_attempts = retries.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid REDIS_RETRY_ATTEMPTS".to_string()))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate(&self) -> crate::VezaResult<()> {
|
|
if self.url.is_empty() {
|
|
return Err(crate::VezaError::Config("Redis URL cannot be empty".to_string()));
|
|
}
|
|
if self.max_connections == 0 {
|
|
return Err(crate::VezaError::Config("Redis max connections must be > 0".to_string()));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Server configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ServerConfig {
|
|
pub host: String,
|
|
pub port: u16,
|
|
pub workers: Option<usize>,
|
|
pub keep_alive: u64,
|
|
pub max_connections: u32,
|
|
pub timeout: u64,
|
|
}
|
|
|
|
impl Default for ServerConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
host: "0.0.0.0".to_string(),
|
|
port: 8080,
|
|
workers: None,
|
|
keep_alive: 30,
|
|
max_connections: 1000,
|
|
timeout: 30,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl VezaConfig for ServerConfig {
|
|
fn load_from_env(&mut self) -> crate::VezaResult<()> {
|
|
if let Ok(host) = env::var("SERVER_HOST") {
|
|
self.host = host;
|
|
}
|
|
if let Ok(port) = env::var("SERVER_PORT") {
|
|
self.port = port.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid SERVER_PORT".to_string()))?;
|
|
}
|
|
if let Ok(workers) = env::var("SERVER_WORKERS") {
|
|
self.workers = Some(workers.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid SERVER_WORKERS".to_string()))?);
|
|
}
|
|
if let Ok(keep_alive) = env::var("SERVER_KEEP_ALIVE") {
|
|
self.keep_alive = keep_alive.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid SERVER_KEEP_ALIVE".to_string()))?;
|
|
}
|
|
if let Ok(max_conn) = env::var("SERVER_MAX_CONNECTIONS") {
|
|
self.max_connections = max_conn.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid SERVER_MAX_CONNECTIONS".to_string()))?;
|
|
}
|
|
if let Ok(timeout) = env::var("SERVER_TIMEOUT") {
|
|
self.timeout = timeout.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid SERVER_TIMEOUT".to_string()))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate(&self) -> crate::VezaResult<()> {
|
|
if self.host.is_empty() {
|
|
return Err(crate::VezaError::Config("Server host cannot be empty".to_string()));
|
|
}
|
|
if self.port == 0 {
|
|
return Err(crate::VezaError::Config("Server port must be > 0".to_string()));
|
|
}
|
|
if self.max_connections == 0 {
|
|
return Err(crate::VezaError::Config("Server max connections must be > 0".to_string()));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// JWT configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct JwtConfig {
|
|
pub secret: String,
|
|
pub access_token_ttl: u64,
|
|
pub refresh_token_ttl: u64,
|
|
pub issuer: String,
|
|
pub audience: String,
|
|
}
|
|
|
|
impl Default for JwtConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
// VEZA-SEC-001: No default secret — JWT_SECRET must be set via load_from_env
|
|
secret: String::new(),
|
|
access_token_ttl: 3600, // 1 hour
|
|
refresh_token_ttl: 604800, // 7 days
|
|
issuer: "veza-api".to_string(),
|
|
audience: "veza-platform".to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl VezaConfig for JwtConfig {
|
|
fn load_from_env(&mut self) -> crate::VezaResult<()> {
|
|
// VEZA-SEC-001: JWT_SECRET is REQUIRED — refusing to start with no secret
|
|
self.secret = env::var("JWT_SECRET")
|
|
.map_err(|_| crate::VezaError::Config("JWT_SECRET must be set — refusing to start with no secret".to_string()))?;
|
|
if let Ok(ttl) = env::var("JWT_ACCESS_TOKEN_TTL") {
|
|
self.access_token_ttl = ttl.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid JWT_ACCESS_TOKEN_TTL".to_string()))?;
|
|
}
|
|
if let Ok(ttl) = env::var("JWT_REFRESH_TOKEN_TTL") {
|
|
self.refresh_token_ttl = ttl.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid JWT_REFRESH_TOKEN_TTL".to_string()))?;
|
|
}
|
|
if let Ok(issuer) = env::var("JWT_ISSUER") {
|
|
self.issuer = issuer;
|
|
}
|
|
if let Ok(audience) = env::var("JWT_AUDIENCE") {
|
|
self.audience = audience;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate(&self) -> crate::VezaResult<()> {
|
|
if self.secret.is_empty() {
|
|
return Err(crate::VezaError::Config("JWT secret cannot be empty".to_string()));
|
|
}
|
|
if self.secret.len() < 32 {
|
|
return Err(crate::VezaError::Config(
|
|
"JWT secret must be at least 32 characters".to_string(),
|
|
));
|
|
}
|
|
if self.access_token_ttl == 0 {
|
|
return Err(crate::VezaError::Config("JWT access token TTL must be > 0".to_string()));
|
|
}
|
|
if self.refresh_token_ttl == 0 {
|
|
return Err(crate::VezaError::Config("JWT refresh token TTL must be > 0".to_string()));
|
|
}
|
|
if self.issuer.is_empty() {
|
|
return Err(crate::VezaError::Config("JWT issuer cannot be empty".to_string()));
|
|
}
|
|
if self.audience.is_empty() {
|
|
return Err(crate::VezaError::Config("JWT audience cannot be empty".to_string()));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Rate limiting configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RateLimitConfig {
|
|
pub enabled: bool,
|
|
pub global_limit: u32,
|
|
pub global_window: u64,
|
|
pub per_user_limit: u32,
|
|
pub per_user_window: u64,
|
|
pub per_ip_limit: u32,
|
|
pub per_ip_window: u64,
|
|
}
|
|
|
|
impl Default for RateLimitConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: true,
|
|
global_limit: 1000,
|
|
global_window: 60,
|
|
per_user_limit: 100,
|
|
per_user_window: 60,
|
|
per_ip_limit: 60,
|
|
per_ip_window: 60,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl VezaConfig for RateLimitConfig {
|
|
fn load_from_env(&mut self) -> crate::VezaResult<()> {
|
|
if let Ok(enabled) = env::var("RATE_LIMIT_ENABLED") {
|
|
self.enabled = enabled.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_ENABLED".to_string()))?;
|
|
}
|
|
if let Ok(limit) = env::var("RATE_LIMIT_GLOBAL_LIMIT") {
|
|
self.global_limit = limit.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_GLOBAL_LIMIT".to_string()))?;
|
|
}
|
|
if let Ok(window) = env::var("RATE_LIMIT_GLOBAL_WINDOW") {
|
|
self.global_window = window.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_GLOBAL_WINDOW".to_string()))?;
|
|
}
|
|
if let Ok(limit) = env::var("RATE_LIMIT_PER_USER_LIMIT") {
|
|
self.per_user_limit = limit.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_PER_USER_LIMIT".to_string()))?;
|
|
}
|
|
if let Ok(window) = env::var("RATE_LIMIT_PER_USER_WINDOW") {
|
|
self.per_user_window = window.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_PER_USER_WINDOW".to_string()))?;
|
|
}
|
|
if let Ok(limit) = env::var("RATE_LIMIT_PER_IP_LIMIT") {
|
|
self.per_ip_limit = limit.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_PER_IP_LIMIT".to_string()))?;
|
|
}
|
|
if let Ok(window) = env::var("RATE_LIMIT_PER_IP_WINDOW") {
|
|
self.per_ip_window = window.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_PER_IP_WINDOW".to_string()))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate(&self) -> crate::VezaResult<()> {
|
|
if self.global_limit == 0 {
|
|
return Err(crate::VezaError::Config("Global rate limit must be > 0".to_string()));
|
|
}
|
|
if self.global_window == 0 {
|
|
return Err(crate::VezaError::Config("Global rate limit window must be > 0".to_string()));
|
|
}
|
|
if self.per_user_limit == 0 {
|
|
return Err(crate::VezaError::Config("Per-user rate limit must be > 0".to_string()));
|
|
}
|
|
if self.per_user_window == 0 {
|
|
return Err(crate::VezaError::Config("Per-user rate limit window must be > 0".to_string()));
|
|
}
|
|
if self.per_ip_limit == 0 {
|
|
return Err(crate::VezaError::Config("Per-IP rate limit must be > 0".to_string()));
|
|
}
|
|
if self.per_ip_window == 0 {
|
|
return Err(crate::VezaError::Config("Per-IP rate limit window must be > 0".to_string()));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Logging configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct LoggingConfig {
|
|
pub level: String,
|
|
pub format: String,
|
|
pub file: Option<String>,
|
|
pub max_size: u64,
|
|
pub max_files: u32,
|
|
pub compress: bool,
|
|
}
|
|
|
|
impl Default for LoggingConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
level: "info".to_string(),
|
|
format: "json".to_string(),
|
|
file: None,
|
|
max_size: 100 * 1024 * 1024, // 100MB
|
|
max_files: 5,
|
|
compress: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl VezaConfig for LoggingConfig {
|
|
fn load_from_env(&mut self) -> crate::VezaResult<()> {
|
|
if let Ok(level) = env::var("LOG_LEVEL") {
|
|
self.level = level;
|
|
}
|
|
if let Ok(format) = env::var("LOG_FORMAT") {
|
|
self.format = format;
|
|
}
|
|
if let Ok(file) = env::var("LOG_FILE") {
|
|
self.file = Some(file);
|
|
}
|
|
if let Ok(size) = env::var("LOG_MAX_SIZE") {
|
|
self.max_size = size.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid LOG_MAX_SIZE".to_string()))?;
|
|
}
|
|
if let Ok(files) = env::var("LOG_MAX_FILES") {
|
|
self.max_files = files.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid LOG_MAX_FILES".to_string()))?;
|
|
}
|
|
if let Ok(compress) = env::var("LOG_COMPRESS") {
|
|
self.compress = compress.parse()
|
|
.map_err(|_| crate::VezaError::Config("Invalid LOG_COMPRESS".to_string()))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate(&self) -> crate::VezaResult<()> {
|
|
let valid_levels = ["trace", "debug", "info", "warn", "error"];
|
|
if !valid_levels.contains(&self.level.as_str()) {
|
|
return Err(crate::VezaError::Config(format!("Invalid log level: {}", self.level)));
|
|
}
|
|
let valid_formats = ["json", "text"];
|
|
if !valid_formats.contains(&self.format.as_str()) {
|
|
return Err(crate::VezaError::Config(format!("Invalid log format: {}", self.format)));
|
|
}
|
|
if self.max_size == 0 {
|
|
return Err(crate::VezaError::Config("Log max size must be > 0".to_string()));
|
|
}
|
|
if self.max_files == 0 {
|
|
return Err(crate::VezaError::Config("Log max files must be > 0".to_string()));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Load configuration from environment with validation
|
|
pub fn load_config<T: VezaConfig>() -> crate::VezaResult<T> {
|
|
let config = T::from_env()?;
|
|
info!("Configuration loaded successfully");
|
|
Ok(config)
|
|
}
|
|
|
|
/// Load configuration from file
|
|
pub fn load_config_from_file<T: VezaConfig>(path: &str) -> crate::VezaResult<T>
|
|
where
|
|
T: for<'de> Deserialize<'de>,
|
|
{
|
|
let content = std::fs::read_to_string(path)
|
|
.map_err(|e| crate::VezaError::Config(format!("Failed to read config file {}: {}", path, e)))?;
|
|
|
|
let config: T = serde_json::from_str(&content)
|
|
.map_err(|e| crate::VezaError::Config(format!("Failed to parse config file {}: {}", path, e)))?;
|
|
|
|
config.validate()?;
|
|
info!("Configuration loaded from file: {}", path);
|
|
Ok(config)
|
|
}
|