SettingsStore

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 operations
  • bool loaded_ – indicates whether the cache has been initialized
  • nlohmann::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, default it_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:

  1. Initialize cache to defaults
  2. If settings.json exists, load it and apply merge_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():

  1. Write formatted JSON to settings.json.tmp
  2. Rename temporary file to settings.json (atomic on same filesystem)
  3. 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 disk
  • reset_to_defaults() resets the cache to defaults and persists it
  • reload() 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 missing
  • get_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() – returns security.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.json exists: 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 list array
    • Object with domains array
    • Root-level array of strings
  • Deduplicates via unordered_set and 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:
    • version
    • updated_at (static ISO date string)
    • list array
  • 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.json and correct normalization; downstream enforcement should continue to validate inputs and treat lists as configuration, not truth.
  • Metadata freshness: updated_at is 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.