Skip to content

Conversation

@Kernel-Error
Copy link
Contributor

@Kernel-Error Kernel-Error commented Jan 13, 2026

Closes #24

Instead of just providing ifconfig wlan0 scan output, here's a complete implementation.

What this PR adds

Enterprise WPA2 (802.1X/EAP) authentication support:

  • New GUI dialog for EAP credentials (PEAP, TTLS, TLS methods)
  • Support for Phase2 authentication (MSCHAPV2, PAP, CHAP, MD5, GTC)
  • CA certificate selection
  • Client certificate and private key support for EAP-TLS
  • Anonymous identity for privacy
  • Enterprise network detection based on RSN capabilities

Changes

  • NetworkMgr/net_api.py: Added is_enterprise_network(), get_security_type(), generate_eap_config(), write_eap_config()
  • NetworkMgr/trayicon.py: Added EnterpriseAuthentication dialog class
  • NetworkMgr/setup-nic.py: Fixed wpa_supplicant.conf permissions (0o600)
  • NetworkMgr/wg_api.py: Fixed KeyError when WireGuard directory doesn't exist

Bug fixes included

  • Fixed caps_string index bug that caused PSK networks to be incorrectly detected as WEP

Testing

  • Tested with FreeRADIUS + hostapd test environment
  • EAP-PEAP/MSCHAPV2 authentication working
  • CA certificate validation working
  • PSK networks still work correctly

Summary by Sourcery

Add WPA-Enterprise (802.1X/EAP) Wi‑Fi support, improve wireless security configuration handling, and fix related permission and WireGuard configuration issues.

New Features:

  • Add Enterprise Wi‑Fi authentication dialog with support for multiple EAP methods, phase 2 methods, CA certificate selection, client certificates, and anonymous identities.
  • Detect enterprise wireless networks and classify security types from scan capabilities, generating appropriate wpa_supplicant EAP configuration blocks.

Bug Fixes:

  • Fix detection of WPA/WPA2-PSK vs WEP networks by using the correct capabilities string from scan results.
  • Ensure wpa_supplicant.conf is created and updated with secure permissions and correct handling when deleting SSID blocks.
  • Prevent missing WireGuard configuration directories from causing errors in wg_api.

Enhancements:

  • Preserve strict file permissions when appending or rewriting wpa_supplicant.conf to protect stored credentials.

Documentation:

  • Add documentation describing how to test Enterprise WPA using FreeRADIUS and hostapd on FreeBSD/GhostBSD.

Tests:

  • Add unit tests for enterprise network detection, security type classification, certificate validation, and EAP configuration generation.

- Add detection of enterprise networks in scan results
- Implement EnterpriseAuthentication dialog with support for:
  - EAP methods: PEAP, TTLS, TLS, LEAP, FAST, PWD
  - Phase 2 authentication: MSCHAPV2, GTC, PAP, CHAP, MD5
  - CA certificate selection
  - Client certificate/key for TLS
  - Anonymous identity support
- Generate proper wpa_supplicant.conf blocks for EAP
- Add certificate validation helpers
- Fix security issue: wpa_supplicant.conf permissions (0o765 -> 0o600)
- Add unit tests for enterprise WPA functions
- Add FreeRADIUS test setup documentation

Addresses: ghostbsd#24
FreeBSD ifconfig scan doesn't distinguish PSK from EAP explicitly.
Use heuristic: RSN without WPS and without typical consumer router
features (HTCAP, VHTCAP, ATH, WME) suggests enterprise AP.
The setup_wpa_supplicant function was using index 6 (short CAPS like
"EPS") instead of index 7 (full caps_string like "RSN HTCAP WME...").

Since "EPS" doesn't contain 'RSN' or 'WPA', networks were incorrectly
falling through to WEP configuration, causing authentication failures.
@Kernel-Error Kernel-Error requested review from a team as code owners January 13, 2026 10:21
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 13, 2026

Reviewer's Guide

