From 549ea0631bed7a14aeeeefef36583ad817522324 Mon Sep 17 00:00:00 2001 From: Dom Date: Mon, 8 Jun 2026 15:27:06 +0200 Subject: [PATCH] =?UTF-8?q?fix(wp-a):=20dashboard=20fail-closed=20sans=20m?= =?UTF-8?q?ot=20de=20passe=20par=20d=C3=A9faut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le dashboard refuse de démarrer si DASHBOARD_PASSWORD absent ET auth non explicitement désactivée (DASHBOARD_AUTH_DISABLED). Supprime le mot de passe par défaut hardcodé exploitable. - web_dashboard/app.py : _require_dashboard_password() fail-closed (lève en prod sans secret ; mode dev/test = DASHBOARD_AUTH_DISABLED=true) - tests/unit/conftest.py : DASHBOARD_AUTH_DISABLED=true par défaut pour les tests - tests/unit/test_dashboard_failclosed_wpa.py : 5 tests (fail-closed, anti-régression défaut) - tests/unit/test_dashboard_auth_p0a.py : fixture _restore_module restaure un état neutre sûr 48 tests dashboard verts (WP-A + non-régression auth/routes). Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/unit/conftest.py | 6 ++++ tests/unit/test_dashboard_auth_p0a.py | 4 ++- tests/unit/test_dashboard_failclosed_wpa.py | 40 +++++++++++++++++++++ web_dashboard/app.py | 28 +++++++++------ 4 files changed, 66 insertions(+), 12 deletions(-) create mode 100644 tests/unit/test_dashboard_failclosed_wpa.py diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 659a04127..38cb2c1e7 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -3,9 +3,15 @@ Force le bon chemin agent_v0 (rpa_vision_v3) pour éviter les conflits avec ~/ai/agent_v0 (standalone). """ +import os import sys from pathlib import Path +# WP-A : par défaut, les tests unitaires tournent avec l'auth dashboard désactivée +# (mode dev/test explicite, fail-closed côté prod). Les tests d'auth dédiés +# (test_dashboard_auth_p0a) surchargent via monkeypatch + reload. +os.environ.setdefault("DASHBOARD_AUTH_DISABLED", "true") + ROOT = str(Path(__file__).resolve().parents[2]) if ROOT in sys.path: diff --git a/tests/unit/test_dashboard_auth_p0a.py b/tests/unit/test_dashboard_auth_p0a.py index 468c39c76..a8679a608 100644 --- a/tests/unit/test_dashboard_auth_p0a.py +++ b/tests/unit/test_dashboard_auth_p0a.py @@ -153,7 +153,9 @@ def _restore_module(monkeypatch): """Recharge web_dashboard.app après chaque test pour que les autres tests (TestDashboardRoutes sans auth explicite) continuent de passer.""" yield - monkeypatch.delenv("DASHBOARD_AUTH_DISABLED", raising=False) + # WP-A : restaurer en mode auth désactivée (état neutre sûr) plutôt que sans secret, + # sinon le reload tombe sur le fail-closed du dashboard. + monkeypatch.setenv("DASHBOARD_AUTH_DISABLED", "true") monkeypatch.delenv("DASHBOARD_USER", raising=False) monkeypatch.delenv("DASHBOARD_PASSWORD", raising=False) if "web_dashboard.app" in sys.modules: diff --git a/tests/unit/test_dashboard_failclosed_wpa.py b/tests/unit/test_dashboard_failclosed_wpa.py new file mode 100644 index 000000000..6486d5079 --- /dev/null +++ b/tests/unit/test_dashboard_failclosed_wpa.py @@ -0,0 +1,40 @@ +"""WP-A — fail-closed du mot de passe dashboard. + +Le dashboard ne doit plus démarrer avec un mot de passe par défaut connu : +sans DASHBOARD_PASSWORD et hors mode dev explicite (DASHBOARD_AUTH_DISABLED), +il doit refuser de démarrer. +""" +from __future__ import annotations + +import pytest + +from web_dashboard.app import _require_dashboard_password + + +def test_secret_present_returns_it(): + assert _require_dashboard_password("vrai-secret", False) == "vrai-secret" + + +def test_auth_disabled_allows_empty(): + # mode dev/test explicite : pas de secret requis + assert _require_dashboard_password("", True) == "" + + +def test_no_secret_no_dev_fails_closed(): + # prod sans secret → fail-closed + with pytest.raises(RuntimeError): + _require_dashboard_password("", False) + + +def test_explicit_secret_wins_over_disabled(): + assert _require_dashboard_password("s", True) == "s" + + +def test_no_default_password_constant_remains(): + # garde-fou anti-régression : l'ancien défaut hardcodé ne doit plus exister + import inspect + + import web_dashboard.app as dash + + src = inspect.getsource(dash) + assert "changeme-dashboard-RpaVision2026!" not in src diff --git a/web_dashboard/app.py b/web_dashboard/app.py index a80685f6a..e752aa200 100644 --- a/web_dashboard/app.py +++ b/web_dashboard/app.py @@ -70,19 +70,25 @@ _DASHBOARD_AUTH_DISABLED = os.getenv("DASHBOARD_AUTH_DISABLED", "").lower() in ( "1", "true", "yes", ) -# Si pas de password défini en env ET auth pas explicitement désactivée → -# on utilise un mot de passe par défaut "safe" (long, random-ish) ET on log -# un WARNING très visible au démarrage pour forcer Dom à le configurer -# avant un déploiement prod. On ne veut surtout pas générer un mot de passe -# aléatoire à chaque boot (même problème que l'API token auto-généré). -if not _DASHBOARD_PASSWORD and not _DASHBOARD_AUTH_DISABLED: - _DASHBOARD_PASSWORD = "changeme-dashboard-RpaVision2026!" - api_logger.warning( - "[SÉCURITÉ] DASHBOARD_PASSWORD non défini en env — utilisation d'un " - "mot de passe par défaut temporaire. DÉFINIR DASHBOARD_PASSWORD " - "AVANT TOUT DÉPLOIEMENT (identifiant : DASHBOARD_USER)." +# WP-A (fail-closed sécurité) : en l'absence de DASHBOARD_PASSWORD et hors mode dev/test +# explicite (DASHBOARD_AUTH_DISABLED), on REFUSE de démarrer plutôt que de tomber sur un +# mot de passe par défaut connu (exploitable). Dev/test : DASHBOARD_AUTH_DISABLED=true ou +# un vrai DASHBOARD_PASSWORD. +def _require_dashboard_password(password: str, auth_disabled: bool) -> str: + """Résout le mot de passe dashboard en fail-closed. Lève si ni secret ni dev explicite.""" + if password: + return password + if auth_disabled: + return "" # auth explicitement désactivée (dev/test) — password non utilisé + raise RuntimeError( + "[SÉCURITÉ] DASHBOARD_PASSWORD non défini et auth non désactivée : le dashboard " + "refuse de démarrer (fail-closed). Définir DASHBOARD_PASSWORD (prod) ou " + "DASHBOARD_AUTH_DISABLED=true (dev/test)." ) + +_DASHBOARD_PASSWORD = _require_dashboard_password(_DASHBOARD_PASSWORD, _DASHBOARD_AUTH_DISABLED) + # Paths publics (pas d'auth, pour healthchecks externes) _PUBLIC_DASHBOARD_PATHS = { "/health",