FirstRunServicesWindow

1. Overview

The FirstRunServicesWindow module implements BastionGuard’s first-run “services wizard” UI. Its goal is to provide a guided, one-time configuration experience where the user selects which protection services should be enabled at login.

The module is implemented in GTKmm (GTK4) and orchestrates both:

  • User-scoped systemd units managed via systemctl --user
  • System-scoped systemd units managed via privileged pkexec systemctl

The wizard persists completion state using a flag file, and stores a structured JSON configuration snapshot for auditing and future UI defaults.


2. Persistent State and Files

2.1 First-Run Completion Flag

To ensure the wizard is shown only once, completion is tracked using a flag file:

~/.config/BastionGuard/first-run-services-done

If the flag exists, maybe_show() returns immediately and does not instantiate the window.


2.2 Configuration Snapshot (config.json)

After applying selections, the module writes a configuration snapshot to:

~/.config/BastionGuard/config.json

The file includes:

  • wizard_completed – boolean marker
  • installed_at – UTC timestamp (ISO-8601 format)
  • services – object mapping systemd unit name → boolean enabled state

3. Units and Privilege Model

3.1 System Units Set

The wizard maintains a static allowlist of system-level units:

SYSTEM_UNITS = {
  "BastionGuard-phishing-scanner.service",
  "BastionGuard-phishing-updater.service",
  "BastionGuard-phishing-updater.timer",
  "Bastionguard-privhelper.service",
  "bastionguard-sanesecurity.service",
  "bastionguard-sanesecurity.timer",
  "BastionGuard-ransomware-realtime.service",
  "BastionGuard-restart-on-graphical.service"
};

Membership is tested using is_system_unit(unit_name). Units in this set are managed via pkexec systemctl.


3.2 User Units

Units not present in SYSTEM_UNITS are treated as user-level services and are managed through:

systemctl --user ...

3.3 Privilege Separation

The module enforces a strict separation:

  • User service operations are executed without privilege escalation
  • System service operations require elevated authorization via pkexec

This ensures that only explicitly flagged system units can trigger privileged actions.


4. Command Execution Helpers

4.1 run_cmd(argv)

The module defines a robust command runner using Gio::Subprocess:

  • Supports optional stdout capture
  • Supports stderr silencing or piping
  • Waits synchronously for completion
  • Returns true when the subprocess completes successfully

This helper is used for both user systemctl operations and privileged wrappers.


4.2 pkexec_systemctl(args)

Privileged system unit management is executed through a dedicated wrapper:

pkexec systemctl <args...>

The wrapper constructs the full argument vector and delegates execution to run_cmd().


5. Directory Creation and Robust Flag Handling

5.1 ensure_config_dir()

Ensures the configuration directory exists:

~/.config/BastionGuard

If missing, the module creates it via make_directory_with_parents(). Exceptions are logged as diagnostics.


5.2 ensure_flag_dir()

Ensures the directory for the first-run flag exists. The path is the same user config directory, but a separate function is used to guarantee correctness and maintain separation of concerns.


5.3 touch_flag_file()

After the wizard is dismissed, the module writes the completion flag by truncating/creating:

~/.config/BastionGuard/first-run-services-done

This write is performed using an ofstream open with std::ios::trunc to ensure idempotent operation.


6. Window Lifecycle and One-Time Display

6.1 Construction and Initialization

The window constructor configures basic window properties and initializes all UI structures:

  • Modal window
  • Resizable
  • Default and minimum size constraints

Initialization sequence:

  1. build_headerbar()
  2. build_ui()
  3. load_current_states()

6.2 maybe_show()

The static entry point maybe_show() enforces one-time display semantics:

  • If the flag file exists → do not show the wizard
  • Otherwise → create a window instance and show it

The window connects to signal_hide() so that when it is hidden (Apply/Close), it:

  • Writes the completion flag
  • Deletes the window instance

7. Header Bar and Window Controls

7.1 Custom Header Bar

The wizard uses a custom header bar:

  • Default title buttons disabled
  • Decoration layout cleared
  • CSS class custom-headerbar applied

The title widget is a centered label:

"BastionGuard - Initial configuration"

(Localized via _().)


7.2 Custom Buttons

The header includes two custom buttons:

  • Minimize – loads icon-minimize.png
  • Close – loads icon-close.png

If icons cannot be loaded, diagnostics are logged and the UI remains functional.


8. Main UI Layout and Service Inventory

8.1 Layout Structure

