feat(security): retire le MFA local, le code d'accès est celui du serveur (v0.8.1)
La v0.8.0 laissait cohabiter deux vérifications du code à 6 chiffres : la fenêtre MFA locale, qui contrôlait un code contre un secret détenu par le poste lui-même — et ne verrouillait donc que sa propre interface — puis la validation par le serveur, seule à décider de l'accès réseau. L'ancienne disparaît, avec le secret TOTP qu'elle stockait en clair dans config.json (effacé au premier lancement) et l'onglet MFA du panneau Administrateur. La saisie passe par une fenêtre dédiée qui relaie le refus exact du serveur et laisse réessayer, la requête partant dans un thread pour qu'un serveur muet ne fige pas l'application. Une ligne « Accès distant » distingue désormais l'état de l'autorisation de celui du tunnel : monté sans code validé, celui-ci ne mène nulle part. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+33
-38
@@ -9,14 +9,13 @@ from PyQt6.QtCore import Qt, QTimer, QDateTime
|
||||
from PyQt6.QtGui import QFont, QCloseEvent, QColor
|
||||
|
||||
from app.core.config import Config
|
||||
from app.core import vpn_session
|
||||
from app.core import wireguard as wg_core
|
||||
from app.core import dns as dns_util
|
||||
from app.core import log as conn_log
|
||||
from app.core import history as hist
|
||||
from app.core import shutdown as shutdown_guard
|
||||
from app.ui.worker import TaskWorker, ValueWorker
|
||||
from app.ui.mfa_dialog import MFADialog
|
||||
from app.ui.access_code_dialog import AccessCodeDialog
|
||||
from app.ui.admin_window import AdminWindow
|
||||
from app.ui.bw_graph import BandwidthGraph
|
||||
from app.ui.history_dialog import HistoryDialog
|
||||
@@ -37,6 +36,7 @@ class MainWindow(QMainWindow):
|
||||
self._worker: TaskWorker | None = None
|
||||
self._ping_worker: ValueWorker | None = None
|
||||
self._is_connected = False
|
||||
self._access_open = False
|
||||
self._reconnect_failures = 0
|
||||
self._quitting = False
|
||||
self._warned_foreign: set[str] = set()
|
||||
@@ -292,7 +292,7 @@ class MainWindow(QMainWindow):
|
||||
("server", "🌐", "Serveur"),
|
||||
("iface", "🔌", "Interface"),
|
||||
("addr", "📍", "Adresse IP"),
|
||||
("mfa", "🔐", "MFA"),
|
||||
("access", "🔐", "Accès distant"),
|
||||
]:
|
||||
rw = QWidget()
|
||||
rw.setStyleSheet("background: transparent;")
|
||||
@@ -539,10 +539,6 @@ class MainWindow(QMainWindow):
|
||||
return
|
||||
if self._worker is not None:
|
||||
return # une (dé)connexion est déjà en cours
|
||||
if self._cfg.mfa_enabled:
|
||||
# Rouvrir une fenêtre MFA modale toutes les 30 s rendrait
|
||||
# l'application inutilisable : la reconnexion doit rester manuelle.
|
||||
return
|
||||
if not self._cfg.configured or self._is_connected:
|
||||
self._reconnect_failures = 0
|
||||
return
|
||||
@@ -558,6 +554,20 @@ class MainWindow(QMainWindow):
|
||||
# ------------------------------------------------------------------ #
|
||||
# Statut WireGuard
|
||||
# ------------------------------------------------------------------ #
|
||||
def _access_state(self) -> str:
|
||||
"""État de l'autorisation réseau accordée par le serveur.
|
||||
|
||||
Distinct de l'état du tunnel : monté, celui-ci ne mène nulle part
|
||||
tant que le serveur n'a pas levé la quarantaine. Sans cette ligne,
|
||||
un utilisateur ayant refusé la saisie du code verrait « Connecté »
|
||||
et un réseau inexplicablement muet.
|
||||
"""
|
||||
if not (self._cfg.wg.get("vpn_username") or "").strip():
|
||||
return "Non géré par le serveur"
|
||||
if not self._is_connected:
|
||||
return "—"
|
||||
return "Ouvert ✓" if self._access_open else "Quarantaine (code requis)"
|
||||
|
||||
def _refresh_status(self):
|
||||
wg = self._cfg.wg
|
||||
srv = wg.get("server_endpoint", "")
|
||||
@@ -565,9 +575,6 @@ class MainWindow(QMainWindow):
|
||||
self._info_labels["server"].setText(f"{srv}:{port}" if srv else "—")
|
||||
self._info_labels["iface"].setText(wg.get("interface_name", "wgs0"))
|
||||
self._info_labels["addr"].setText(wg.get("client_address", "—"))
|
||||
self._info_labels["mfa"].setText(
|
||||
"Activé ✓" if self._cfg.mfa_enabled else "Désactivé"
|
||||
)
|
||||
|
||||
# Auto-reconnect timer
|
||||
if self._cfg.get("ui", "auto_reconnect"):
|
||||
@@ -586,6 +593,11 @@ class MainWindow(QMainWindow):
|
||||
self._is_connected = connected
|
||||
if connected:
|
||||
self._reconnect_failures = 0
|
||||
else:
|
||||
# Tunnel tombé : l'autorisation obtenue auprès du serveur ne vaut
|
||||
# que pour l'adresse du pair, elle ne survit pas à la déconnexion.
|
||||
self._access_open = False
|
||||
self._info_labels["access"].setText(self._access_state())
|
||||
|
||||
if connected:
|
||||
if self._connected_since is None:
|
||||
@@ -659,20 +671,6 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
return
|
||||
|
||||
if self._cfg.mfa_enabled:
|
||||
if not self._cfg.mfa_secret:
|
||||
if not silent:
|
||||
QMessageBox.warning(self, "MFA non configuré",
|
||||
"MFA activé mais aucun secret défini.")
|
||||
return
|
||||
if silent:
|
||||
return # jamais de fenêtre MFA sur minuterie
|
||||
dlg = MFADialog(self._cfg.mfa_secret, self)
|
||||
if dlg.exec() != MFADialog.DialogCode.Accepted or not dlg.is_verified():
|
||||
self._add_log("MFA annulé ou échoué", "warning")
|
||||
return
|
||||
self._add_log("Authentification MFA réussie ✓", "success")
|
||||
|
||||
self._begin_task("Connexion…")
|
||||
shutdown_guard.arm() # le tunnel devra être rendu à la sortie
|
||||
self._worker = TaskWorker(wg_core.connect, self._cfg,
|
||||
@@ -710,20 +708,17 @@ class MainWindow(QMainWindow):
|
||||
"warning")
|
||||
return
|
||||
|
||||
code, accepted = QInputDialog.getText(
|
||||
self, "Code d'accès",
|
||||
"Le tunnel est monté, mais l'accès aux ressources distantes\n"
|
||||
"reste fermé tant que votre code n'est pas validé.\n\n"
|
||||
"Code à 6 chiffres :",
|
||||
)
|
||||
if not accepted or not code.strip():
|
||||
self._add_log("Authentification annulée — accès distant fermé", "warning")
|
||||
return
|
||||
|
||||
ok, message = vpn_session.authenticate(self._cfg, code.strip())
|
||||
self._add_log(message, "success" if ok else "error")
|
||||
if not ok:
|
||||
QMessageBox.warning(self, "Accès refusé", message)
|
||||
dlg = AccessCodeDialog(self._cfg, self)
|
||||
accepted = dlg.exec() == AccessCodeDialog.DialogCode.Accepted
|
||||
self._access_open = accepted
|
||||
if accepted:
|
||||
self._add_log(dlg.message(), "success")
|
||||
else:
|
||||
# La fenêtre affiche déjà le refus du serveur et laisse réessayer :
|
||||
# y superposer une boîte d'alerte n'apprendrait rien de plus.
|
||||
self._add_log(dlg.message() or "Authentification abandonnée — "
|
||||
"accès distant fermé", "warning")
|
||||
self._refresh_status()
|
||||
|
||||
def _begin_task(self, label: str):
|
||||
self._connecting = True
|
||||
|
||||
Reference in New Issue
Block a user