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
+10 -10
View File
@@ -1,6 +1,6 @@
# Web UI Redesign — Implementation Plan (Phase 1)
**Status:** Stages A, B, C complete + gates approved (989 tests pass, cov 94.12%, ruff+mypy clean). Stage D (service management, SECURITY-CRITICAL) not started. Uncommitted.
**Status:** Stages A–D COMPLETE + all gates approved (1044 tests pass, cov 94.20%, ruff+mypy clean). Committed on branch `feat/web-ui-redesign`. **Remaining: human deploy of Stage D** — `pip install .` into prod venv, install `deploy/sudoers.d/ci-web` (`visudo -c`), restart `ci-web.service` (privilege-boundary change, not auto-applied).
**Owner / executor:** Sonnet (implementation) under Opus supervision (review gate after every stage).
**Scope:** Presentation redesign + new read-only panels + service management. **No** time-series store, **no** light theme, **no** rebuild of Gitea job/log views.
**Target files root:** `src/ci_orchestrator/web/`, `src/ci_orchestrator/operations/`.
@@ -46,15 +46,15 @@ Legend: `[ ]` todo · `[~]` in progress · `[x]` done · `[!]` blocked
- [x] **C-GATE — supervisor review** (Opus: gates green 989 pass / cov 94.12% / ruff+mypy clean; subagent cut off mid-C2 by session limit — Opus finished wiring + fixed 2 left-over test bugs: empty-VM header assert, OIDC keyring stub)
### Stage D — Service management (SECURITY-CRITICAL, last)
- [ ] D1. `operations/services.py` — allow-list, `service_states()`, `control_service()`
- [ ] D2. POST `/services/{service_id}/{action}` — confirm + CSRF + audit + refusals
- [ ] D3. `ci-web` self-restart via detached `systemd-run` (response returns first)
- [ ] D4. Services panel partial (state-aware buttons, active-VM warning on stop)
- [ ] D5. `sudoers.d/ci-web` (exact-command NOPASSWD) + installer `visudo -c` validate
- [ ] D6. Config `[web].managed_services` override + `config.example.toml` + `WEB-UI.md`
- [ ] D7. Security regression tests (unknown id → 400 & systemctl never called; etc.)
- [ ] D8. Stage D tests green · ruff · mypy --strict · coverage ≥90
- [ ] **D-GATE — supervisor review + deploy note**
- [x] D1. `operations/services.py` — allow-list, `service_states()`, `control_service()`
- [x] D2. POST `/services/{service_id}/{action}` — confirm + CSRF + audit + refusals
- [x] D3. `ci-web` self-restart via detached `systemd-run` (response returns first)
- [x] D4. Services panel partial (state-aware buttons, active-VM warning on stop)
- [x] D5. `sudoers.d/ci-web` (exact-command NOPASSWD) + installer `visudo -c` validate
- [x] D6. Config `[web].managed_services` override + `config.example.toml` + `WEB-UI.md`
- [x] D7. Security regression tests (unknown id → 400 & systemctl never called; etc.)
- [x] D8. Stage D tests green · ruff · mypy --strict · coverage ≥90
- [x] **D-GATE — supervisor review + deploy note** (Opus: gates green 1044 pass / cov 94.20% / ruff+mypy clean; verified control_service arg-list+allow-list+stop-refusal+no-shell, route 400-before-subprocess for all 3 refusals + CSRF-first + confirm-gate, sudoers exact-command ↔ argv match with no ci-web stop, 46 security/unit tests, no live-host execution. **DEPLOY (human step, not done): on merge → `pip install .` into prod venv, install `deploy/sudoers.d/ci-web` to /etc/sudoers.d/ (mode 0440, `visudo -c`), `sudo systemctl restart ci-web.service`.**)
---