Implements WPA-Enterprise (802.1X/EAP) Wi‑Fi support end‑to‑end, including network capability detection, EAP wpa_supplicant configuration generation, a new GTK dialog for enterprise credentials, more secure handling of wpa_supplicant.conf, a WireGuard configs bugfix, and test/docs coverage for the new behavior.

Sequence diagram for enterprise Wi‑Fi connection setup

sequenceDiagram
    actor User
    participant TrayIcon
    participant EnterpriseDialog
    participant NetApi
    participant WpaSupplicantConf as Wpa_supplicant_conf

    User->>TrayIcon: Click enterprise_ssid in menu
    TrayIcon->>TrayIcon: menu_click_enterprise(ssid_info, wificard)
    TrayIcon->>WpaSupplicantConf: Check for existing ssid network block
    alt Config_exists
        TrayIcon->>TrayIcon: connectToSsid(ssid, wificard)
    else No_config
        TrayIcon->>TrayIcon: EnterpriseAuthentication(ssid_info, wificard, failed=False)
        TrayIcon->>EnterpriseDialog: Show EAP credential form
        User->>EnterpriseDialog: Select EAP_method, phase2, CA_cert, credentials
        User->>EnterpriseDialog: Click Connect
        EnterpriseDialog->>TrayIcon: add_enterprise_to_wpa_supplicant(ssid_info, card)
        TrayIcon->>NetApi: write_eap_config(ssid, eap_config)
        NetApi->>WpaSupplicantConf: Append EAP network block (0600 perms)
        TrayIcon->>TrayIcon: try_to_connect_to_ssid(ssid, ssid_info, card)
    end
    TrayIcon-->>User: Updated connection status in tray
Loading

Updated class diagram for trayicon enterprise support and net_api helpers

classDiagram
    class TrayIcon {
        +ssid_menu_item(sn, caps, ssid, ssid_info, wificard)
        +menu_click_lock(widget, ssid_info, wificard)
        +menu_click_enterprise(widget, ssid_info, wificard)
        +disconnect_wifi(widget, wificard)
        +setup_wpa_supplicant(ssid, ssid_info, pwd, card)
        +Open_Wpa_Supplicant(ssid, card)
        +add_enterprise_to_wpa_supplicant(widget, ssid_info, card)
        +on_eap_method_changed(combo)
        +close_eap_window(widget)
        +on_eap_password_check(widget)
        +EnterpriseAuthentication(ssid_info, card, failed)
        -eap_window
        -eap_method_combo
        -phase2_combo
        -identity_entry
        -anon_identity_entry
        -eap_password
        -ca_cert_chooser
        -client_cert_chooser
        -private_key_chooser
        -private_key_passwd
        -client_cert_box
        -private_key_box
        -private_key_passwd_box
        -phase2_box
        -password_box
    }

    class NetApi {
        <<module>>
        +EAP_METHODS : list
        +PHASE2_METHODS : list
        +DEFAULT_CA_CERT : str
        +card_online(netcard)
        +barpercent(sn)
        +is_enterprise_network(caps_string)
        +get_security_type(caps_string)
        +validate_certificate(cert_path)
        +get_system_ca_certificates()
        +networkdictionary()
        +delete_ssid_wpa_supplicant_config(ssid)
        +generate_eap_config(ssid, eap_config)
        +write_eap_config(ssid, eap_config)
        +wait_inet(card)
    }

    class WpaSupplicantConf {
        <<file>>
        +path : "/etc/wpa_supplicant.conf"
        +mode : 0600
    }

    TrayIcon ..> NetApi : uses
    TrayIcon ..> WpaSupplicantConf : appends_network_blocks
    NetApi ..> WpaSupplicantConf : writes_EAP_config
Loading

File-Level Changes

Change Details Files
Add enterprise (802.1X/EAP) network detection and wpa_supplicant config generation utilities.
  • Introduce EAP_METHODS, PHASE2_METHODS, and DEFAULT_CA_CERT constants for WPA‑Enterprise support.
  • Add is_enterprise_network() and get_security_type() helpers and integrate them into networkdictionary() to enrich SSID scan info with security type and enterprise flag.
  • Add certificate helpers validate_certificate() and get_system_ca_certificates() to validate and locate CA bundles.
  • Add generate_eap_config() and write_eap_config() to produce and append WPA‑EAP network blocks to wpa_supplicant.conf with secure permissions.
