- Onglet "🧹 Nettoyage" dans le dashboard (iframe vers port 5006) - Indicateur d'état + bouton de démarrage si cleaner down - Service systemd rpa-session-cleaner intégré au target rpa-vision - svc.sh et services.conf incluent session-cleaner (port 5006) P0-A — Auth dashboard Flask : - HTTP Basic obligatoire sur tous les endpoints (sauf /health, /healthz) - Credentials via DASHBOARD_USER + DASHBOARD_PASSWORD - 13 tests Nettoyage UI : - Section "Détection Visuelle" OWL retirée (modèle remplacé par pipeline VLM) - Dashboard préfère auto shot_*_blurred.png (avec ?raw=1 pour brut) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
161 lines
6.0 KiB
Python
161 lines
6.0 KiB
Python
"""
|
|
Tests du Fix P0-A : authentification HTTP Basic sur le dashboard Flask (port 5001).
|
|
|
|
Avant ce fix, 71 endpoints étaient exposés sans auth. Le middleware
|
|
`_dashboard_basic_auth_middleware` ajoute un challenge 401 sur toutes les
|
|
routes HTTP sauf les healthchecks publics.
|
|
|
|
Contrôles :
|
|
- Sans Authorization header → 401 avec WWW-Authenticate
|
|
- Avec mauvais credentials → 401
|
|
- Avec bons credentials → passage normal (200)
|
|
- /health, /healthz, /api/health restent publics (monitoring externe)
|
|
- Mode TESTING sans DASHBOARD_AUTH_ENABLED → bypass (compat tests existants)
|
|
- DASHBOARD_AUTH_DISABLED=true → bypass global (dev local)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import importlib
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
# Ajouter le répertoire racine au path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_enabled_client(monkeypatch):
|
|
"""Client Flask avec l'auth activée (TESTING + DASHBOARD_AUTH_ENABLED).
|
|
|
|
On recharge le module pour forcer la relecture des variables d'env.
|
|
"""
|
|
monkeypatch.setenv("DASHBOARD_USER", "lea")
|
|
monkeypatch.setenv("DASHBOARD_PASSWORD", "secret-test-pwd")
|
|
monkeypatch.delenv("DASHBOARD_AUTH_DISABLED", raising=False)
|
|
|
|
# Recharger le module pour que les constantes soient relues
|
|
if "web_dashboard.app" in sys.modules:
|
|
importlib.reload(sys.modules["web_dashboard.app"])
|
|
from web_dashboard.app import app
|
|
|
|
app.config["TESTING"] = True
|
|
app.config["DASHBOARD_AUTH_ENABLED"] = True
|
|
with app.test_client() as c:
|
|
yield c
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_disabled_client(monkeypatch):
|
|
"""Client Flask avec l'auth désactivée (bypass global)."""
|
|
monkeypatch.setenv("DASHBOARD_AUTH_DISABLED", "true")
|
|
if "web_dashboard.app" in sys.modules:
|
|
importlib.reload(sys.modules["web_dashboard.app"])
|
|
from web_dashboard.app import app
|
|
|
|
app.config["TESTING"] = True
|
|
with app.test_client() as c:
|
|
yield c
|
|
|
|
|
|
def _basic_auth_header(user: str, password: str) -> str:
|
|
token = base64.b64encode(f"{user}:{password}".encode()).decode()
|
|
return f"Basic {token}"
|
|
|
|
|
|
class TestDashboardAuthP0A:
|
|
"""Fix P0-A : auth HTTP Basic obligatoire sur le dashboard."""
|
|
|
|
def test_no_auth_header_returns_401(self, auth_enabled_client):
|
|
"""Sans header Authorization → 401 + challenge WWW-Authenticate."""
|
|
resp = auth_enabled_client.get("/api/system/status")
|
|
assert resp.status_code == 401
|
|
assert "WWW-Authenticate" in resp.headers
|
|
assert "Basic" in resp.headers["WWW-Authenticate"]
|
|
|
|
def test_wrong_password_returns_401(self, auth_enabled_client):
|
|
"""Mauvais mot de passe → 401."""
|
|
resp = auth_enabled_client.get(
|
|
"/api/system/status",
|
|
headers={"Authorization": _basic_auth_header("lea", "wrong")},
|
|
)
|
|
assert resp.status_code == 401
|
|
|
|
def test_wrong_user_returns_401(self, auth_enabled_client):
|
|
"""Mauvais utilisateur → 401."""
|
|
resp = auth_enabled_client.get(
|
|
"/api/system/status",
|
|
headers={"Authorization": _basic_auth_header("intruder", "secret-test-pwd")},
|
|
)
|
|
assert resp.status_code == 401
|
|
|
|
def test_malformed_header_returns_401(self, auth_enabled_client):
|
|
"""Header mal formé (pas de Basic) → 401."""
|
|
resp = auth_enabled_client.get(
|
|
"/api/system/status",
|
|
headers={"Authorization": "Bearer tototoken"},
|
|
)
|
|
assert resp.status_code == 401
|
|
|
|
def test_valid_credentials_pass(self, auth_enabled_client):
|
|
"""Bons credentials → 200."""
|
|
resp = auth_enabled_client.get(
|
|
"/api/system/status",
|
|
headers={"Authorization": _basic_auth_header("lea", "secret-test-pwd")},
|
|
)
|
|
assert resp.status_code == 200
|
|
|
|
def test_healthz_public(self, auth_enabled_client):
|
|
"""/healthz reste public (systemd healthcheck)."""
|
|
resp = auth_enabled_client.get("/healthz")
|
|
assert resp.status_code == 200
|
|
|
|
def test_health_public(self, auth_enabled_client):
|
|
"""/health reste public (monitoring externe)."""
|
|
resp = auth_enabled_client.get("/health")
|
|
assert resp.status_code == 200
|
|
|
|
def test_api_health_public(self, auth_enabled_client):
|
|
"""/api/health reste public (NPM reverse proxy)."""
|
|
resp = auth_enabled_client.get("/api/health")
|
|
assert resp.status_code == 200
|
|
|
|
def test_auth_disabled_bypass(self, auth_disabled_client):
|
|
"""DASHBOARD_AUTH_DISABLED=true → pas d'auth requise."""
|
|
resp = auth_disabled_client.get("/api/system/status")
|
|
assert resp.status_code == 200
|
|
|
|
def test_config_endpoint_requires_auth(self, auth_enabled_client):
|
|
"""L'endpoint sensible /api/config exige l'auth."""
|
|
resp = auth_enabled_client.get("/api/config")
|
|
assert resp.status_code == 401
|
|
|
|
def test_services_endpoint_requires_auth(self, auth_enabled_client):
|
|
"""L'endpoint sensible /api/services exige l'auth."""
|
|
resp = auth_enabled_client.get("/api/services")
|
|
assert resp.status_code == 401
|
|
|
|
def test_services_start_all_requires_auth(self, auth_enabled_client):
|
|
"""Un endpoint POST destructeur exige l'auth."""
|
|
resp = auth_enabled_client.post("/api/services/start-all")
|
|
assert resp.status_code == 401
|
|
|
|
def test_index_page_requires_auth(self, auth_enabled_client):
|
|
"""Même la page HTML d'accueil exige l'auth (pas de leak côté public)."""
|
|
resp = auth_enabled_client.get("/")
|
|
assert resp.status_code == 401
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
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)
|
|
monkeypatch.delenv("DASHBOARD_USER", raising=False)
|
|
monkeypatch.delenv("DASHBOARD_PASSWORD", raising=False)
|
|
if "web_dashboard.app" in sys.modules:
|
|
importlib.reload(sys.modules["web_dashboard.app"])
|