feat(web): service management panel over a sudo allow-list (stage D)

Adds operations/services.py: a fixed ManagedService allow-list (act-runner, act-runner-rust, vmware-host, ci-web) with find_service/service_states/control_service. control_service builds an argument list only (never shell=True), validates the action against ALLOWED_ACTIONS (ValueError->400), refuses stop on restart-only services (ci-web, allow_stop=False) without building a command, and defers ci-web's own restart via 'sudo -n systemd-run --on-active=2 systemctl restart ci-web.service' so the HTTP response returns first.

Route POST /services/{service_id}/{action} in web/routes.py mirrors vms_remove: require_user + require_csrf + two-step confirm gate + one audit entry per stage; unknown id / bad action / disallowed-stop short-circuit to 400 before any subprocess or audit; the vmware-host confirm step warns with the active-VM count. New /services read partial with state-aware Start/Stop/Restart buttons, polled every 15s.

deploy/sudoers.d/ci-web grants ci-runner exact-command NOPASSWD for exactly these nine systemctl lines plus the systemd-run deferral — no wildcards, no bare systemctl, no ci-web stop; setup-web-linux.sh installs it mode 0440 and validates with 'visudo -cf', rolling back on failure. Config gains [web].managed_services override; docs/WEB-UI.md documents the panel + security model.

Gates: 1044 tests pass (46 new security/unit tests), coverage 94.20%, ruff + mypy --strict clean. Not yet deployed: sudoers install + prod-venv reinstall + ci-web restart are a human privilege-boundary step.
This commit is contained in:
2026-07-24 12:11:36 +02:00
parent 9d7bbee35d
commit e56eae95c7
16 changed files with 1828 additions and 20 deletions
+22
View File
@@ -107,3 +107,25 @@ retention_days = 30
# redirect_url = "http://192.168.1.50:8800/auth/callback"
# client_secret_cred = "WebOidcClientSecret" # keyring KEY NAME, not the secret
# scopes = ["openid", "profile"]
#
# Optional: override the default systemd-unit allow-list the dashboard's
# service-management panel (Stage D) is allowed to start/stop/restart. Leave
# unset to use the built-in default (act-runner, act-runner-rust,
# vmware-host, ci-web -- see operations/services.py::MANAGED_SERVICES).
# A malformed row (missing/wrong-typed key) is skipped, never crashes
# startup. Every command reaching sudo is an exact-match systemctl/systemd-run
# invocation approved in deploy/sudoers.d/ci-web -- adding a unit here without
# also adding its NOPASSWD entries there just means every action for it fails
# (sudo prompts for a password that is never supplied, exit non-zero).
#
# [[web.managed_services]]
# id = "act-runner"
# unit = "act-runner.service"
# label = "Runner"
# allow_stop = true
#
# [[web.managed_services]]
# id = "ci-web"
# unit = "ci-web.service"
# label = "Web UI"
# allow_stop = false # restart-only -- stopping it would lock the operator out