NetworkMgr/net_api.py
Wire enterprise network metadata into the tray icon UI and add a GTK enterprise authentication dialog.
  • Extend ssid_menu_item() to detect enterprise networks from the enriched ssid_info structure and route clicks to a new enterprise handler.
  • Add menu_click_enterprise() to either connect using existing config or open enterprise auth dialog.
  • Fix PSK vs WEP detection by correctly deriving the caps_string from the extended ssid_info indexes.
  • Harden writes to /etc/wpa_supplicant.conf in setup_wpa_supplicant() and Open_Wpa_Supplicant() by using umask 0o077 and context managers.
  • Add EnterpriseAuthentication dialog, with EAP method and inner auth selection, username/anonymous identity, password visibility toggle, CA/client cert/private key choosers, and hook it to add_enterprise_to_wpa_supplicant() which delegates EAP config writing to net_api.write_eap_config().
NetworkMgr/trayicon.py
Tighten wpa_supplicant.conf creation and updates to use secure permissions.
  • Change initial chmod on /etc/wpa_supplicant.conf to 0o600 during setup.
  • Ensure delete_ssid_wpa_supplicant_config() rewrites /etc/wpa_supplicant.conf with umask 0o077 and chmod 0o600.
src/setup-nic.py
NetworkMgr/net_api.py
Fix WireGuard configuration dictionary construction bug.
  • Ensure maindictionary['configs'] is always populated outside the interface loop to avoid KeyError when the WireGuard directory is missing or empty.
NetworkMgr/wg_api.py
Document and test the new enterprise WPA behavior.
  • Add a detailed FreeRADIUS + hostapd test guide explaining how to set up a WPA2‑Enterprise testbed and how to exercise NetworkMgr and wpa_supplicant against it.
  • Add unit tests covering enterprise detection heuristics, security type classification, certificate validation, and EAP config generation, including constants coverage.
docs/TESTING_ENTERPRISE_WPA.md
tests/unit/test_enterprise_wpa.py

Assessment against linked issues

Issue Objective Addressed Explanation
#24 Add full WPA/WPA2-Enterprise (802.1X/EAP) Wi‑Fi support, including GUI workflow to configure enterprise credentials (EAP methods, phase2 methods, usernames/passwords) and RADIUS/certificate‑based authentication.
#24 Extend network scanning/handling logic so enterprise networks are detected, classified distinctly from PSK/open networks, and connected using appropriate EAP configuration and wpa_supplicant network blocks.
#24 Ensure secure handling of WPA configuration files (wpa_supplicant.conf), including correct permissions and support for storing certificate/key paths required for enterprise authentication.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 7 issues, and left some high level feedback:

  • In trayicon.py, the new uses of EAP_METHODS, PHASE2_METHODS, get_system_ca_certificates, and write_eap_config rely on these being imported into the module; consider adding explicit imports from net_api to avoid NameError at runtime.
  • menu_click_enterprise currently does a synchronous open('/etc/wpa_supplicant.conf').read() without a context manager or error handling on every click; consider guarding for missing/permission-denied files and avoiding a full blocking read (e.g., use a context manager and short-circuit search).
  • generate_eap_config directly interpolates user-provided values (ssid, identity, passwords, paths) into the wpa_supplicant.conf block without escaping quotes or special characters, which can break the config or be abused; consider sanitizing or escaping these strings before writing.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In trayicon.py, the new uses of EAP_METHODS, PHASE2_METHODS, get_system_ca_certificates, and write_eap_config rely on these being imported into the module; consider adding explicit imports from net_api to avoid NameError at runtime.
- menu_click_enterprise currently does a synchronous open('/etc/wpa_supplicant.conf').read() without a context manager or error handling on every click; consider guarding for missing/permission-denied files and avoiding a full blocking read (e.g., use a context manager and short-circuit search).
- generate_eap_config directly interpolates user-provided values (ssid, identity, passwords, paths) into the wpa_supplicant.conf block without escaping quotes or special characters, which can break the config or be abused; consider sanitizing or escaping these strings before writing.

