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:
+11
-82
@@ -12,7 +12,6 @@ from PyQt6.QtGui import QFont
|
||||
from app.core.config import Config
|
||||
from app.core import wireguard as wg_core
|
||||
from app.core import dns as dns_util
|
||||
from app.core import mfa as mfa_core
|
||||
from app.ui import theme
|
||||
from app.ui.worker import ValueWorker
|
||||
from app.utils.platform_utils import (
|
||||
@@ -567,54 +566,6 @@ class AdminWindow(QDialog):
|
||||
lay.addStretch()
|
||||
return page
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Onglet MFA
|
||||
# ------------------------------------------------------------------ #
|
||||
def _subtab_mfa(self) -> QWidget:
|
||||
page, lay = self._test_page()
|
||||
lay.addWidget(self._hint(
|
||||
"Exige un code à usage unique avant chaque connexion. Le secret se "
|
||||
"scanne dans Bitwarden ou toute application TOTP ; conservez-le "
|
||||
"ailleurs qu'ici, il est le seul moyen de régénérer les codes."
|
||||
))
|
||||
|
||||
self._mfa_enabled_cb = QCheckBox("Activer la vérification MFA avant connexion")
|
||||
self._mfa_enabled_cb.setStyleSheet(f"color: {theme.TEXT}; font-weight: bold;")
|
||||
lay.addWidget(self._mfa_enabled_cb)
|
||||
|
||||
sep = QFrame()
|
||||
sep.setFrameShape(QFrame.Shape.HLine)
|
||||
sep.setStyleSheet(f"color: {theme.BORDER_SOFT};")
|
||||
lay.addWidget(sep)
|
||||
|
||||
grp = QGroupBox("Secret TOTP")
|
||||
form = QFormLayout(grp)
|
||||
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
||||
self._mfa_secret = QLineEdit()
|
||||
self._mfa_secret.setPlaceholderText("(générer ou coller votre secret TOTP)")
|
||||
self._row(form, "Secret :", self._mfa_secret)
|
||||
btn_gen_secret = QPushButton("Générer un nouveau secret")
|
||||
btn_gen_secret.clicked.connect(self._generate_mfa_secret)
|
||||
self._row(form, "", btn_gen_secret)
|
||||
lay.addWidget(grp)
|
||||
|
||||
grp2 = QGroupBox("QR Code — Scanner avec Bitwarden")
|
||||
v2 = QVBoxLayout(grp2)
|
||||
self._qr_label = QLabel("(générez un secret pour afficher le QR Code)")
|
||||
self._qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self._qr_label.setFixedHeight(190)
|
||||
v2.addWidget(self._qr_label)
|
||||
self._mfa_uri_label = QLabel("")
|
||||
self._mfa_uri_label.setWordWrap(True)
|
||||
self._mfa_uri_label.setStyleSheet(f"font-size: 9px; color: {theme.TEXT_FAINT};")
|
||||
v2.addWidget(self._mfa_uri_label)
|
||||
btn_show_qr = QPushButton("Afficher le QR Code")
|
||||
btn_show_qr.clicked.connect(self._show_qr)
|
||||
v2.addWidget(btn_show_qr)
|
||||
lay.addWidget(grp2)
|
||||
lay.addStretch()
|
||||
return page
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Onglet Test connexion
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -628,7 +579,7 @@ class AdminWindow(QDialog):
|
||||
|
||||
Les droits d'exécution (sudo/UAC) ont migré vers l'onglet Sécurité :
|
||||
ce n'est pas un diagnostic de connexion mais un réglage d'accès, au
|
||||
même titre que le MFA ou le mot de passe administrateur.
|
||||
même titre que le mot de passe administrateur.
|
||||
"""
|
||||
w, lay = self._dark_page("📡 Tests & diagnostic", theme.BANNER["test"])
|
||||
lay.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -947,19 +898,18 @@ class AdminWindow(QDialog):
|
||||
# Onglet Sécurité
|
||||
# ------------------------------------------------------------------ #
|
||||
def _tab_security(self) -> QWidget:
|
||||
"""Sécurité : MFA, mot de passe administrateur et droits d'exécution.
|
||||
"""Sécurité : mot de passe administrateur et droits d'exécution.
|
||||
|
||||
Les trois répondent à la même question — qui (ou quoi) a le droit de
|
||||
se connecter et d'administrer. Les droits sudo/UAC y ont rejoint MFA
|
||||
et mot de passe : ce n'est pas un test de connexion, c'est un réglage
|
||||
d'accès du même ordre.
|
||||
Les deux répondent à la même question — qui a le droit d'administrer
|
||||
ce poste. L'onglet MFA a disparu avec le secret TOTP local : le code
|
||||
d'accès est désormais réglé sur le serveur, qui est le seul à le
|
||||
connaître et le seul à ouvrir l'accès réseau.
|
||||
"""
|
||||
w, lay = self._dark_page("🔒 Sécurité", theme.BANNER["security"])
|
||||
lay.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
sub = QTabWidget()
|
||||
sub.setStyleSheet(theme.TABBAR_CSS)
|
||||
sub.addTab(self._subtab_mfa(), "🔐 MFA (TOTP)")
|
||||
sub.addTab(self._subtab_password(), "🔑 Mot de passe")
|
||||
sub.addTab(self._subtab_privileges(), self._privileges_tab_title())
|
||||
lay.addWidget(sub)
|
||||
@@ -1077,8 +1027,8 @@ class AdminWindow(QDialog):
|
||||
return box
|
||||
|
||||
def _about_pills(self, info) -> list[tuple[str, str]]:
|
||||
"""Pastilles de version/licence, plus l'état réel de MFA et des
|
||||
droits sudo — lu en direct, jamais une valeur supposée."""
|
||||
"""Pastilles de version/licence, plus l'état réel de l'authentification
|
||||
serveur et des droits sudo — lu en direct, jamais une valeur supposée."""
|
||||
from app.utils.platform_utils import (
|
||||
has_root_privileges, is_windows, privilege_report,
|
||||
)
|
||||
@@ -1086,10 +1036,10 @@ class AdminWindow(QDialog):
|
||||
(f"v{info.VERSION}", theme.ACCENT),
|
||||
(info.LICENSE, "#5b6b7a"),
|
||||
]
|
||||
if self._cfg.mfa_enabled:
|
||||
pills.append(("🔐 MFA activée", theme.OK_SOLID))
|
||||
if (self._cfg.wg.get("vpn_username") or "").strip():
|
||||
pills.append(("🔐 Code d'accès serveur", theme.OK_SOLID))
|
||||
else:
|
||||
pills.append(("🔐 MFA désactivée", "#5b6b7a"))
|
||||
pills.append(("🔐 Aucun code d'accès", "#5b6b7a"))
|
||||
|
||||
if not is_windows():
|
||||
if has_root_privileges():
|
||||
@@ -1321,8 +1271,6 @@ class AdminWindow(QDialog):
|
||||
self._split_dns_domains.setText(", ".join(domains) if isinstance(domains, list) else domains)
|
||||
self._priv_key.setText(wg.get("client_private_key", ""))
|
||||
self._pub_key.setText(wg.get("client_public_key", ""))
|
||||
self._mfa_enabled_cb.setChecked(self._cfg.mfa_enabled)
|
||||
self._mfa_secret.setText(self._cfg.mfa_secret)
|
||||
|
||||
# Un chargement de profil repart d'une configuration de confiance
|
||||
# (gérée par WGSecure) : seul un import .conf brut verrouille.
|
||||
@@ -1350,8 +1298,6 @@ class AdminWindow(QDialog):
|
||||
domains_text = self._split_dns_domains.text().strip()
|
||||
domains_list = [d.strip() for d in domains_text.split(",") if d.strip()] if domains_text else []
|
||||
self._cfg.set("wg", "split_dns_domains", domains_list)
|
||||
self._cfg.mfa_enabled = self._mfa_enabled_cb.isChecked()
|
||||
self._cfg.mfa_secret = self._mfa_secret.text().strip()
|
||||
self._cfg.save()
|
||||
|
||||
def _save_and_close(self):
|
||||
@@ -1512,23 +1458,6 @@ class AdminWindow(QDialog):
|
||||
v.addWidget(btn_close)
|
||||
dlg.exec()
|
||||
|
||||
def _generate_mfa_secret(self):
|
||||
secret = mfa_core.generate_secret()
|
||||
self._mfa_secret.setText(secret)
|
||||
self._show_qr()
|
||||
|
||||
def _show_qr(self):
|
||||
secret = self._mfa_secret.text().strip()
|
||||
if not secret:
|
||||
QMessageBox.warning(self, "Erreur", "Aucun secret MFA renseigné.")
|
||||
return
|
||||
pixmap = mfa_core.generate_qr_pixmap(secret)
|
||||
self._qr_label.setPixmap(
|
||||
pixmap.scaled(210, 210, Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation)
|
||||
)
|
||||
self._mfa_uri_label.setText(mfa_core.get_provisioning_uri(secret))
|
||||
|
||||
#: Rendu d'une étape de diagnostic : puce, couleur du libellé.
|
||||
_STEP_STYLE = {
|
||||
"ok": ("✅", theme.OK_TEXT),
|
||||
|
||||
Reference in New Issue
Block a user