Skip to main content

quorum_core/utility/
config.rs

1// config.rs (REFACTORED)
2//! Centralized configuration management.
3//!
4//! Configuration is loaded once at startup from an AES-256-GCM encrypted file (`secrets.enc`),
5//! decrypted using a passphrase the admin enters at the terminal. No `.env` files, no plaintext
6//! secrets on disk.
7//!
8//! The loaded config is stored in a global [`ArcSwap`] singleton, meaning:
9//! - All reads are atomic and lock-free via [`Config::get()`]
10//! - The config can be hot-reloaded at runtime via [`Config::reload()`] without restarting the server
11//! - Any reload is immediately visible to all subsequent [`Config::get()`] calls across all threads
12//!
13//! **All configuration fields are defined in `config_schema.rs`** using the `define_config_schema!` macro.
14//! This file now handles only the runtime logic (loading, reloading, parsing) — it never duplicates field definitions.
15
16use crate::utility::secrets::{SECRETS_BACKUP_PATH, SECRETS_PATH};
17use crate::utility::secrets::{
18    load_encrypted_config, prompt_passphrase, prompt_passphrase_new, run_setup,
19    save_encrypted_config, secrets_exist,
20};
21use crate::utility::std::{press_enter_to_continue, typewriter_println};
22use arc_swap::ArcSwap;
23use colored::Colorize;
24use std::path::Path;
25use std::sync::Arc;
26use std::sync::OnceLock;
27use zeroize::Zeroizing;
28
29// Import the generated types and functions from the schema macro
30use crate::utility::config_schema::{ConfigFields, parse_config_field};
31
32/// The global configuration singleton.
33static CONFIG: OnceLock<ArcSwap<ConfigFields>> = OnceLock::new();
34
35// ===================================================================
36// PUBLIC API (Config methods)
37// ===================================================================
38
39/// Server-wide configuration, loaded from the encrypted `secrets.enc` file at startup.
40pub struct Config;
41
42impl Config {
43    /// Loads configuration from `secrets.enc` and initializes the global singleton.
44    ///
45    /// On first run (no `secrets.enc` exists), walks the admin through the interactive setup
46    /// wizard, prompts for a passphrase, encrypts the config, saves it, then loads it.
47    ///
48    /// On subsequent runs, prompts for the passphrase and decrypts the existing file.
49    pub fn load() -> Result<(), Box<dyn std::error::Error>> {
50        if !Path::new(SECRETS_PATH).exists() && Path::new(SECRETS_BACKUP_PATH).exists() {
51            let backup_data = std::fs::read(SECRETS_BACKUP_PATH)
52                .map_err(|e| format!("Failed to read backup: {}", e))?;
53            std::fs::write(SECRETS_PATH, backup_data)
54                .map_err(|e| format!("Failed to restore from backup: {}", e))?;
55        }
56
57        if secrets_exist() {
58            if !cfg!(debug_assertions) {
59                println!();
60                typewriter_println(&format!(
61                    "{}",
62                    "Enter passphrase to unlock the server...".cyan().bold()
63                ))
64                .map_err(|e| e.to_string())?;
65
66                let passphrase = Zeroizing::new(prompt_passphrase()?);
67                Self::load_with_passphrase(&passphrase)
68            } else {
69                Self::load_with_passphrase("correct horse battery staple")
70            }
71        } else {
72            let config_fields = run_setup()?;
73
74            if !cfg!(debug_assertions) {
75                let passphrase = Zeroizing::new(prompt_passphrase_new()?);
76
77                println!();
78                typewriter_println(&format!(
79                    "{}",
80                    "Passphrase setup successfully!".cyan().bold()
81                ))
82                .map_err(|e| e.to_string())?;
83
84                press_enter_to_continue(true, true);
85                save_encrypted_config(&config_fields, &passphrase)?;
86            } else {
87                save_encrypted_config(&config_fields, "correct horse battery staple")?;
88            }
89
90            Self::load()
91        }
92    }
93
94    /// Reloads configuration from `secrets.enc` without restarting the server.
95    pub fn reload() -> Result<(), Box<dyn std::error::Error>> {
96        typewriter_println(&format!(
97            "{}",
98            "Enter passphrase to reload config...".cyan().bold()
99        ))
100        .map_err(|e| e.to_string())?;
101
102        let passphrase = prompt_passphrase()?;
103        let config_fields = load_encrypted_config(&passphrase)?;
104
105        CONFIG
106            .get()
107            .expect("Config not initialized. Call Config::load() first.")
108            .store(Arc::new(config_fields));
109
110        Ok(())
111    }
112
113    /// Loads config with a known passphrase (used during setup and testing).
114    pub fn load_with_passphrase(passphrase: &str) -> Result<(), Box<dyn std::error::Error>> {
115        let config_fields = load_encrypted_config(passphrase)
116            .map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
117
118        if let Some(existing) = CONFIG.get() {
119            existing.store(Arc::new(config_fields));
120            Ok(())
121        } else {
122            CONFIG
123                .set(ArcSwap::from_pointee(config_fields))
124                .map_err(|_| "Config already initialized".into())
125        }
126    }
127
128    /// Returns a snapshot of the current configuration.
129    pub fn get() -> arc_swap::Guard<Arc<ConfigFields>> {
130        CONFIG
131            .get()
132            .expect("Config not initialized. Call Config::load() first.")
133            .load()
134    }
135
136    /// Updates a single config field by key.
137    ///
138    /// # Arguments
139    /// * `config_key` - The name of the field to update (e.g., "server_port", "jwt_secret")
140    /// * `config_value` - The new value as a string (will be parsed to the field's type)
141    ///
142    /// # Error
143    /// Returns an error if:
144    /// - The key is unknown
145    /// - The value cannot be parsed to the field's type
146    /// - Config is not initialized
147    pub fn update(config_key: &str, config_value: &str) -> Result<(), Box<dyn std::error::Error>> {
148        let existing = CONFIG
149            .get()
150            .ok_or("Config not initialized. Call Config::load() first.")?;
151
152        let current = existing.load();
153        let mut new_fields = (**current).clone();
154
155        // Try to parse the field
156        match parse_config_field(&mut new_fields, config_key, config_value) {
157            Ok(true) => {
158                // Field was recognized and parsed successfully
159                save_encrypted_config(&new_fields, "correct horse battery staple")?;
160                existing.store(Arc::new(new_fields));
161                Ok(())
162            }
163            Ok(false) => {
164                // Field name not recognized
165                Err(format!("Unknown config key: {}", config_key).into())
166            }
167            Err(e) => {
168                // Parse failed
169                Err(e.into())
170            }
171        }
172    }
173}
174
175pub use crate::utility::config_schema::{CONFIG_SCHEMA, field_names};