feat(vwb): Basic auth LAN sur backend 5002 — creds dashboard, loopback exempté
Some checks failed
tests / Lint (ruff + black) (push) Failing after 1m52s
tests / Tests unitaires (sans GPU) (push) Failing after 1m52s
tests / Tests sécurité (critique) (push) Has been skipped

VWB backend exposé au LAN sans auth (point pré-clinique). Ajoute HTTP Basic auth
(mêmes identifiants que le dashboard: DASHBOARD_USER/DASHBOARD_PASSWORD) via
@app.before_request ; exempte loopback (intégration dashboard/agent_chat intacte),
/health et OPTIONS. Frontend = Create React App (pas Vite) → auth backend suffit
(navigateur LAN challengé au 1er XHR vers 5002) ; build statique = cible clinique.

Déployé + vérifié DGX: loopback 200, LAN no-creds 401, LAN+creds 200. 10 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-06-19 16:27:15 +02:00
parent ec1fb81054
commit cf81ce4c7b
2 changed files with 298 additions and 0 deletions

View File

@@ -28,6 +28,109 @@ load_dotenv() # fallback .env dans cwd (n'écrase pas les vars déjà définies
# Initialize Flask app
app = Flask(__name__)
# ============================================================
# HTTP Basic Auth LAN (cohérent avec le dashboard 5001)
# ============================================================
# Le VWB (backend 5002) était exposé au LAN SANS authentification. On ajoute
# un middleware before_request qui exige un header Authorization: Basic <b64>
# pour toute requête NON-loopback (LAN), avec les MÊMES credentials que le
# dashboard : DASHBOARD_USER / DASHBOARD_PASSWORD (dans .env.local).
#
# GARDE-FOU CRITIQUE — exemption loopback :
# Le dashboard (agent_chat/app.py `_fetch_vwb_workflows`) et les healthchecks
# appellent ce backend en boucle locale (http://localhost:5002 → 127.0.0.1).
# Exiger l'auth en loopback CASSERAIT l'intégration dashboard↔VWB. On exempte
# donc 127.0.0.1 / ::1 (et ::ffff:127.0.0.1) de toute auth.
#
# Différence assumée avec le dashboard (fail-closed) : ici on NE crashe PAS si
# DASHBOARD_PASSWORD est absent. On log un warning et on laisse passer le LAN
# (mode POC dev/dégradé). En clinique, DASHBOARD_PASSWORD est défini dans
# .env.local (chargé ci-dessus, lignes 24-26) → l'auth LAN est effective.
import base64 as _base64
import hmac as _hmac
_VWB_AUTH_USER = os.getenv("DASHBOARD_USER", "lea").strip()
_VWB_AUTH_PASSWORD = os.getenv("DASHBOARD_PASSWORD", "").strip()
# Désactivation explicite (dev/tests, parité avec le dashboard).
_VWB_AUTH_DISABLED = os.getenv("DASHBOARD_AUTH_DISABLED", "").lower() in (
"1", "true", "yes",
)
# Adresses considérées comme loopback (server-to-server, jamais challengées).
_VWB_LOOPBACK_ADDRS = {"127.0.0.1", "::1", "::ffff:127.0.0.1"}
# Paths publics (pas d'auth) — healthchecks systemd / NPM / smokes.
_VWB_PUBLIC_PATHS = {"/health", "/api/health"}
if not _VWB_AUTH_PASSWORD and not _VWB_AUTH_DISABLED:
logging.getLogger("vwb.auth").warning(
"[SECURITE] DASHBOARD_PASSWORD non defini : l'auth Basic LAN du VWB "
"(5002) est INACTIVE (le LAN passe sans credentials). Definir "
"DASHBOARD_PASSWORD dans .env.local pour l'activer (cible clinique)."
)
def _vwb_auth_ok(header_value: str) -> bool:
"""Valide le header Authorization Basic. Comparaison constant-time.
Logique identique au dashboard (`web_dashboard/app.py::_dashboard_auth_ok`).
"""
if not header_value or not header_value.lower().startswith("basic "):
return False
try:
decoded = _base64.b64decode(header_value[6:].strip()).decode("utf-8")
except (ValueError, UnicodeDecodeError):
return False
if ":" not in decoded:
return False
user, _, password = decoded.partition(":")
user_ok = _hmac.compare_digest(user, _VWB_AUTH_USER)
pwd_ok = _hmac.compare_digest(password, _VWB_AUTH_PASSWORD)
return user_ok and pwd_ok
@app.before_request
def _vwb_basic_auth_middleware():
"""Middleware d'auth HTTP Basic LAN sur le backend VWB (port 5002).
- Bypass total si DASHBOARD_AUTH_DISABLED=true (dev/tests).
- Bypass total si DASHBOARD_PASSWORD absent (mode POC degrade, warning emis
au demarrage) — on ne casse pas le service faute de secret.
- Loopback (127.0.0.1 / ::1) : JAMAIS challenge (proxy dashboard, healthcheck).
- Preflight CORS (OPTIONS) : laisse passer (le navigateur n'envoie pas
l'en-tete Authorization au preflight).
- Paths publics (_VWB_PUBLIC_PATHS) : healthchecks externes.
- Sinon (requete LAN) : header Authorization: Basic <b64> obligatoire, sinon 401.
"""
from flask import request, Response
# Dev / tests / mode degrade sans secret : bypass total
if _VWB_AUTH_DISABLED or not _VWB_AUTH_PASSWORD:
return None
# Preflight CORS : pas d'auth (le navigateur n'envoie pas les credentials)
if request.method == "OPTIONS":
return None
# Exemption loopback (server-to-server : dashboard, healthcheck)
if (request.remote_addr or "") in _VWB_LOOPBACK_ADDRS:
return None
# Paths publics (healthchecks externes)
if (request.path or "/") in _VWB_PUBLIC_PATHS:
return None
if _vwb_auth_ok(request.headers.get("Authorization", "")):
return None
# Pas authentifie — challenge 401 avec WWW-Authenticate
return Response(
'{"error": "authentication required"}',
status=401,
mimetype="application/json",
headers={"WWW-Authenticate": 'Basic realm="RPA Vision V3 VWB"'},
)
# ============================================================
# Logging — fichier rotatif + console (idempotent)
# ============================================================