mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-17 20:39:13 +00:00
web_interface/blueprints/api_v3.py held 111 routes, 56 helpers and 181 functions in one module -- 9% of the core by line count and three times the next largest file. It becomes a package of nine route modules grouped by path segment, plus __init__.py for the shared imports, constants, Blueprint and helpers. Every route module decorates the SAME api_v3 Blueprint object, so endpoint names stay api_v3.<function>, the URL map is unchanged and app.py is untouched. Verified: 111 routes before, 111 after, byte-identical rules, endpoints and methods, and every endpoint still on the one blueprint. plugins 3,867 config 1,178 starlark 692 system 619 fonts 452 misc 398 wifi 361 display 326 backup 212 __init__ 1,787 (imports, constants, Blueprint, 56 helpers) Two things the URL-map check could not catch, both found by running the suite: 1. PROJECT_ROOT = Path(__file__).parent.parent.parent. Moving the code one directory deeper made that resolve to web_interface/ instead of the project root. Nothing failed at import; it surfaced as ~110 tests failing with 404s and "installation script not found", because every path built from it was one level too shallow. Now parents[3], and test_api_v3_url_map.py asserts PROJECT_ROOT/run.py exists so the next move cannot repeat it. 2. Module-attribute patching. Tests do monkeypatch.setattr(api_v3_module, "_BACKUP_EXPORT_DIR", ...) and a route module that binds such a name by value never sees the patch. The shared code therefore stays in __init__.py rather than moving to a _common submodule -- it has to live on the module the tests patch -- and the eleven names tests patch are read back through the package (_pkg.X) instead of bound by value. Those eleven were found by AST-scanning every setattr in the test tree, not by guessing; "time" is among them, used to drive a fake clock through the second-resolution credential-backup filenames. Test changes are confined to what genuinely moved: patch targets that now name the owning route module, imports of helpers, and six tests that scan the api_v3 source as a file and now read the package directory. Full suite: 4,278 passed, 68 skipped, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
82 lines
3.3 KiB
Python
82 lines
3.3 KiB
Python
"""Guards that every privileged systemctl call the web interface makes is
|
|
covered by a passwordless-sudo grant in configure_web_sudo.sh.
|
|
|
|
The web interface runs headless (no TTY), so any `sudo` call that is not
|
|
matched by a NOPASSWD rule in /etc/sudoers.d/ledmatrix_web falls back to a
|
|
password prompt and fails with:
|
|
|
|
sudo: a terminal is required to read the password
|
|
|
|
sudo matches the command line by exact string, so `systemctl start ledmatrix`
|
|
and `systemctl start ledmatrix.service` are NOT interchangeable. This test
|
|
parses both the production blueprint and the sudoers-generator script and
|
|
asserts the (verb, unit) pairs line up, catching the suffix-mismatch class of
|
|
bug before it ships.
|
|
"""
|
|
|
|
import ast
|
|
import re
|
|
from pathlib import Path
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
# api_v3 is a package; a sudo systemctl call can live in any of its modules.
|
|
API_V3_PKG = PROJECT_ROOT / "web_interface" / "blueprints" / "api_v3"
|
|
|
|
|
|
def _api_v3_source() -> str:
|
|
return "\n".join(p.read_text() for p in sorted(API_V3_PKG.glob("*.py")))
|
|
SUDOERS_SCRIPT = PROJECT_ROOT / "scripts" / "install" / "configure_web_sudo.sh"
|
|
|
|
|
|
def _sudo_systemctl_calls(source: str) -> set[tuple[str, str]]:
|
|
"""Return (verb, unit) for every list literal beginning with
|
|
['sudo', 'systemctl', ...] passed to a subprocess call in the source."""
|
|
calls: set[tuple[str, str]] = set()
|
|
for node in ast.walk(ast.parse(source)):
|
|
if not isinstance(node, ast.List):
|
|
continue
|
|
elts = node.elts
|
|
if len(elts) < 4:
|
|
continue
|
|
if not all(isinstance(e, ast.Constant) and isinstance(e.value, str) for e in elts[:4]):
|
|
continue
|
|
if elts[0].value == "sudo" and elts[1].value == "systemctl":
|
|
calls.add((elts[2].value, elts[3].value))
|
|
return calls
|
|
|
|
|
|
def _granted_systemctl_rules(script: str) -> set[tuple[str, str]]:
|
|
"""Return (verb, unit) for each `$SYSTEMCTL_PATH <verb> <unit>` NOPASSWD
|
|
grant emitted by the sudoers-generator script."""
|
|
rules: set[tuple[str, str]] = set()
|
|
for match in re.finditer(r"\$SYSTEMCTL_PATH\s+(\S+)\s+(\S+)", script):
|
|
verb, unit = match.group(1), match.group(2).rstrip('"')
|
|
rules.add((verb, unit))
|
|
return rules
|
|
|
|
|
|
def test_every_sudo_systemctl_call_is_granted() -> None:
|
|
calls = _sudo_systemctl_calls(_api_v3_source())
|
|
rules = _granted_systemctl_rules(SUDOERS_SCRIPT.read_text())
|
|
|
|
assert calls, "expected to find sudo systemctl calls in api_v3.py"
|
|
|
|
uncovered = {c for c in calls if c not in rules}
|
|
assert not uncovered, (
|
|
"These sudo systemctl calls have no matching NOPASSWD grant in "
|
|
"configure_web_sudo.sh; they will fail headless with "
|
|
"'sudo: a terminal is required to read the password': "
|
|
+ ", ".join(f"systemctl {v} {u}" for v, u in sorted(uncovered))
|
|
)
|
|
|
|
|
|
def test_units_are_fully_qualified() -> None:
|
|
"""Privileged systemctl calls must name the unit as <name>.service so they
|
|
match the sudoers grants, which use the fully-qualified unit name."""
|
|
calls = _sudo_systemctl_calls(_api_v3_source())
|
|
unqualified = {(v, u) for v, u in calls if not u.endswith(".service")}
|
|
assert not unqualified, (
|
|
"sudo systemctl calls must use fully-qualified .service unit names: "
|
|
+ ", ".join(f"systemctl {v} {u}" for v, u in sorted(unqualified))
|
|
)
|