Files
hermes-agent/tests/hermes_cli/test_dashboard_system_gateway_elevation.py
ethernet 1f886188b6 test(dashboard): elevation probes compare against the installation-bound runtime command
Detached dashboard actions spawn runtime_command(PROJECT_ROOT, subcommand); the
removed _dashboard_spawn_executable interpreter pick has no seam to patch.
2026-09-19 03:54:12 -04:00

98 lines
4.5 KiB
Python

"""Dashboard gateway lifecycle actions must elevate on a system-scope install.
Regression for #110820: the Restart Gateway button spawned ``hermes gateway restart`` as the
dashboard's own user, and the CLI refuses system-scope verbs below root, so the button could
never work on a systemd *system* install — the refusal only reached the action log while the
endpoint reported a started action.
"""
from __future__ import annotations
import subprocess
from unittest.mock import MagicMock, patch
import pytest
from hermes_cli._launchers import runtime_command
@pytest.fixture
def unprivileged(monkeypatch):
"""Run the predicate as a non-root user (CI containers can be root)."""
monkeypatch.setattr("os.geteuid", lambda: 1000)
@pytest.fixture
def system_scope_install(monkeypatch, tmp_path):
"""Only the SYSTEM unit is installed, the shape that needs root."""
system_unit = tmp_path / "etc" / "hermes-gateway.service"
system_unit.parent.mkdir(parents=True)
system_unit.write_text("[Service]\n", encoding="utf-8")
user_unit = tmp_path / "user" / "hermes-gateway.service"
monkeypatch.setattr(
"hermes_cli.gateway.get_systemd_unit_path",
lambda system=False: system_unit if system else user_unit,
)
return system_unit, user_unit
def _spawn(tmp_path, subcommand, *, sudo_ok: bool, targeted_only: bool = False, probes: list | None = None):
"""Run ``_spawn_hermes_action(subcommand)`` with Popen (and the ``sudo -n`` probes) faked;
the elevation decision runs through the production helpers. Returns the attempted argv.
``targeted_only`` models a command-scoped NOPASSWD sudoers entry: the blanket ``sudo -n true``
is refused but anything else under ``sudo -n`` is allowed."""
from hermes_cli import web_server_gateway
def run(argv, **_kwargs):
if probes is not None:
probes.append(list(argv))
refused = not sudo_ok or (targeted_only and argv[:3] == ["sudo", "-n", "true"])
return subprocess.CompletedProcess(argv, 1 if refused else 0)
child = MagicMock(spec=subprocess.Popen, pid=4242) # spec'd before Popen is patched
web_server_gateway._ACTION_LOG_FILES.setdefault("probe", "probe.log")
with patch.object(web_server_gateway, "_ACTION_LOG_DIR", tmp_path / "logs"), patch.object(
web_server_gateway.subprocess, "run", side_effect=run
), patch.object(web_server_gateway.subprocess, "Popen", return_value=child) as popen, patch(
"hermes_cli.web_server.PROJECT_ROOT", tmp_path
):
web_server_gateway._spawn_hermes_action(subcommand, "probe")
return popen.call_args.args[0]
@pytest.mark.parametrize(
"subcommand, both_units, elevated",
[
(["gateway", "restart"], False, True),
(["-p", "default", "gateway", "start"], False, True),
(["gateway", "status"], False, False), # no root gate on status
(["gateway", "restart"], True, False), # both units installed -> the CLI picks user scope
],
)
def test_only_system_scope_lifecycle_verbs_are_spawned_under_sudo(
unprivileged, system_scope_install, monkeypatch, tmp_path, subcommand, both_units, elevated
):
_system_unit, user_unit = system_scope_install
if both_units:
user_unit.parent.mkdir(parents=True)
user_unit.write_text("[Service]\n", encoding="utf-8")
monkeypatch.setattr("hermes_cli.web_server_profiles._resolve_profile_dir", lambda name: tmp_path / name)
argv = _spawn(tmp_path, subcommand, sudo_ok=True)
plain = runtime_command(tmp_path, subcommand)
assert argv == (["sudo", "-n", *plain] if elevated else plain)
def test_restart_without_passwordless_sudo_fails_the_request(unprivileged, system_scope_install, tmp_path):
# The endpoint reports Popen success; a child that can only refuse must fail the REQUEST instead.
with pytest.raises(RuntimeError, match="passwordless sudo is unavailable"):
_spawn(tmp_path, ["gateway", "restart"], sudo_ok=False)
def test_targeted_nopasswd_sudoers_still_elevates(unprivileged, system_scope_install, tmp_path):
# Same posture as the ``hermes update`` fleet restart: a refused blanket ``sudo -n true`` is
# inconclusive; the exact argv is then checked non-destructively (``sudo -n -l -- <argv>``).
probes: list = []
argv = _spawn(tmp_path, ["gateway", "restart"], sudo_ok=True, targeted_only=True, probes=probes)
plain = runtime_command(tmp_path, ["gateway", "restart"])
assert argv == ["sudo", "-n", *plain]
assert probes == [["sudo", "-n", "true"], ["sudo", "-n", "-l", "--", *plain]]