auth_framework/config/
config_manager.rs

1//! Advanced configuration management using the `config` crate.
2//!
3//! This module provides flexible configuration loading from multiple sources:
4//! - Configuration files (TOML, YAML, JSON, RON, INI)
5//! - Environment variables
6//! - Command line arguments (when integrated with clap)
7//! - Include directives for modular configuration files
8//!
9//! The configuration system is designed to be easily integrated into parent applications
10//! while providing sensible defaults for standalone use.
11
12use crate::errors::{AuthError, Result};
13use config::{Config, Environment, File, FileFormat};
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::path::Path;
17
18/// Comprehensive configuration manager for the auth framework
19#[derive(Debug, Clone)]
20pub struct ConfigManager {
21    /// The underlying config builder
22    config: Config,
23    /// Configuration source paths for reference
24    sources: Vec<String>,
25    /// Environment variable prefix
26    env_prefix: String,
27}
28
29/// Configuration builder for easy integration into parent applications
30#[derive(Debug, Clone)]
31pub struct ConfigBuilder {
32    /// Configuration sources in order of priority (later sources override earlier ones)
33    sources: Vec<ConfigSource>,
34    /// Environment variable prefix
35    env_prefix: String,
36    /// Whether to include default auth-framework config files
37    include_defaults: bool,
38    /// Custom configuration file search paths
39    search_paths: Vec<String>,
40}
41
42/// Represents a configuration source
43#[derive(Debug, Clone)]
44pub enum ConfigSource {
45    /// Configuration file (path, format, required)
46    File {
47        path: String,
48        format: FileFormat,
49        required: bool,
50    },
51    /// Environment variables with prefix
52    Environment { prefix: String },
53    /// Direct configuration values
54    Values(HashMap<String, config::Value>),
55    /// Include another configuration directory
56    IncludeDir { path: String, pattern: String },
57}
58
59/// Settings that can be used by parent applications to configure auth-framework
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[derive(Default)]
62pub struct AuthFrameworkSettings {
63    /// Main auth framework configuration
64    #[serde(flatten)]
65    pub auth: super::AuthConfig,
66
67    /// Threat intelligence configuration
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub threat_intelligence: Option<crate::threat_intelligence::ThreatIntelConfig>,
70
71    /// Session configuration
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub session: Option<SessionSettings>,
74
75    /// Additional custom settings for extensibility
76    #[serde(flatten)]
77    pub custom: HashMap<String, serde_json::Value>,
78}
79
80/// Session-specific configuration settings
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct SessionSettings {
83    /// Maximum number of concurrent sessions per user
84    pub max_concurrent_sessions: Option<u32>,
85
86    /// Session cleanup interval in seconds
87    pub cleanup_interval: Option<u64>,
88
89    /// Enable session device tracking
90    pub enable_device_tracking: Option<bool>,
91
92    /// Session cookie settings
93    pub cookie: Option<SessionCookieSettings>,
94}
95
96/// Session cookie configuration
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct SessionCookieSettings {
99    /// Cookie name
100    pub name: Option<String>,
101
102    /// Cookie domain
103    pub domain: Option<String>,
104
105    /// Cookie path
106    pub path: Option<String>,
107
108    /// Cookie max age in seconds
109    pub max_age: Option<u64>,
110
111    /// Whether cookie is HTTP only
112    pub http_only: Option<bool>,
113}
114
115impl Default for ConfigBuilder {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121impl ConfigBuilder {
122    /// Create a new configuration builder with sensible defaults
123    pub fn new() -> Self {
124        Self {
125            sources: Vec::new(),
126            env_prefix: "AUTH_FRAMEWORK".to_string(),
127            include_defaults: true,
128            search_paths: vec![
129                ".".to_string(),
130                "./config".to_string(),
131                "/etc/auth-framework".to_string(),
132                dirs::config_dir()
133                    .map(|d| d.join("auth-framework").to_string_lossy().to_string())
134                    .unwrap_or_else(|| "./config".to_string()),
135            ],
136        }
137    }
138
139    /// Set the environment variable prefix
140    pub fn with_env_prefix(mut self, prefix: impl Into<String>) -> Self {
141        self.env_prefix = prefix.into();
142        self
143    }
144
145    /// Disable loading of default auth-framework configuration files
146    pub fn without_defaults(mut self) -> Self {
147        self.include_defaults = false;
148        self
149    }
150
151    /// Add a configuration file source
152    pub fn add_file<P: AsRef<Path>>(mut self, path: P, required: bool) -> Self {
153        let path_str = path.as_ref().to_string_lossy().to_string();
154        let format = Self::detect_format(&path_str);
155
156        self.sources.push(ConfigSource::File {
157            path: path_str,
158            format,
159            required,
160        });
161        self
162    }
163
164    /// Add a configuration file with explicit format
165    pub fn add_file_with_format<P: AsRef<Path>>(
166        mut self,
167        path: P,
168        format: FileFormat,
169        required: bool,
170    ) -> Self {
171        self.sources.push(ConfigSource::File {
172            path: path.as_ref().to_string_lossy().to_string(),
173            format,
174            required,
175        });
176        self
177    }
178
179    /// Add environment variables as a source
180    pub fn add_env_source(mut self, prefix: impl Into<String>) -> Self {
181        self.sources.push(ConfigSource::Environment {
182            prefix: prefix.into(),
183        });
184        self
185    }
186
187    /// Add direct configuration values
188    pub fn add_values(mut self, values: HashMap<String, config::Value>) -> Self {
189        self.sources.push(ConfigSource::Values(values));
190        self
191    }
192
193    /// Add a directory include source (loads all matching files)
194    pub fn add_include_dir(mut self, path: impl Into<String>, pattern: impl Into<String>) -> Self {
195        self.sources.push(ConfigSource::IncludeDir {
196            path: path.into(),
197            pattern: pattern.into(),
198        });
199        self
200    }
201
202    /// Add a search path for configuration files
203    pub fn add_search_path(mut self, path: impl Into<String>) -> Self {
204        self.search_paths.push(path.into());
205        self
206    }
207
208    /// Build the configuration manager
209    pub fn build(self) -> Result<ConfigManager> {
210        let mut config = Config::builder();
211        let mut sources = Vec::new();
212
213        // Add default auth-framework configuration files if requested
214        if self.include_defaults {
215            // Look for auth-framework configuration files in search paths
216            for search_path in &self.search_paths {
217                for filename in &[
218                    "auth-framework.toml",
219                    "auth-framework.yaml",
220                    "auth-framework.yml",
221                    "auth-framework.json",
222                    "auth.toml",
223                    "auth.yaml",
224                    "auth.yml",
225                    "auth.json",
226                ] {
227                    let path = Path::new(search_path).join(filename);
228                    if path.exists() {
229                        let format = Self::detect_format(&path.to_string_lossy());
230                        config = config
231                            .add_source(File::from(path.clone()).format(format).required(false));
232                        sources.push(path.to_string_lossy().to_string());
233                    }
234                }
235            }
236        }
237
238        // Add user-specified sources in order
239        for source in self.sources {
240            match source {
241                ConfigSource::File {
242                    path,
243                    format,
244                    required,
245                } => {
246                    config = config.add_source(File::new(&path, format).required(required));
247                    sources.push(path);
248                }
249                ConfigSource::Environment { prefix } => {
250                    config = config.add_source(
251                        Environment::with_prefix(&prefix)
252                            .prefix_separator("_")
253                            .separator("__"),
254                    );
255                    sources.push(format!("env:{}", prefix));
256                }
257                ConfigSource::Values(values) => {
258                    for (key, value) in values {
259                        config = config.set_override(&key, value).map_err(|e| {
260                            AuthError::config(format!("Failed to set override: {e}"))
261                        })?;
262                    }
263                    sources.push("values:override".to_string());
264                }
265                ConfigSource::IncludeDir { path, pattern } => {
266                    // Load all matching files from the directory
267                    if let Ok(entries) = std::fs::read_dir(&path) {
268                        let mut files: Vec<_> = entries
269                            .filter_map(|entry| entry.ok())
270                            .filter(|entry| entry.file_name().to_string_lossy().contains(&pattern))
271                            .collect();
272
273                        // Sort for consistent loading order
274                        files.sort_by_key(|e| e.file_name());
275
276                        for entry in files {
277                            let file_path = entry.path();
278                            let format = Self::detect_format(&file_path.to_string_lossy());
279                            config = config.add_source(
280                                File::from(file_path.clone()).format(format).required(false),
281                            );
282                            sources.push(file_path.to_string_lossy().to_string());
283                        }
284                    }
285                }
286            }
287        }
288
289        // Always add the main environment source last (highest priority)
290        config = config.add_source(
291            Environment::with_prefix(&self.env_prefix)
292                .prefix_separator("_")
293                .separator("__"),
294        );
295        sources.push(format!("env:{}", self.env_prefix));
296
297        let built_config = config
298            .build()
299            .map_err(|e| AuthError::config(format!("Failed to build configuration: {e}")))?;
300
301        Ok(ConfigManager {
302            config: built_config,
303            sources,
304            env_prefix: self.env_prefix,
305        })
306    }
307
308    /// Detect file format from extension
309    fn detect_format(path: &str) -> FileFormat {
310        let path = Path::new(path);
311        match path.extension().and_then(|s| s.to_str()) {
312            Some("toml") => FileFormat::Toml,
313            Some("yaml") | Some("yml") => FileFormat::Yaml,
314            Some("json") => FileFormat::Json,
315            Some("ron") => FileFormat::Ron,
316            Some("ini") => FileFormat::Ini,
317            _ => FileFormat::Toml, // Default to TOML
318        }
319    }
320}
321
322impl ConfigManager {
323    /// Create a new configuration manager with default settings
324    pub fn new() -> Result<Self> {
325        ConfigBuilder::new().build()
326    }
327
328    /// Create a configuration manager for a specific application
329    pub fn for_application(app_name: &str) -> Result<Self> {
330        ConfigBuilder::new()
331            .with_env_prefix(format!("{}_AUTH_FRAMEWORK", app_name.to_uppercase()))
332            .add_file(format!("{}.toml", app_name), false)
333            .add_file(format!("config/{}.toml", app_name), false)
334            .build()
335    }
336
337    /// Get the auth framework settings
338    pub fn get_auth_settings(&self) -> Result<AuthFrameworkSettings> {
339        self.config
340            .clone()
341            .try_deserialize::<AuthFrameworkSettings>()
342            .map_err(|e| AuthError::config(format!("Failed to deserialize auth settings: {e}")))
343    }
344
345    /// Get a specific configuration section
346    pub fn get_section<T>(&self, section: &str) -> Result<T>
347    where
348        T: for<'de> Deserialize<'de>,
349    {
350        self.config
351            .get::<T>(section)
352            .map_err(|e| AuthError::config(format!("Failed to get section '{}': {e}", section)))
353    }
354
355    /// Get a configuration value by key
356    pub fn get<T>(&self, key: &str) -> Result<T>
357    where
358        T: for<'de> Deserialize<'de>,
359    {
360        self.config
361            .get::<T>(key)
362            .map_err(|e| AuthError::config(format!("Failed to get key '{}': {e}", key)))
363    }
364
365    /// Get a configuration value with a default
366    pub fn get_or_default<T>(&self, key: &str, default: T) -> T
367    where
368        T: for<'de> Deserialize<'de>,
369    {
370        self.config.get::<T>(key).unwrap_or(default)
371    }
372
373    /// Check if a key exists in the configuration
374    pub fn has_key(&self, key: &str) -> bool {
375        self.config.get::<config::Value>(key).is_ok()
376    }
377
378    /// Get all keys with a specific prefix
379    pub fn get_keys_with_prefix(&self, _prefix: &str) -> Vec<String> {
380        // This would require access to the internal structure
381        // For now, we'll provide a simplified implementation
382        Vec::new()
383    }
384
385    /// Get configuration sources used
386    pub fn sources(&self) -> &[String] {
387        &self.sources
388    }
389
390    /// Get the environment prefix
391    pub fn env_prefix(&self) -> &str {
392        &self.env_prefix
393    }
394
395    /// Validate the configuration
396    pub fn validate(&self) -> Result<()> {
397        let auth_config = self.get_auth_settings()?;
398        auth_config.auth.validate()
399    }
400
401    /// Create a nested configuration manager for a subsection
402    pub fn section(&self, section: &str) -> Result<ConfigManager> {
403        let section_config = self
404            .config
405            .get::<HashMap<String, config::Value>>(section)
406            .map_err(|e| AuthError::config(format!("Failed to get section '{}': {e}", section)))?;
407
408        let mut config_builder = Config::builder();
409        for (key, value) in section_config {
410            config_builder = config_builder
411                .set_override(&key, value)
412                .map_err(|e| AuthError::config(format!("Failed to set override: {e}")))?;
413        }
414
415        let built_config = config_builder
416            .build()
417            .map_err(|e| AuthError::config(format!("Failed to build section config: {e}")))?;
418
419        Ok(ConfigManager {
420            config: built_config,
421            sources: vec![format!("section:{}", section)],
422            env_prefix: format!("{}_{}", self.env_prefix, section.to_uppercase()),
423        })
424    }
425
426    /// Merge with another configuration (other takes precedence)
427    pub fn merge(self, other: ConfigManager) -> Result<ConfigManager> {
428        let mut sources = self.sources;
429        sources.extend(other.sources);
430
431        // For simplicity, we'll use the other's config as the primary
432        // In a real implementation, we'd properly merge the configurations
433        Ok(ConfigManager {
434            config: other.config,
435            sources,
436            env_prefix: other.env_prefix,
437        })
438    }
439
440    /// Export the current configuration to a specific format
441    pub fn export_to_string(&self, format: FileFormat) -> Result<String> {
442        let settings = self.get_auth_settings()?;
443
444        match format {
445            FileFormat::Toml => toml::to_string_pretty(&settings)
446                .map_err(|e| AuthError::config(format!("Failed to serialize to TOML: {e}"))),
447            FileFormat::Yaml => serde_yaml::to_string(&settings)
448                .map_err(|e| AuthError::config(format!("Failed to serialize to YAML: {e}"))),
449            FileFormat::Json => serde_json::to_string_pretty(&settings)
450                .map_err(|e| AuthError::config(format!("Failed to serialize to JSON: {e}"))),
451            _ => Err(AuthError::config("Unsupported export format")),
452        }
453    }
454}
455
456impl Default for ConfigManager {
457    fn default() -> Self {
458        Self::new().expect("Failed to create default configuration manager")
459    }
460}
461
462
463impl Default for SessionSettings {
464    fn default() -> Self {
465        Self {
466            max_concurrent_sessions: Some(5),
467            cleanup_interval: Some(3600), // 1 hour
468            enable_device_tracking: Some(true),
469            cookie: Some(SessionCookieSettings::default()),
470        }
471    }
472}
473
474impl Default for SessionCookieSettings {
475    fn default() -> Self {
476        Self {
477            name: Some("auth_session".to_string()),
478            domain: None,
479            path: Some("/".to_string()),
480            max_age: Some(86400), // 24 hours
481            http_only: Some(true),
482        }
483    }
484}
485
486/// Helper trait for easy integration into parent application configurations
487pub trait ConfigIntegration {
488    /// Get the auth framework configuration section
489    fn auth_framework(&self) -> Option<&AuthFrameworkSettings>;
490
491    /// Get the auth framework configuration section (mutable)
492    fn auth_framework_mut(&mut self) -> Option<&mut AuthFrameworkSettings>;
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498
499    #[test]
500    fn test_config_builder_basic() {
501        let config = ConfigBuilder::new()
502            .with_env_prefix("TEST")
503            .build()
504            .expect("Failed to build config");
505
506        assert_eq!(config.env_prefix(), "TEST");
507    }
508
509    #[test]
510    fn test_config_manager_default() {
511        // Since ConfigManager::new() tries to load from files/env which may not exist,
512        // we'll test the default settings directly instead
513        let settings = AuthFrameworkSettings::default();
514
515        // Should have default values
516        assert!(!settings.auth.enable_multi_factor);
517        assert_eq!(settings.auth.token_lifetime.as_secs(), 3600); // 1 hour
518        assert_eq!(settings.auth.issuer, "auth-framework");
519    }
520
521    #[test]
522    fn test_application_specific_config() {
523        let config = ConfigManager::for_application("myapp").expect("Failed to create app config");
524
525        assert_eq!(config.env_prefix(), "MYAPP_AUTH_FRAMEWORK");
526    }
527
528    #[test]
529    fn test_config_sources() {
530        let config = ConfigBuilder::new()
531            .add_file("nonexistent.toml", false)
532            .add_env_source("TEST")
533            .build()
534            .expect("Failed to build config");
535
536        let sources = config.sources();
537        assert!(!sources.is_empty());
538    }
539}
540
541