fix(wp-a): dashboard fail-closed sans mot de passe par défaut
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
40
tests/unit/test_dashboard_failclosed_wpa.py
Normal file
40
tests/unit/test_dashboard_failclosed_wpa.py
Normal file
@@ -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
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user