## Individual Comments

### Comment 1
<location> `NetworkMgr/trayicon.py:249-250` </location>
<code_context>
             self.Authentication(ssid_info, wificard, False)
         self.updateinfo()

+    def menu_click_enterprise(self, widget, ssid_info, wificard):
+        if f'"{ssid_info[0]}"' in open('/etc/wpa_supplicant.conf').read():
+            connectToSsid(ssid_info[0], wificard)
+        else:
</code_context>

<issue_to_address>
**issue (bug_risk):** Directly opening and reading /etc/wpa_supplicant.conf without a context manager or error handling is fragile.

If `/etc/wpa_supplicant.conf` is missing or unreadable, this will raise and break the menu action, and it loads the whole file just to check membership. Please (1) use a context manager, (2) catch `FileNotFoundError`/IO errors and fail gracefully, and (3) consider reading line-by-line or a more structured lookup instead of loading the entire file on the UI thread.
</issue_to_address>

### Comment 2
<location> `NetworkMgr/net_api.py:476-485` </location>
<code_context>
+    return ws
+
+
+def write_eap_config(ssid, eap_config):
+    """
+    Write EAP configuration to wpa_supplicant.conf with secure permissions.
+    """
+    config = generate_eap_config(ssid, eap_config)
+    wpa_conf_path = '/etc/wpa_supplicant.conf'
+
+    # Write with restrictive permissions (owner read/write only)
+    old_umask = os.umask(0o077)
+    try:
+        with open(wpa_conf_path, 'a') as wsf:
+            wsf.write(config)
+    finally:
+        os.umask(old_umask)
+
+    # Ensure file permissions are correct
+    try:
+        os.chmod(wpa_conf_path, 0o600)
+    except PermissionError:
+        pass  # May need root, handled by sudoers
</code_context>

<issue_to_address>
**🚨 suggestion (security):** wpa_supplicant.conf permissions are only enforced here and in delete/setup paths, not after all writes.

`umask(0o077)` secures only newly created files. If `/etc/wpa_supplicant.conf` already exists with permissive permissions, the `setup_wpa_supplicant()` and `Open_Wpa_Supplicant()` paths won’t fix that, since they don’t call `chmod` like this function and `delete_ssid_wpa_supplicant_config()` do. Please also apply `os.chmod(wpa_conf_path, 0o600)` in those paths after writing to ensure existing configs are consistently locked down.

Suggested implementation:

```python
    ws += '\n}\n'
    return ws


def _ensure_wpa_conf_permissions(wpa_conf_path: str = '/etc/wpa_supplicant.conf') -> None:
    """
    Ensure wpa_supplicant.conf has restrictive permissions (owner read/write only).
    This is safe to call after any write to the file.
    """
    try:
        os.chmod(wpa_conf_path, 0o600)
    except PermissionError:
        # May need root; permission handling should be done by caller/sudoers
        pass


def write_eap_config(ssid, eap_config):

```

```python
    # Ensure file permissions are correct (even if file already existed)
    _ensure_wpa_conf_permissions(wpa_conf_path)

```

To fully implement your suggestion, you should also:
1. Locate the `setup_wpa_supplicant()` implementation (or similarly named function that initializes `/etc/wpa_supplicant.conf`) and, immediately after any write that touches `/etc/wpa_supplicant.conf`, call:
   ```python
   _ensure_wpa_conf_permissions('/etc/wpa_supplicant.conf')
   ```
2. Locate the `Open_Wpa_Supplicant()` path (or any function that updates `/etc/wpa_supplicant.conf` outside of `write_eap_config` and `delete_ssid_wpa_supplicant_config`) and likewise call:
   ```python
   _ensure_wpa_conf_permissions('/etc/wpa_supplicant.conf')
   ```
   right after the write completes.
