1use crate::errors::{AuthError, Result};
13use config::{Config, Environment, File, FileFormat};
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::path::Path;
17
18#[derive(Debug, Clone)]
20pub struct ConfigManager {
21 config: Config,
23 sources: Vec<String>,
25 env_prefix: String,
27}
28
29#[derive(Debug, Clone)]
31pub struct ConfigBuilder {
32 sources: Vec<ConfigSource>,
34 env_prefix: String,
36 include_defaults: bool,
38 search_paths: Vec<String>,
40}
41
42#[derive(Debug, Clone)]
44pub enum ConfigSource {
45 File {
47 path: String,
48 format: FileFormat,
49 required: bool,
50 },
51 Environment { prefix: String },
53 Values(HashMap<String, config::Value>),
55 IncludeDir { path: String, pattern: String },
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61#[derive(Default)]
62pub struct AuthFrameworkSettings {
63 #[serde(flatten)]
65 pub auth: super::AuthConfig,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub threat_intelligence: Option<crate::threat_intelligence::ThreatIntelConfig>,
70
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub session: Option<SessionSettings>,
74
75 #[serde(flatten)]
77 pub custom: HashMap<String, serde_json::Value>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct SessionSettings {
83 pub max_concurrent_sessions: Option<u32>,
85
86 pub cleanup_interval: Option<u64>,
88
89 pub enable_device_tracking: Option<bool>,
91
92 pub cookie: Option<SessionCookieSettings>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct SessionCookieSettings {
99 pub name: Option<String>,
101
102 pub domain: Option<String>,
104
105 pub path: Option<String>,
107
108 pub max_age: Option<u64>,
110
111 pub http_only: Option<bool>,
113}
114
115impl Default for ConfigBuilder {
116 fn default() -> Self {
117 Self::new()
118 }
119}
120
121impl ConfigBuilder {
122 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 pub fn with_env_prefix(mut self, prefix: impl Into<String>) -> Self {
141 self.env_prefix = prefix.into();
142 self
143 }
144
145 pub fn without_defaults(mut self) -> Self {
147 self.include_defaults = false;
148 self
149 }
150
151 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 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 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 pub fn add_values(mut self, values: HashMap<String, config::Value>) -> Self {
189 self.sources.push(ConfigSource::Values(values));
190 self
191 }
192
193 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 pub fn add_search_path(mut self, path: impl Into<String>) -> Self {
204 self.search_paths.push(path.into());
205 self
206 }
207
208 pub fn build(self) -> Result<ConfigManager> {
210 let mut config = Config::builder();
211 let mut sources = Vec::new();
212
213 if self.include_defaults {
215 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 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 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 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 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 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, }
319 }
320}
321
322impl ConfigManager {
323 pub fn new() -> Result<Self> {
325 ConfigBuilder::new().build()
326 }
327
328 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 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 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 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 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 pub fn has_key(&self, key: &str) -> bool {
375 self.config.get::<config::Value>(key).is_ok()
376 }
377
378 pub fn get_keys_with_prefix(&self, _prefix: &str) -> Vec<String> {
380 Vec::new()
383 }
384
385 pub fn sources(&self) -> &[String] {
387 &self.sources
388 }
389
390 pub fn env_prefix(&self) -> &str {
392 &self.env_prefix
393 }
394
395 pub fn validate(&self) -> Result<()> {
397 let auth_config = self.get_auth_settings()?;
398 auth_config.auth.validate()
399 }
400
401 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 pub fn merge(self, other: ConfigManager) -> Result<ConfigManager> {
428 let mut sources = self.sources;
429 sources.extend(other.sources);
430
431 Ok(ConfigManager {
434 config: other.config,
435 sources,
436 env_prefix: other.env_prefix,
437 })
438 }
439
440 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), 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), http_only: Some(true),
482 }
483 }
484}
485
486pub trait ConfigIntegration {
488 fn auth_framework(&self) -> Option<&AuthFrameworkSettings>;
490
491 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 let settings = AuthFrameworkSettings::default();
514
515 assert!(!settings.auth.enable_multi_factor);
517 assert_eq!(settings.auth.token_lifetime.as_secs(), 3600); 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