The UI is composed of:

  • A left sidebar with branding
  • A right content container holding selection controls

The sidebar includes:

  • Logo loaded from logo-sidebar.png
  • Brand label “BastionGuard”
  • A vertical spacer to keep header content pinned

8.2 Services Grid

Services are rendered as a two-column grid:

  • Column 1: a check button with the service display label
  • Column 2: a wrapped description label with ellipsization

Each entry is represented by:

  • Systemd unit name (string)
  • Localized label (UI title)
  • Localized description (UI help text)

Check buttons are defaulted to active at creation time, then immediately overwritten by the real system state during load_current_states().


8.3 Wizard Actions

The footer contains three primary actions:

  • Minimal configuration – applies a curated allowlist of essential services
  • Enable all – sets all service toggles to active
  • Apply and continue – applies changes and closes the window

9. State Discovery and Synchronization

9.1 load_current_states()

On startup, the wizard queries the current enabled state of each unit and synchronizes the corresponding checkbox.

Logic:

  • If is_system_unit(unit) → call is_system_service_enabled()
  • Else → call is_user_service_enabled()

9.2 Enabled-State Query Semantics

Enabled state is determined using:

  • systemctl --user is-enabled <unit> for user units
  • systemctl is-enabled <unit> for system units

The module:

  • Captures stdout and stderr
  • Trims newline characters from stdout
  • Checks for the literal value enabled

If the command fails, the unit is treated as not enabled.


10. Selection Presets

10.1 select_all()

Sets all service check buttons to active.


10.2 select_minimal()

Applies a minimal configuration based on a curated set of unit names. Only those units in the minimal allowlist are enabled; all others are disabled.

The minimal set is defined in-code and can include both user and system units.


11. Applying Changes

11.1 apply_and_close()

When the user applies changes, the wizard:

  1. For each unit, computes the desired state (want) from the checkbox
  2. Queries the current enabled state (current)
  3. Skips units where want == current
  4. Otherwise enables/disables the unit using the correct privilege path
  5. Writes config.json snapshot
  6. Closes the window via hide()

This design minimizes systemctl calls and reduces privileged prompts to only the units requiring changes.


12. systemd Operations

12.1 User Units

Enable operation:

systemctl --user enable --now <unit>
systemctl --user start <unit>

Disable operation:

systemctl --user stop <unit>
systemctl --user disable --now <unit>

Explicit start and stop calls are executed for additional robustness even though enable --now and disable --now already imply activation changes.


12.2 System Units

Enable operation (privileged):

pkexec systemctl enable --now <unit>
pkexec systemctl start <unit>

Disable operation (privileged):

pkexec systemctl stop <unit>
pkexec systemctl disable --now <unit>

Stderr is not silenced for system operations to improve diagnostics during privileged failures.


13. Close Semantics and Event Handling

13.1 Header Close Button

The custom close button triggers apply_and_close(), ensuring changes are applied consistently even if the user closes via the header.


13.2 Close Request Handling

The window implements on_close_request() and also calls apply_and_close(), ensuring consistent persistence and service reconciliation on window close events.


13.3 Minimize Behavior

The minimize button hides the window conditionally based on GTKmm version guard:

  • For GTKmm ≥ 4.10, minimize action is implemented as hide()

14. Configuration Persistence

14.1 save_config()

The module writes a structured JSON document containing wizard completion metadata and service toggle states.

Timestamping is generated using UTC time:

Glib::DateTime::create_now_utc()

The JSON is saved with indentation:

config.dump(2)

On write failures, a diagnostic is logged.


15. Runtime and Security Considerations

  • Privilege separation: only units explicitly classified as system units invoke pkexec, reducing accidental privilege escalation
  • Allowlist-based privilege: system units are gated through a static allowlist (SYSTEM_UNITS) rather than dynamic naming
  • Operational safety: state reconciliation skips unchanged units, reducing systemctl churn and unnecessary prompts
  • One-time semantics: completion flag prevents repeated presentation while still allowing later adjustments through Settings
  • Auditability: config.json provides a stable snapshot of initial service enablement choices
  • Crash tolerance: directory creation uses “create with parents” and failure is logged without hard crash
  • Localization readiness: user-visible strings are wrapped via _()
  • Synchronous systemctl: systemctl calls are blocking; if large numbers of units are added, a progress UI or background execution may be required
  • Polkit UX: enabling/disabling system units may prompt for authorization; callers should ensure the wizard messaging prepares the user appropriately