use std::path::{Path, PathBuf}; use std::time::Duration; use platform_config::ConfigLoader; use serde::{Deserialize, Serialize}; use tracing::info; use crate::error::{infrastructure, ControlCenterError, Result}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ControlCenterConfig { pub server: ServerConfig, pub database: DatabaseConfig, pub policies: PolicyConfig, pub auth: AuthConfig, pub compliance: ComplianceConfig, pub anomaly: AnomalyConfig, pub logging: LoggingConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServerConfig { pub host: String, pub port: u16, pub cors_origins: Vec, pub request_timeout_ms: u64, pub max_request_size: usize, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DatabaseConfig { pub url: String, pub namespace: String, pub database: String, pub username: Option, pub password: Option, pub connection_timeout_ms: u64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PolicyConfig { pub policy_dir: PathBuf, pub schema_dir: PathBuf, pub templates_dir: PathBuf, pub enable_versioning: bool, pub enable_caching: bool, pub cache_ttl_seconds: u64, pub validation_strict: bool, pub hooks: HooksConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HooksConfig { pub pre_execution: Vec, pub post_execution: Vec, pub on_policy_violation: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthConfig { pub jwt_secret: String, pub jwt_expiry_hours: u64, pub require_mfa: bool, pub password_policy: PasswordPolicy, pub session_timeout_minutes: u64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PasswordPolicy { pub min_length: usize, pub require_uppercase: bool, pub require_lowercase: bool, pub require_numbers: bool, pub require_special_chars: bool, pub max_age_days: u64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ComplianceConfig { pub soc2: Soc2Config, pub hipaa: HipaaConfig, pub reports: ReportsConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Soc2Config { pub enabled: bool, pub audit_log_retention_days: u64, pub require_approval_for_production: bool, pub sensitive_data_encryption: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HipaaConfig { pub enabled: bool, pub require_data_minimization: bool, pub audit_all_access: bool, pub encrypt_at_rest: bool, pub encrypt_in_transit: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReportsConfig { pub output_dir: PathBuf, pub formats: Vec, pub schedule_cron: Option, pub email_notifications: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AnomalyConfig { pub enabled: bool, pub baseline_window_hours: u64, pub detection_threshold: f64, pub statistical_methods: Vec, pub alert_channels: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoggingConfig { pub level: String, pub file_path: Option, pub structured: bool, pub include_spans: bool, } impl Default for ControlCenterConfig { fn default() -> Self { Self { server: ServerConfig { host: "127.0.0.1".to_string(), port: 8080, cors_origins: vec!["*".to_string()], request_timeout_ms: 30000, max_request_size: 10 * 1024 * 1024, // 10MB }, database: DatabaseConfig { url: "memory".to_string(), namespace: "control_center".to_string(), database: "policies".to_string(), username: None, password: None, connection_timeout_ms: 5000, }, policies: PolicyConfig { policy_dir: "policies".into(), schema_dir: "schemas".into(), templates_dir: "templates".into(), enable_versioning: true, enable_caching: true, cache_ttl_seconds: 300, validation_strict: true, hooks: HooksConfig { pre_execution: vec![], post_execution: vec![], on_policy_violation: vec![], }, }, auth: AuthConfig { jwt_secret: "change_me_in_production".to_string(), jwt_expiry_hours: 24, require_mfa: false, password_policy: PasswordPolicy { min_length: 8, require_uppercase: true, require_lowercase: true, require_numbers: true, require_special_chars: true, max_age_days: 90, }, session_timeout_minutes: 60, }, compliance: ComplianceConfig { soc2: Soc2Config { enabled: false, audit_log_retention_days: 365, require_approval_for_production: true, sensitive_data_encryption: true, }, hipaa: HipaaConfig { enabled: false, require_data_minimization: true, audit_all_access: true, encrypt_at_rest: true, encrypt_in_transit: true, }, reports: ReportsConfig { output_dir: "reports".into(), formats: vec!["json".to_string(), "html".to_string()], schedule_cron: None, email_notifications: vec![], }, }, anomaly: AnomalyConfig { enabled: true, baseline_window_hours: 24, detection_threshold: 2.0, statistical_methods: vec![ "zscore".to_string(), "iqr".to_string(), "isolation_forest".to_string(), ], alert_channels: vec![], }, logging: LoggingConfig { level: "info".to_string(), file_path: None, structured: true, include_spans: true, }, } } } impl ControlCenterConfig { /// Load configuration with hierarchical fallback logic: /// 1. Environment variable CONTROL_CENTER_CONFIG (explicit config path) /// 2. Mode-specific config: /// provisioning/platform/config/control-center.{mode}.toml /// 3. System defaults: config.defaults.toml /// /// Then environment variables (CONTROL_CENTER_*) override specific fields. pub fn load() -> Result { let mut config = Self::load_from_hierarchy()?; Self::apply_env_overrides(&mut config)?; Ok(config) } /// Internal: Load configuration from hierarchy without env var overrides fn load_from_hierarchy() -> Result { // Priority 1: Explicit config path from environment variable if let Ok(config_path) = std::env::var("CONTROL_CENTER_CONFIG") { return Self::from_file(&config_path); } // Priority 2: Mode-specific config (provisioning/platform/config/) if let Ok(mode) = std::env::var("CONTROL_CENTER_MODE") { let mode_config_path = format!("provisioning/platform/config/control-center.{}.toml", mode); if Path::new(&mode_config_path).exists() { return Self::from_file(&mode_config_path); } } // Priority 3: System defaults let defaults_path = Path::new("config/control-center.defaults.toml"); if defaults_path.exists() { return Self::from_file(defaults_path); } // Priority 4: Built-in defaults if file doesn't exist Ok(Self::default()) } /// Apply environment variable overrides to configuration /// Environment variables use format: CONTROL_CENTER_{SECTION}_{KEY}=value /// Example: CONTROL_CENTER_SERVER_PORT=8888 fn apply_env_overrides(config: &mut Self) -> Result<()> { // Server overrides if let Ok(host) = std::env::var("CONTROL_CENTER_SERVER_HOST") { config.server.host = host; } if let Ok(port) = std::env::var("CONTROL_CENTER_SERVER_PORT") { config.server.port = port.parse().map_err(|_| { ControlCenterError::Infrastructure( infrastructure::InfrastructureError::Configuration( "CONTROL_CENTER_SERVER_PORT must be a valid port number".to_string(), ), ) })?; } // Auth overrides if let Ok(jwt_secret) = std::env::var("CONTROL_CENTER_JWT_SECRET") { config.auth.jwt_secret = jwt_secret; } if let Ok(require_mfa) = std::env::var("CONTROL_CENTER_REQUIRE_MFA") { config.auth.require_mfa = require_mfa.to_lowercase() == "true"; } if let Ok(session_timeout) = std::env::var("CONTROL_CENTER_SESSION_TIMEOUT_MINUTES") { config.auth.session_timeout_minutes = session_timeout.parse().map_err(|_| { ControlCenterError::Infrastructure( infrastructure::InfrastructureError::Configuration( "CONTROL_CENTER_SESSION_TIMEOUT_MINUTES must be a valid number".to_string(), ), ) })?; } // Database overrides if let Ok(url) = std::env::var("CONTROL_CENTER_DATABASE_URL") { config.database.url = url; } if let Ok(username) = std::env::var("CONTROL_CENTER_DATABASE_USERNAME") { config.database.username = Some(username); } if let Ok(password) = std::env::var("CONTROL_CENTER_DATABASE_PASSWORD") { config.database.password = Some(password); } // Logging overrides if let Ok(level) = std::env::var("CONTROL_CENTER_LOG_LEVEL") { config.logging.level = level; } // Compliance overrides if let Ok(soc2_enabled) = std::env::var("CONTROL_CENTER_SOC2_ENABLED") { config.compliance.soc2.enabled = soc2_enabled.to_lowercase() == "true"; } if let Ok(hipaa_enabled) = std::env::var("CONTROL_CENTER_HIPAA_ENABLED") { config.compliance.hipaa.enabled = hipaa_enabled.to_lowercase() == "true"; } Ok(()) } /// Load configuration from file with environment variable interpolation pub fn from_file>(path: P) -> Result { let content = std::fs::read_to_string(path.as_ref()).map_err(|e| { ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration( format!("Failed to read config file {:?}: {}", path.as_ref(), e), )) })?; // Interpolate environment variables let interpolated = Self::interpolate_env_vars(&content)?; let config: Self = toml::from_str(&interpolated).map_err(|e| { ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration( format!("Failed to parse config: {}", e), )) })?; config.validate()?; Ok(config) } /// Interpolate environment variables in configuration fn interpolate_env_vars(content: &str) -> Result { let mut result = content.to_string(); // Replace ${VAR_NAME} with environment variable values let re = regex::Regex::new(r"\$\{([^}]+)\}").map_err(|e| { ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration( format!("Invalid regex pattern: {}", e), )) })?; for captures in re.captures_iter(content) { let var_name = &captures[1]; if let Ok(var_value) = std::env::var(var_name) { let placeholder = format!("${{{}}}", var_name); result = result.replace(&placeholder, &var_value); } } Ok(result) } /// Validate configuration values pub fn validate(&self) -> Result<()> { // Validate server config if self.server.port == 0 { return Err(ControlCenterError::Infrastructure( infrastructure::InfrastructureError::Configuration( "Server port cannot be 0".to_string(), ), )); } // Validate policy directories exist if !self.policies.policy_dir.exists() { return Err(ControlCenterError::Infrastructure( infrastructure::InfrastructureError::Configuration(format!( "Policy directory does not exist: {:?}", self.policies.policy_dir )), )); } // Validate auth config if self.auth.jwt_secret == "change_me_in_production" { tracing::warn!("Using default JWT secret - change in production!"); } if self.auth.jwt_secret.len() < 32 { return Err(ControlCenterError::Infrastructure( infrastructure::InfrastructureError::Configuration( "JWT secret must be at least 32 characters".to_string(), ), )); } // Validate password policy if self.auth.password_policy.min_length < 8 { return Err(ControlCenterError::Infrastructure( infrastructure::InfrastructureError::Configuration( "Password minimum length must be at least 8 characters".to_string(), ), )); } Ok(()) } /// Get configuration as JSON string pub fn to_json(&self) -> Result { serde_json::to_string_pretty(self).map_err(|e| { ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration( format!("Failed to serialize config to JSON: {}", e), )) }) } /// Create a default configuration file pub fn create_default_config>(path: P) -> Result<()> { let config = Self::default(); let toml_content = toml::to_string_pretty(&config).map_err(|e| { ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration( format!("Failed to serialize default config: {}", e), )) })?; std::fs::write(path.as_ref(), toml_content).map_err(|e| { ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration( format!("Failed to write config file {:?}: {}", path.as_ref(), e), )) })?; Ok(()) } } impl ConfigLoader for ControlCenterConfig { fn service_name() -> &'static str { "control-center" } fn load_from_hierarchy() -> std::result::Result> { let service = Self::service_name(); if let Some(path) = platform_config::resolve_config_path(service) { return Self::from_path(&path).map_err(|e| { Box::new(std::io::Error::other(e.to_string())) as Box }); } Ok(Self::default()) } fn apply_env_overrides( &mut self, ) -> std::result::Result<(), Box> { Self::apply_env_overrides(self).map_err(|e| { Box::new(std::io::Error::other(e.to_string())) as Box }) } fn from_path>( path: P, ) -> std::result::Result> { let path = path.as_ref(); let json_value = platform_config::format::load_config(path).map_err(|e| { let err: Box = Box::new(e); err })?; serde_json::from_value(json_value).map_err(|e| { let err_msg = format!( "Failed to deserialize control-center config from {:?}: {}", path, e ); Box::new(std::io::Error::new( std::io::ErrorKind::InvalidData, err_msg, )) as Box }) } }