fix(profiles): load_routes must not crash on an untraversable parent

Found by deploying to the real board, not by tests: GET /status returned 500.

/etc/secubox/waf is 0750 root:root while haproxy-routes.json inside it is 0644
— the file is world-readable, the directory is not traversable by the secubox
user the service runs as. Path.exists() raises PermissionError in that case, and
it sat OUTSIDE the try that already guarded read_text.

This is the fifth instance of the same class on this branch. The previous four
were 'unknown must not become a definite answer'; this one is its twin:
unknown must not become a crash either. An unreadable routes file now yields
None (undeterminable) as intended, so /status degrades to 'exposure unknown'
instead of 500ing the whole panel.

The directory permissions are the WAF module's business and are left alone —
this module's job is to survive them.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
CyberMind-FR 2026-07-17 13:40:21 +02:00
parent 78f5e80747
commit cedc012051
2 changed files with 25 additions and 2 deletions

View File

@ -52,11 +52,17 @@ def _run_cmd(argv: list[str]) -> tuple[int | None, str]:
def load_routes(path: Path = ROUTES_FILE) -> set[str] | None:
"""Domaines routés par le WAF. Fichier absent = aucune route (pas une erreur).
`.exists()` est DANS le try : il lève PermissionError quand un parent n'est
pas traversable, et c'est le cas réel sur la board — /etc/secubox/waf est
0750 root:root alors que haproxy-routes.json est 0644. Le laisser dehors
faisait remonter un 500 sur /status au lieu d'un « indéterminable ».
Inconnu ne doit ni mentir NI planter.
Fichier présent mais illisible/corrompu = indéterminable None, pas set()."""
path = Path(path)
if not path.exists():
return set()
try:
if not path.exists():
return set()
return set(json.loads(path.read_text(encoding="utf-8")))
except (OSError, json.JSONDecodeError):
return None

View File

@ -164,3 +164,20 @@ def test_observe_portal_routed_none_when_routes_undeterminable(monkeypatch):
})
a = observe(LXC, run=run)
assert a.portal_routed is None
def test_load_routes_untraversable_parent_is_none_not_crash(tmp_path):
"""Cas RÉEL de la board : /etc/secubox/waf est 0750 root:root alors que
haproxy-routes.json est 0644 le fichier est lisible, le répertoire non
traversable. `.exists()` lève alors PermissionError. Le laisser hors du try
remontait un 500 sur GET /status. Inconnu ne doit ni mentir ni planter."""
import os
waf = tmp_path / "waf"
waf.mkdir()
routes = waf / "haproxy-routes.json"
routes.write_text(json.dumps({"peertube.gk2.secubox.in": ["127.0.0.1", 9000]}))
os.chmod(waf, 0o000) # parent non traversable
try:
assert load_routes(routes) is None # indéterminable, pas set(), pas d'exception
finally:
os.chmod(waf, 0o755) # sinon tmp_path n'est pas nettoyable