3. If those functions write to `wpa_supplicant.conf` via a variable path (e.g., `wpa_conf_path`), pass that variable to `_ensure_wpa_conf_permissions` instead of the hard-coded string, to keep the behavior consistent with any future path changes.
</issue_to_address>

### Comment 3
<location> `NetworkMgr/trayicon.py:581-593` </location>
<code_context>
+        self.eap_window.add(main_box)
+
+        # Title
+        if failed:
+            title_text = _(f"{ssid_info[0]} Enterprise Authentication Failed")
+        else:
+            title_text = _(f"Enterprise Authentication for {ssid_info[0]}")
+        title_label = Gtk.Label()
+        title_label.set_markup(f"<b><span size='large'>{title_text}</span></b>")
</code_context>

<issue_to_address>
**suggestion:** Using f-strings inside _() breaks translation of these messages.

Wrapping an f-string in `_()` makes the entire interpolated text (including the SSID) the msgid, which translation tools can’t reuse. For strings with variables, keep a static msgid and interpolate afterward, e.g. `title_text = _('Enterprise Authentication for %s') % ssid_info[0]` (and similarly for the failure case), so the catalog entries stay stable and reusable.

```suggestion
        # Title
        if failed:
            title_text = _("%s Enterprise Authentication Failed") % ssid_info[0]
        else:
            title_text = _("Enterprise Authentication for %s") % ssid_info[0]
        title_label = Gtk.Label()
        title_label.set_markup(f"<b><span size='large'>{title_text}</span></b>")
        main_box.pack_start(title_label, False, False, 5)

        # Security info
        security_type = ssid_info[7] if len(ssid_info) > 7 else "WPA2-EAP"
        security_label = Gtk.Label(_("Security: %s") % security_type)
        main_box.pack_start(security_label, False, False, 0)
```
</issue_to_address>

### Comment 4
<location> `tests/unit/test_enterprise_wpa.py:70-73` </location>
<code_context>
+        """Test WPA-EAP detection."""
+        assert get_security_type("WPA EAP") == "WPA-EAP"
+
+    def test_get_security_type_wpa2_psk(self):
+        """Test WPA2-PSK detection."""
+        assert get_security_type("RSN HTCAP WME") == "WPA2-PSK"
+        assert get_security_type("RSN") == "WPA2-PSK"
+
+    def test_get_security_type_wpa_psk(self):
</code_context>

<issue_to_address>
**issue (testing):** The `"RSN"` case in `test_get_security_type_wpa2_psk` contradicts the documented enterprise detection heuristic and is likely an incorrect expectation.

Per the `is_enterprise_network` docstring, `"RSN"` without WPS/HTCAP/VHTCAP/ATH is classified as enterprise, so `get_security_type("RSN")` returns `"WPA2-EAP"`, not `"WPA2-PSK"`. This makes the test expectation inconsistent with the current heuristic and will fail.

Please either:
- Change the expectation to `"WPA2-EAP"` and add a brief comment noting that bare `RSN` is treated as enterprise, or
- Update the heuristic in `is_enterprise_network` if the intended behavior is different, and then align the tests accordingly.

The tests should clearly encode the intended classification of a minimal `"RSN"` beacon.
</issue_to_address>

### Comment 5
<location> `tests/unit/test_enterprise_wpa.py:15-23` </location>
<code_context>
+top_dir = str(Path(__file__).absolute().parent.parent.parent)
+sys.path.insert(0, top_dir)
+
+from NetworkMgr.net_api import (
+    EAP_METHODS,
+    PHASE2_METHODS,
+    DEFAULT_CA_CERT,
+    is_enterprise_network,
+    get_security_type,
+    validate_certificate,
+    get_system_ca_certificates,
+    generate_eap_config,
+)
+
</code_context>

<issue_to_address>
**suggestion (testing):** Add tests for `get_system_ca_certificates` and `write_eap_config`, especially around filesystem interactions and permissions.

The enterprise WPA logic depends on correct filesystem behavior and secure handling of `/etc/wpa_supplicant.conf` and system CA bundles, but there are no tests covering:

- `get_system_ca_certificates()`: returning only existing paths across the supported locations.
- `write_eap_config(...)`: appending the EAP config, using a restrictive umask, and setting `0o600` on the target file.

Please add tests that, for example:
- Monkeypatch `os.path.exists` (and optionally `os.path.isfile`) to simulate different CA bundle layouts and assert the returned paths.
- Monkeypatch `open`, `os.umask`, and `os.chmod` in `NetworkMgr.net_api` to write to a temp file and assert that:
  - The appended EAP block matches `generate_eap_config`.
  - `os.umask` is temporarily set to `0o077`.
  - `os.chmod` is called with `0o600` on the file.

This will verify the secure-storage behavior and help prevent regressions.

Suggested implementation:

```python
import os
import sys
import io
import tempfile
from pathlib import Path

import pytest

# Add project root to path
top_dir = str(Path(__file__).absolute().parent.parent.parent)
sys.path.insert(0, top_dir)

from NetworkMgr import net_api as net_api_module
from NetworkMgr.net_api import (
    EAP_METHODS,
    PHASE2_METHODS,
    DEFAULT_CA_CERT,
    get_system_ca_certificates,
    generate_eap_config,
    write_eap_config,
)


def test_get_system_ca_certificates_returns_empty_when_no_paths_exist(monkeypatch):
    """get_system_ca_certificates should return an empty list when no CA bundle paths exist."""
    def fake_exists(path):
        return False

    monkeypatch.setattr(net_api_module.os, "path", net_api_module.os.path)
    monkeypatch.setattr(net_api_module.os.path, "exists", fake_exists)

    result = get_system_ca_certificates()
    assert isinstance(result, list)
    assert result == []


def test_get_system_ca_certificates_returns_list_when_paths_exist(monkeypatch):
    """
    get_system_ca_certificates should return a list of CA bundle paths when they exist.

    We don't depend on specific paths, only that:
    - it returns a non-empty list when all probed paths "exist"
    - all returned entries are absolute paths.
    """
    def fake_exists(path):
        return True

    monkeypatch.setattr(net_api_module.os, "path", net_api_module.os.path)
    monkeypatch.setattr(net_api_module.os.path, "exists", fake_exists)

    result = get_system_ca_certificates()
    assert isinstance(result, list)
    # If the implementation ever has no configured paths this will fail,
    # which is fine because the logic would then be pointless.
    assert len(result) > 0
    assert all(os.path.isabs(p) for p in result)


def test_write_eap_config_secure_file_handling(monkeypatch):
    """
    write_eap_config should:
    - use generate_eap_config to build the EAP block
    - temporarily set umask to 0o077
    - call chmod with 0o600 on the target file
    - append the generated EAP config to the file.
    """
    # Capture the EAP config that write_eap_config attempts to write.
    generated_block = "network={\n    ssid=\"test-ssid\"\n}\n"
    captured_generate_args = {}

    def fake_generate_eap_config(*args, **kwargs):
        captured_generate_args["args"] = args
        captured_generate_args["kwargs"] = kwargs
        return generated_block

    monkeypatch.setattr(net_api_module, "generate_eap_config", fake_generate_eap_config)

    # Capture umask changes.
    umask_calls = []

    def fake_umask(mode):
        umask_calls.append(mode)
        # Simulate a previous umask value that will be restored.
        return 0o022

    monkeypatch.setattr(net_api_module.os, "umask", fake_umask)

    # Capture chmod calls.
    chmod_calls = []

    def fake_chmod(path, mode):
        chmod_calls.append((path, mode))

    monkeypatch.setattr(net_api_module.os, "chmod", fake_chmod)

    # Capture file writes without touching the real filesystem.
    written_content = {"data": ""}

    class DummyFile(io.StringIO):
        def __enter__(self):
            return self

        def __exit__(self, exc_type, exc, tb):
            # Do not suppress exceptions
            return False

        def write(self, s):
            written_content["data"] += s
            return len(s)

    def fake_open(*args, **kwargs):
        # We don't care about the filename or mode here; we just capture the content.
        return DummyFile()

    monkeypatch.setattr(net_api_module, "open", fake_open)

    # Call write_eap_config with dummy parameters; the exact signature is delegated
    # to the implementation, but we assume a typical EAP config signature.
    # Adjust arguments here if the real function signature differs.
    write_eap_config(
        ssid="test-ssid",
        identity="user@example.com",
        password="secret-password",
        ca_cert=DEFAULT_CA_CERT,
        eap_method=EAP_METHODS[0],
        phase2_method=PHASE2_METHODS[0],
    )

    # Assert umask was set to 0o077 at some point.
    assert 0o077 in umask_calls

    # Assert chmod was called with 0o600 on the WPA supplicant file.
    assert any(mode == 0o600 for _, mode in chmod_calls)

    # Assert that the generated EAP block was appended to the file.
    assert generated_block in written_content["data"]

```

