447 lines
16 KiB
Rust
Raw Normal View History

use crate::error::{ControlCenterError, Result, infrastructure};
2025-10-07 10:59:52 +01:00
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
2025-10-07 10:59:52 +01:00
use std::time::Duration;
use tracing::info;
use platform_config::ConfigLoader;
2025-10-07 10:59:52 +01:00
#[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<String>,
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<String>,
pub password: Option<String>,
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<String>,
pub post_execution: Vec<String>,
pub on_policy_violation: Vec<String>,
}
#[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<String>,
pub schedule_cron: Option<String>,
pub email_notifications: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnomalyConfig {
pub enabled: bool,
pub baseline_window_hours: u64,
pub detection_threshold: f64,
pub statistical_methods: Vec<String>,
pub alert_channels: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
pub level: String,
pub file_path: Option<PathBuf>,
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<Self> {
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<Self> {
// 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(())
}
2025-10-07 10:59:52 +01:00
/// Load configuration from file with environment variable interpolation
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = std::fs::read_to_string(path.as_ref())
.map_err(|e| ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
2025-10-07 10:59:52 +01:00
format!("Failed to read config file {:?}: {}", path.as_ref(), e)
)))?;
2025-10-07 10:59:52 +01:00
// 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(
2025-10-07 10:59:52 +01:00
format!("Failed to parse config: {}", e)
)))?;
2025-10-07 10:59:52 +01:00
config.validate()?;
Ok(config)
}
/// Interpolate environment variables in configuration
fn interpolate_env_vars(content: &str) -> Result<String> {
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(
2025-10-07 10:59:52 +01:00
format!("Invalid regex pattern: {}", e)
)))?;
2025-10-07 10:59:52 +01:00
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(
2025-10-07 10:59:52 +01:00
"Server port cannot be 0".to_string()
)));
2025-10-07 10:59:52 +01:00
}
// Validate policy directories exist
if !self.policies.policy_dir.exists() {
return Err(ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
2025-10-07 10:59:52 +01:00
format!("Policy directory does not exist: {:?}", self.policies.policy_dir)
)));
2025-10-07 10:59:52 +01:00
}
// 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(
2025-10-07 10:59:52 +01:00
"JWT secret must be at least 32 characters".to_string()
)));
2025-10-07 10:59:52 +01:00
}
// Validate password policy
if self.auth.password_policy.min_length < 8 {
return Err(ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
2025-10-07 10:59:52 +01:00
"Password minimum length must be at least 8 characters".to_string()
)));
2025-10-07 10:59:52 +01:00
}
Ok(())
}
/// Get configuration as JSON string
pub fn to_json(&self) -> Result<String> {
serde_json::to_string_pretty(self)
.map_err(|e| ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
2025-10-07 10:59:52 +01:00
format!("Failed to serialize config to JSON: {}", e)
)))
2025-10-07 10:59:52 +01:00
}
/// Create a default configuration file
pub fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
let config = Self::default();
let toml_content = toml::to_string_pretty(&config)
.map_err(|e| ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
2025-10-07 10:59:52 +01:00
format!("Failed to serialize default config: {}", e)
)))?;
2025-10-07 10:59:52 +01:00
std::fs::write(path.as_ref(), toml_content)
.map_err(|e| ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
2025-10-07 10:59:52 +01:00
format!("Failed to write config file {:?}: {}", path.as_ref(), e)
)))?;
2025-10-07 10:59:52 +01:00
Ok(())
}
}
impl ConfigLoader for ControlCenterConfig {
fn service_name() -> &'static str {
"control-center"
}
fn load_from_hierarchy() -> std::result::Result<Self, Box<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>);
}
Ok(Self::default())
}
fn apply_env_overrides(&mut self) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
Self::apply_env_overrides(self)
.map_err(|e| Box::new(std::io::Error::other(e.to_string())) as Box<dyn std::error::Error + Send + Sync>)
}
fn from_path<P: AsRef<Path>>(path: P) -> std::result::Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let path = path.as_ref();
let json_value = platform_config::format::load_config(path)
.map_err(|e| {
let err: Box<dyn std::error::Error + Send + Sync> = 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<dyn std::error::Error + Send + Sync>
})
}
2025-10-07 10:59:52 +01:00
}