1. Overview
The SettingsStore module is BastionGuard’s centralized, thread-safe persistence layer for user-scoped application settings and selected security allowlists. It provides:
- A JSON-backed settings database stored in the user’s configuration directory
- Non-destructive defaults merging (safe evolution across versions)
- Convenient typed getters/setters using dotted keys (e.g.,
security.secure_payments_enabled) - Atomic persistence with temporary files and rename semantics
- Specialized APIs for Secure Payments domain allowlists (
payments.json)
The module is designed to be callable from multiple threads. All operations are guarded by an internal mutex and use a lazy-loading cache to minimize disk access.
2. Storage Layout
2.1 Configuration Directory
All user settings are stored under:
~/.config/BastionGuard/
Paths are derived from the user home directory via Glib::get_home_dir().
2.2 Primary Settings File
The main settings file is:
~/.config/BastionGuard/settings.json
This file stores structured settings such as security toggles and UI preferences.
2.3 Secure Payments Allowlist Files
The Secure Payments domain allowlist uses two layers:
- System baseline:
/usr/share/BastionGuard/data/payments/payments.json - User override:
~/.config/BastionGuard/payments.json
The user file is ensured to exist via ensure_payments_json_exists(), either by copying the system baseline or by seeding a minimal default list.
3. Concurrency and Cache Model
SettingsStore maintains a process-wide JSON cache and state flags:
std::mutex mtx_– guards all store operationsbool loaded_– indicates whether the cache has been initializednlohmann::json cache_– in-memory settings representation
The cache is lazily initialized on first use through ensure_loaded_locked() and remains in memory until reload() or process termination.
4. Defaults and Non-destructive Merge
4.1 Default Schema
Default settings are defined in defaults(). The baseline structure includes:
security.secure_payments_enabled(boolean, default false)ui.language(string, defaultit_IT)
This default tree provides a stable schema foundation and a forward-compatible space for future options.
4.2 Merge Semantics (defaults + disk)
When loading, SettingsStore applies a non-destructive merge strategy:
- Initialize cache to defaults
- If
settings.jsonexists, load it and applymerge_patch()
This ensures:
- New defaults are automatically present after upgrades
- User-defined values overwrite defaults
- Unknown/extra keys from disk remain preserved
If settings.json is missing, the store treats it as first-run and does not create it automatically. If it is corrupt/unparseable, the store falls back to in-memory defaults.
5. Atomic Persistence
5.1 settings.json Writes
All writes use a two-phase atomic strategy in write_to_disk_locked():
- Write formatted JSON to
settings.json.tmp - Rename temporary file to
settings.json(atomic on same filesystem) - If rename fails, remove the target and retry rename
This reduces corruption risk under crashes or abrupt termination.
5.2 Flush and Reset
flush()forces the current cache to diskreset_to_defaults()resets the cache to defaults and persists itreload()clears cache state and re-reads from disk (defaults + merge)
6. Dotted-key Accessors
6.1 Key Parsing
Settings are addressed through dotted paths (e.g., security.secure_payments_enabled). Keys are split by . using split_dotted().
Node traversal and creation are handled by:
get_node_locked()– read-only traversal; returns null if path is missingget_or_create_node_locked()– ensures intermediate objects exist, returns parent of leaf
6.2 Typed Getters
SettingsStore provides typed getters with fallback defaults:
bool get_bool(key, fallback)int get_int(key, fallback)std::string get_string(key, fallback)
If the node does not exist or has an incompatible type, the fallback value is returned. Numeric conversion supports both integer and floating JSON numbers for get_int().
6.3 Typed Setters
Setters create missing object nodes as needed and persist immediately:
set_bool(key, value)set_int(key, value)set_string(key, value)
Each setter updates cache_ and then calls write_to_disk_locked(cache_), ensuring durable state after each mutation.
7. Feature-specific Public API
7.1 Secure Payments Toggle
SettingsStore exposes an explicit feature API for Secure Payments:
get_secure_payments_enabled()– returnssecurity.secure_payments_enabled(default false)set_secure_payments_enabled(enabled)– sets and persists the toggle
This API decouples callers from internal key naming and preserves a stable public contract.
8. Secure Payments Domain Allowlist Management
8.1 File Presence and Seeding
The user allowlist file is ensured to exist via:
void ensure_payments_json_exists()
Behavior:
- If
~/.config/BastionGuard/payments.jsonexists: no action - If missing and system baseline exists: copy system file into user file
- If copy fails or system baseline is absent: write a minimal seeded JSON object
The seed is generated by default_payments_seed() and includes metadata and a baseline list of major payment domains.
8.2 Domain Normalization
Domain strings are normalized consistently to improve matching reliability:
- Trim whitespace
- Lowercase conversion
- Strip leading dot
- Strip
www. - Strip port suffix
Normalization is implemented in normalize_domain().
8.3 Reading Payment Domains
Domain aggregation is provided by:
std::vector<std::string> get_payment_domains()
Behavior:
- Ensures the user payments file exists
- Loads and extracts domains from both system and user JSON files
- Supports JSON formats:
- Object with
listarray - Object with
domainsarray - Root-level array of strings
- Object with
- Deduplicates via
unordered_setand returns a sorted list
The extraction logic is implemented in extract_domains_from_payments_json().
8.4 Saving User Payment Domains
User allowlist persistence is performed via:
bool save_payment_domains_user(const std::vector<std::string>& domains)
This method:
- Normalizes and deduplicates domains
- Writes a JSON object containing:
versionupdated_at(static ISO date string)listarray
- Persists atomically via
write_json_atomic()
8.5 Resetting User Domains from System Baseline
The method:
bool reset_payment_domains_from_system()
restores the user file by copying the system baseline into ~/.config/BastionGuard/payments.json. If the system baseline does not exist, it returns false.
9. Security and Operational Considerations
- Thread safety: all public operations are mutex-guarded; callers can use SettingsStore from multiple threads.
- Durability model: setters persist immediately, reducing risk of losing settings on abnormal exit.
- Forward compatibility: defaults + merge_patch protects against missing keys after upgrades and preserves unknown keys.
- Atomic writes: temporary file + rename semantics reduces settings corruption risk.
- Corruption handling: invalid JSON on disk does not crash the application; defaults remain active in memory.
- Allowlist hygiene: Secure Payments security depends on the quality of
payments.jsonand correct normalization; downstream enforcement should continue to validate inputs and treat lists as configuration, not truth. - Metadata freshness:
updated_atis currently a static string in the seed and write paths; if consumers rely on this value, it should be updated programmatically to the current date at write time.
In summary, SettingsStore provides a robust JSON-backed configuration store for BastionGuard, with safe defaults merging, atomic persistence, and specialized support for Secure Payments domain allowlists.