These tests assume the following about `NetworkMgr.net_api`:

1. `get_system_ca_certificates()` uses `os.path.exists` on one or more configured CA bundle paths.
2. `write_eap_config(...)`:
   - Calls `generate_eap_config(...)` (imported in the same module) to build the EAP block.
   - Uses `os.umask` to set a restrictive `0o077` umask and restores the original value afterward.
   - Uses `os.chmod(path, 0o600)` on the WPA supplicant configuration file.
   - Opens the WPA supplicant configuration file via the module-level `open` (e.g. in append mode).

If the actual `write_eap_config` signature differs from the call in the test (e.g. different parameter names or order, or it accepts a configuration object instead of individual parameters), adjust the arguments in `test_write_eap_config_secure_file_handling` accordingly so they match the real function.

If `get_system_ca_certificates` or `write_eap_config` are implemented in a different module or under different names, update the imports and `monkeypatch.setattr` targets to match the real locations.
</issue_to_address>

### Comment 6
<location> `tests/unit/test_enterprise_wpa.py:110-114` </location>
<code_context>
+        assert is_valid is False
+        assert "not found" in error
+
+    def test_validate_certificate_directory(self):
+        """Test validation with directory instead of file."""
+        is_valid, error = validate_certificate("/tmp")
+        assert is_valid is False
+        assert "Not a file" in error
+
+    def test_validate_certificate_valid_file(self):
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding a test for `validate_certificate` where the file exists but is not readable, to cover the permission-error path.

Current tests cover empty/None inputs, non-existent paths, directories, and readable files. One implemented but untested path is when the file exists and is a regular file, but is not readable (`os.access(path, os.R_OK)` is `False`).

Please add a test that creates a temp file, revokes read permissions (or monkeypatches `os.access` to return `False` for that path), and asserts `validate_certificate(path)` returns `False` with an error containing "not readable". This will exercise the permission-check branch and verify the error message.
</issue_to_address>

### Comment 7
<location> `docs/TESTING_ENTERPRISE_WPA.md:9` </location>
<code_context>
+
+- FreeBSD/GhostBSD system
+- A wireless card that supports AP mode (for hostapd)
+- Another wireless card (or separate machine) for testing as client
+
+## Installation
</code_context>

<issue_to_address>
**nitpick (typo):** Consider adding an article: "for testing as a client".

You could revise this to: "Another wireless card (or separate machine) for testing as a client."

```suggestion
- Another wireless card (or separate machine) for testing as a client
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Kernel-Error and others added 2 commits January 13, 2026 11:36
- Add input escaping for wpa_supplicant.conf values (security)
- Use context manager and error handling for config file reads
- Fix i18n strings to use %s interpolation instead of f-strings
- Fix test expectation for bare RSN classification
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
@ericbsd ericbsd merged commit e222f08 into ghostbsd:master Jan 14, 2026
1 check passed
@ericbsd
Copy link
Member

ericbsd commented Jan 14, 2026

Thanks for the contribution!

@Kernel-Error Kernel-Error deleted the feature/enterprise-wpa-support branch January 14, 2026 11:49
@ericbsd ericbsd moved this from In Review to Done in Development Tracker Jan 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Feature] networkmgr enterprise wlan support

2 participants