fix(auth): le serveur reconnaît le poste à son adresse, plus à un identifiant
La fenêtre de saisie réclamait l'Identifiant VPN à chaque ouverture sur un poste neuf, et l'authentification échouait. Les deux symptômes n'en faisaient qu'un : l'identifiant n'était enregistré qu'en cas d'authentification réussie, or aucune n'aboutissait, donc il était redemandé sans fin. Il n'est plus transmis du tout. `POST /api/session` désigne désormais le compte par l'adresse source de la requête, comme `GET /api/session` le fait depuis la v0.8.6. Le serveur l'a toujours su ; le poste, lui, ne faisait que répéter un renseignement qu'il pouvait taper de travers. C'est aussi plus sûr : sur WireGuard l'adresse source est garantie par le routage par clé, alors qu'un identifiant en clair permettait d'ouvrir l'accès d'un compte depuis n'importe quelle adresse. Exige la v0.3.2 du serveur, qui accepte les deux formes. Le champ disparaît de la fenêtre de saisie. Celui du panneau Administrateur reste, purement indicatif désormais, avec une infobulle qui le dit. Trois décisions d'interface se fondaient sur « un identifiant est-il configuré ? », une question que le poste ne pouvait pas trancher : proposer ou non la saisie automatique, le libellé de la ligne Accès distant, et l'aspect du bouton. Elles suivent maintenant la réponse du serveur, qui distingue « aucun pair à cette adresse » (401, nouveau) de « accès fermé, code attendu » (403). Un poste non enrôlé n'est donc plus harcelé pour un code qui n'ouvrirait rien, et la ligne affiche « Non géré par le serveur » au lieu de le supposer. Corrige aussi une régression de la v0.9.0 : `tunnel_gateway` retenait la route par défaut des IPs autorisées comme réseau de tunnel, et rendait donc 0.0.0.1 comme passerelle en tunnel intégral — sondage et validation partaient vers une adresse inexistante. Elle ne retient plus que les réseaux contenant l'adresse du client, préfère le plus spécifique, et se replie sur /24 (ou /64) faute de candidat. Le diagnostic « Pair joignable dans le tunnel » en bénéficie aussi. Enfin, `authenticate` distingue le silence du réseau d'un refus du serveur, comme `session_status` le faisait déjà. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b3c0515244
commit
023ec81e03
+35
-14
@@ -68,12 +68,19 @@ class AuthResult(NamedTuple):
|
|||||||
code, alors que l'accès était peut-être déjà ouvert. Faux ici veut dire
|
code, alors que l'accès était peut-être déjà ouvert. Faux ici veut dire
|
||||||
« on ne sait pas » : serveur injoignable, réponse illisible, ou route
|
« on ne sait pas » : serveur injoignable, réponse illisible, ou route
|
||||||
`/api/session` absente d'un serveur antérieur à la v0.3.0.
|
`/api/session` absente d'un serveur antérieur à la v0.3.0.
|
||||||
|
|
||||||
|
`enrolled` ne vaut faux que lorsque le serveur affirme ne connaître aucun
|
||||||
|
pair à cette adresse — le poste n'est pas enrôlé et aucun code ne
|
||||||
|
l'ouvrira. Renseigné par `session_status` seul : sur une demande
|
||||||
|
d'ouverture, le même code HTTP signifie « identifiant ou code invalide »,
|
||||||
|
ce qui est tout autre chose.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ok: bool
|
ok: bool
|
||||||
message: str
|
message: str
|
||||||
access_until: datetime | None = None
|
access_until: datetime | None = None
|
||||||
authoritative: bool = True
|
authoritative: bool = True
|
||||||
|
enrolled: bool = True
|
||||||
|
|
||||||
|
|
||||||
def _parse_until(raw: object) -> datetime | None:
|
def _parse_until(raw: object) -> datetime | None:
|
||||||
@@ -255,6 +262,14 @@ def session_status(cfg: Config) -> AuthResult:
|
|||||||
return AuthResult(False, detail or "État de l'accès inconnu "
|
return AuthResult(False, detail or "État de l'accès inconnu "
|
||||||
"(serveur sans /api/session)",
|
"(serveur sans /api/session)",
|
||||||
None, False)
|
None, False)
|
||||||
|
if exc.code == 401:
|
||||||
|
# Le serveur ne connaît aucun pair à cette adresse : ce poste
|
||||||
|
# n'est pas enrôlé, et aucun code n'y changera rien. Distinct du
|
||||||
|
# 403 « accès fermé », où une saisie a tout son sens. Les deux
|
||||||
|
# rendaient 403 avant la v0.3.2 du serveur, et le client réclamait
|
||||||
|
# donc un code à des postes qui n'en avaient aucun à donner.
|
||||||
|
return AuthResult(False, detail or "Ce poste n'est pas enrôlé sur "
|
||||||
|
"le serveur.", None, True, False)
|
||||||
# Toute autre réponse HTTP vient du serveur lui-même : elle fait
|
# Toute autre réponse HTTP vient du serveur lui-même : elle fait
|
||||||
# autorité, un 403 comme le reste.
|
# autorité, un 403 comme le reste.
|
||||||
return AuthResult(False, detail or f"Accès fermé (code {exc.code})")
|
return AuthResult(False, detail or f"Accès fermé (code {exc.code})")
|
||||||
@@ -264,7 +279,7 @@ def session_status(cfg: Config) -> AuthResult:
|
|||||||
# on ne prétend pas non plus qu'il est fermé.
|
# on ne prétend pas non plus qu'il est fermé.
|
||||||
return AuthResult(False, f"État de l'accès inconnu : {exc}", None, False)
|
return AuthResult(False, f"État de l'accès inconnu : {exc}", None, False)
|
||||||
|
|
||||||
def authenticate(cfg: Config, code: str, username: str = "") -> AuthResult:
|
def authenticate(cfg: Config, code: str) -> AuthResult:
|
||||||
"""Valide un code TOTP auprès du serveur et ouvre l'accès réseau.
|
"""Valide un code TOTP auprès du serveur et ouvre l'accès réseau.
|
||||||
|
|
||||||
Retourne un `AuthResult`. Un échec n'est jamais silencieux : sans
|
Retourne un `AuthResult`. Un échec n'est jamais silencieux : sans
|
||||||
@@ -272,23 +287,25 @@ def authenticate(cfg: Config, code: str, username: str = "") -> AuthResult:
|
|||||||
l'utilisateur doit comprendre pourquoi plutôt que de constater un
|
l'utilisateur doit comprendre pourquoi plutôt que de constater un
|
||||||
réseau inexplicablement muet.
|
réseau inexplicablement muet.
|
||||||
|
|
||||||
`username` permet de fournir l'identifiant à la volée, quand il n'est pas
|
Aucun identifiant n'est transmis : le serveur reconnaît l'appelant à l'IP
|
||||||
encore en configuration : la fenêtre de saisie le demande alors avec le
|
source de son tunnel, comme il le fait déjà pour `session_status`. Il
|
||||||
code, plutôt que de renvoyer l'utilisateur vers le panneau Administrateur
|
l'exigeait, et le poste devait donc le connaître — la fenêtre de saisie
|
||||||
au moment précis où il cherche à ouvrir son accès. Il est enregistré
|
le réclamait à l'utilisateur, qui pouvait le taper de travers, et un poste
|
||||||
une fois le serveur l'ayant accepté.
|
n'ayant jamais réussi à s'authentifier le redemandait indéfiniment. Le
|
||||||
|
serveur, lui, l'a toujours su.
|
||||||
|
|
||||||
|
Cette identification est plus sûre, pas seulement plus simple : sur
|
||||||
|
WireGuard l'adresse source est garantie par le routage par clé, alors
|
||||||
|
qu'un identifiant en clair permettait d'ouvrir l'accès d'un compte depuis
|
||||||
|
n'importe quelle adresse.
|
||||||
"""
|
"""
|
||||||
base = api_base_url(cfg)
|
base = api_base_url(cfg)
|
||||||
if not base:
|
if not base:
|
||||||
return AuthResult(False, "Adresse du serveur d'authentification "
|
return AuthResult(False, "Adresse du serveur d'authentification "
|
||||||
"inconnue. Renseignez-la dans le panneau "
|
"inconnue. Renseignez-la dans le panneau "
|
||||||
"Administrateur.")
|
"Administrateur.", None, False)
|
||||||
|
|
||||||
username = (username or cfg.wg.get("vpn_username") or "").strip()
|
payload = json.dumps({"code": code}).encode("utf-8")
|
||||||
if not username:
|
|
||||||
return AuthResult(False, "Indiquez votre identifiant VPN.")
|
|
||||||
|
|
||||||
payload = json.dumps({"username": username, "code": code}).encode("utf-8")
|
|
||||||
request = urllib.request.Request(
|
request = urllib.request.Request(
|
||||||
f"{base}/api/session", data=payload,
|
f"{base}/api/session", data=payload,
|
||||||
headers={"Content-Type": "application/json"}, method="POST",
|
headers={"Content-Type": "application/json"}, method="POST",
|
||||||
@@ -314,7 +331,11 @@ def authenticate(cfg: Config, code: str, username: str = "") -> AuthResult:
|
|||||||
return AuthResult(False, detail
|
return AuthResult(False, detail
|
||||||
or f"Authentification refusée (code {exc.code})")
|
or f"Authentification refusée (code {exc.code})")
|
||||||
except urllib.error.URLError as exc:
|
except urllib.error.URLError as exc:
|
||||||
|
# `authoritative` faux : le serveur n'a rien dit. Il n'a donc contesté
|
||||||
|
# ni le code ni l'identifiant, et l'appelant peut retenir ce dernier
|
||||||
|
# plutôt que de le redemander à chaque essai (cf. AccessCodeDialog).
|
||||||
return AuthResult(False, f"Serveur d'authentification injoignable "
|
return AuthResult(False, f"Serveur d'authentification injoignable "
|
||||||
f"({base}) : {exc.reason}")
|
f"({base}) : {exc.reason}", None, False)
|
||||||
except (OSError, ValueError) as exc:
|
except (OSError, ValueError) as exc:
|
||||||
return AuthResult(False, f"Échec de l'authentification : {exc}")
|
return AuthResult(False, f"Échec de l'authentification : {exc}",
|
||||||
|
None, False)
|
||||||
|
|||||||
+33
-5
@@ -615,12 +615,34 @@ def _networks(value: str) -> list[ipaddress.IPv4Network | ipaddress.IPv6Network]
|
|||||||
return nets
|
return nets
|
||||||
|
|
||||||
|
|
||||||
|
# Réseau supposé autour de l'adresse du client quand rien d'autre ne permet
|
||||||
|
# de le déterminer : les conventions les plus répandues pour un tunnel.
|
||||||
|
_TUNNEL_FALLBACK_PREFIX = {4: 24, 6: 64}
|
||||||
|
|
||||||
|
|
||||||
def tunnel_gateway(client_address: str, allowed_ips: str = "") -> str:
|
def tunnel_gateway(client_address: str, allowed_ips: str = "") -> str:
|
||||||
"""Première IP utilisable du réseau du tunnel (typiquement le serveur).
|
"""Première IP utilisable du réseau du tunnel (typiquement le serveur).
|
||||||
|
|
||||||
Une adresse client en /32 — la forme la plus courante — ne décrit aucun
|
Une adresse client en /32 — la forme la plus courante — ne décrit aucun
|
||||||
réseau : on se replie alors sur le premier réseau des « IPs autorisées »,
|
réseau : on se replie alors sur les « IPs autorisées ».
|
||||||
qui contient le pair distant.
|
|
||||||
|
Trois pièges, tous rencontrés :
|
||||||
|
|
||||||
|
- Une route par défaut (`0.0.0.0/0`, `::/0`) est une IP autorisée
|
||||||
|
parfaitement banale — c'est le tunnel intégral — mais ne désigne aucun
|
||||||
|
réseau de tunnel. La retenir donnait pour passerelle son premier hôte,
|
||||||
|
`0.0.0.1`, injoignable par construction.
|
||||||
|
- Les IPs autorisées listent aussi les réseaux *distants*, joints à
|
||||||
|
travers le tunnel. Le serveur n'y est pas : il est dans le réseau du
|
||||||
|
tunnel, celui qui contient l'adresse du client. D'où le filtre sur
|
||||||
|
l'appartenance.
|
||||||
|
- Plusieurs réseaux peuvent contenir le client. Le plus spécifique est le
|
||||||
|
bon : un /16 englobant décrirait un plan d'adressage, pas ce tunnel-ci.
|
||||||
|
|
||||||
|
Faute de tout candidat, on suppose le réseau usuel autour de l'adresse du
|
||||||
|
client — /24 en IPv4, /64 en IPv6. C'est ce que faisait l'ancienne
|
||||||
|
déduction, et elle avait raison sur ce point : mieux vaut une convention
|
||||||
|
répandue qu'aucune adresse du tout.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
iface = ipaddress.ip_interface(client_address.split(",")[0].strip())
|
iface = ipaddress.ip_interface(client_address.split(",")[0].strip())
|
||||||
@@ -630,9 +652,15 @@ def tunnel_gateway(client_address: str, allowed_ips: str = "") -> str:
|
|||||||
candidates = []
|
candidates = []
|
||||||
if iface.network.prefixlen < iface.network.max_prefixlen - 1:
|
if iface.network.prefixlen < iface.network.max_prefixlen - 1:
|
||||||
candidates.append(iface.network)
|
candidates.append(iface.network)
|
||||||
candidates += [n for n in _networks(allowed_ips)
|
usable = [n for n in _networks(allowed_ips)
|
||||||
if n.prefixlen < n.max_prefixlen - 1
|
if n.version == iface.ip.version
|
||||||
and n.version == iface.ip.version]
|
and 0 < n.prefixlen < n.max_prefixlen - 1
|
||||||
|
and iface.ip in n]
|
||||||
|
candidates += sorted(usable, key=lambda n: n.prefixlen, reverse=True)
|
||||||
|
if not candidates:
|
||||||
|
candidates.append(ipaddress.ip_network(
|
||||||
|
f"{iface.ip}/{_TUNNEL_FALLBACK_PREFIX[iface.ip.version]}",
|
||||||
|
strict=False))
|
||||||
|
|
||||||
for net in candidates:
|
for net in candidates:
|
||||||
for host in net.hosts():
|
for host in net.hosts():
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ class AccessCodeDialog(QDialog):
|
|||||||
self._worker: ValueWorker | None = None
|
self._worker: ValueWorker | None = None
|
||||||
self._message = ""
|
self._message = ""
|
||||||
self._access_until = None
|
self._access_until = None
|
||||||
self._username = ""
|
|
||||||
self.setWindowTitle("WGSecure — Code d'accès")
|
self.setWindowTitle("WGSecure — Code d'accès")
|
||||||
self.setFixedWidth(360)
|
self.setFixedWidth(360)
|
||||||
self.setModal(True)
|
self.setModal(True)
|
||||||
@@ -88,19 +87,11 @@ class AccessCodeDialog(QDialog):
|
|||||||
sep.setStyleSheet(f"color: {theme.BORDER_SOFT};")
|
sep.setStyleSheet(f"color: {theme.BORDER_SOFT};")
|
||||||
layout.addWidget(sep)
|
layout.addWidget(sep)
|
||||||
|
|
||||||
# L'identifiant n'est demandé que s'il manque encore. Le renvoyer au
|
# Aucun identifiant n'est demandé : le serveur reconnaît le poste à
|
||||||
# panneau Administrateur à cet instant précis — tunnel monté, accès
|
# l'IP source de son tunnel. Ce champ existait parce que le serveur
|
||||||
# fermé, code en main — était un cul-de-sac : il est saisi ici, puis
|
# exigeait l'identifiant, et il n'apportait que des ennuis — une
|
||||||
# enregistré dès que le serveur l'a reconnu.
|
# faute de frappe faisait refuser un code valide, et un poste n'ayant
|
||||||
self._user_input: QLineEdit | None = None
|
# jamais réussi à s'authentifier se le voyait redemander sans fin.
|
||||||
if not (self._cfg.wg.get("vpn_username") or "").strip():
|
|
||||||
self._user_input = QLineEdit()
|
|
||||||
self._user_input.setPlaceholderText("Identifiant VPN (ex. TestB)")
|
|
||||||
self._user_input.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
self._user_input.setStyleSheet(self._input_css(theme.BORDER_SOFT))
|
|
||||||
self._user_input.returnPressed.connect(self._submit)
|
|
||||||
layout.addWidget(self._user_input)
|
|
||||||
|
|
||||||
self._code_input = QLineEdit()
|
self._code_input = QLineEdit()
|
||||||
self._code_input.setPlaceholderText("000 000")
|
self._code_input.setPlaceholderText("000 000")
|
||||||
self._code_input.setMaxLength(7)
|
self._code_input.setMaxLength(7)
|
||||||
@@ -188,18 +179,10 @@ class AccessCodeDialog(QDialog):
|
|||||||
self._fail("Entrez exactement 6 chiffres.")
|
self._fail("Entrez exactement 6 chiffres.")
|
||||||
return
|
return
|
||||||
|
|
||||||
self._username = ""
|
|
||||||
if self._user_input is not None:
|
|
||||||
self._username = self._user_input.text().strip()
|
|
||||||
if not self._username:
|
|
||||||
self._error_label.setText("Indiquez votre identifiant VPN.")
|
|
||||||
self._user_input.setStyleSheet(self._input_css(theme.FAIL_TEXT))
|
|
||||||
return
|
|
||||||
|
|
||||||
self._error_label.setText("")
|
self._error_label.setText("")
|
||||||
self._set_busy(True)
|
self._set_busy(True)
|
||||||
self._worker = ValueWorker(vpn_session.authenticate, self._cfg, raw,
|
self._worker = ValueWorker(vpn_session.authenticate, self._cfg, raw,
|
||||||
self._username, parent=self)
|
parent=self)
|
||||||
self._worker.done.connect(self._on_reply)
|
self._worker.done.connect(self._on_reply)
|
||||||
self._worker.finished.connect(self._worker.deleteLater)
|
self._worker.finished.connect(self._worker.deleteLater)
|
||||||
self._worker.start()
|
self._worker.start()
|
||||||
@@ -210,15 +193,10 @@ class AccessCodeDialog(QDialog):
|
|||||||
# `ValueWorker` absorbe une exception imprévue en rendant None :
|
# `ValueWorker` absorbe une exception imprévue en rendant None :
|
||||||
# dire alors que l'échec vient d'ici, et non d'un refus serveur.
|
# dire alors que l'échec vient d'ici, et non d'un refus serveur.
|
||||||
result = vpn_session.AuthResult(
|
result = vpn_session.AuthResult(
|
||||||
False, "Échec inattendu de l'authentification.")
|
False, "Échec inattendu de l'authentification.", None, False)
|
||||||
self._message = result.message
|
self._message = result.message
|
||||||
self._access_until = result.access_until
|
self._access_until = result.access_until
|
||||||
if result.ok:
|
if result.ok:
|
||||||
# Enregistré seulement maintenant : un identifiant que le serveur
|
|
||||||
# a refusé n'a rien à faire dans la configuration.
|
|
||||||
if self._username:
|
|
||||||
self._cfg.set("wg", "vpn_username", self._username)
|
|
||||||
self._cfg.save()
|
|
||||||
self.accept()
|
self.accept()
|
||||||
return
|
return
|
||||||
self._set_busy(False)
|
self._set_busy(False)
|
||||||
@@ -233,8 +211,6 @@ class AccessCodeDialog(QDialog):
|
|||||||
self._btn_ok.setEnabled(not busy)
|
self._btn_ok.setEnabled(not busy)
|
||||||
self._btn_ok.setText("Vérification…" if busy else "Valider")
|
self._btn_ok.setText("Vérification…" if busy else "Valider")
|
||||||
self._code_input.setEnabled(not busy)
|
self._code_input.setEnabled(not busy)
|
||||||
if self._user_input is not None:
|
|
||||||
self._user_input.setEnabled(not busy)
|
|
||||||
# « Plus tard » est neutralisé le temps de la requête : la fermeture
|
# « Plus tard » est neutralisé le temps de la requête : la fermeture
|
||||||
# rendait la main à l'appelant sur un refus, alors que le serveur
|
# rendait la main à l'appelant sur un refus, alors que le serveur
|
||||||
# pouvait encore répondre par une acceptation. Celle-ci arrivait après
|
# pouvait encore répondre par une acceptation. Celle-ci arrivait après
|
||||||
|
|||||||
@@ -396,8 +396,9 @@ class AdminWindow(QDialog):
|
|||||||
self._vpn_username.setPlaceholderText("TestB")
|
self._vpn_username.setPlaceholderText("TestB")
|
||||||
self._vpn_username.setToolTip(
|
self._vpn_username.setToolTip(
|
||||||
"Nom du compte VPN tel qu'il figure sur le serveur.\n"
|
"Nom du compte VPN tel qu'il figure sur le serveur.\n"
|
||||||
"Il sert à valider votre code à 6 chiffres après la connexion.\n"
|
"Purement indicatif : le serveur reconnaît ce poste à l'adresse\n"
|
||||||
"Laissé vide, aucun code n'est demandé (serveur en mode permissif)."
|
"de son tunnel, et l'identifiant ne lui est plus transmis.\n"
|
||||||
|
"Le laisser vide ne change rien au fonctionnement."
|
||||||
)
|
)
|
||||||
self._row(form2, "Identifiant VPN :", self._vpn_username)
|
self._row(form2, "Identifiant VPN :", self._vpn_username)
|
||||||
|
|
||||||
|
|||||||
+42
-14
@@ -76,6 +76,11 @@ class MainWindow(QMainWindow):
|
|||||||
self._access_probed_at: datetime | None = None
|
self._access_probed_at: datetime | None = None
|
||||||
self._access_denials = 0
|
self._access_denials = 0
|
||||||
self._access_message = ""
|
self._access_message = ""
|
||||||
|
# Le serveur connaît-il un pair à notre adresse de tunnel ? `None`
|
||||||
|
# tant qu'il n'a pas répondu. Ce constat remplace l'ancien « un
|
||||||
|
# identifiant VPN est-il configuré ? », qui prétendait répondre à la
|
||||||
|
# question depuis le poste alors que seul le serveur la tranche.
|
||||||
|
self._access_enrolled: bool | None = None
|
||||||
# Un seul nouvel essai quand le serveur reste muet alors qu'une saisie
|
# Un seul nouvel essai quand le serveur reste muet alors qu'une saisie
|
||||||
# devait être proposée : au-delà, le bouton prend le relais plutôt que
|
# devait être proposée : au-delà, le bouton prend le relais plutôt que
|
||||||
# de faire tourner un sondage en boucle.
|
# de faire tourner un sondage en boucle.
|
||||||
@@ -715,9 +720,16 @@ class MainWindow(QMainWindow):
|
|||||||
# L'autorisation a couru puis expiré : le dire, plutôt que de
|
# L'autorisation a couru puis expiré : le dire, plutôt que de
|
||||||
# laisser croire qu'aucun code n'a jamais été saisi.
|
# laisser croire qu'aucun code n'a jamais été saisi.
|
||||||
return "Autorisation expirée (code requis)"
|
return "Autorisation expirée (code requis)"
|
||||||
if (self._cfg.wg.get("vpn_username") or "").strip():
|
if self._access_enrolled is False:
|
||||||
return "Quarantaine (code requis)"
|
# Le serveur affirme ne connaître aucun pair à cette adresse :
|
||||||
return "Non vérifié"
|
# aucun code n'ouvrira quoi que ce soit ici.
|
||||||
|
return "Non géré par le serveur"
|
||||||
|
if self._access_enrolled is None:
|
||||||
|
# Rien demandé, ou serveur muet : le client n'en sait rien et ne
|
||||||
|
# prétend donc rien. Il l'affirmait autrefois d'après sa seule
|
||||||
|
# configuration locale, et se trompait.
|
||||||
|
return "Non vérifié"
|
||||||
|
return "Quarantaine (code requis)"
|
||||||
|
|
||||||
def _refresh_status(self):
|
def _refresh_status(self):
|
||||||
wg = self._cfg.wg
|
wg = self._cfg.wg
|
||||||
@@ -768,6 +780,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._access_denials = 0
|
self._access_denials = 0
|
||||||
self._access_message = ""
|
self._access_message = ""
|
||||||
self._probe_retried = False
|
self._probe_retried = False
|
||||||
|
self._access_enrolled = None
|
||||||
self._info_labels["access"].setText(self._access_state())
|
self._info_labels["access"].setText(self._access_state())
|
||||||
|
|
||||||
# Le bouton n'apparaît que lorsqu'il y a réellement un code à saisir ;
|
# Le bouton n'apparaît que lorsqu'il y a réellement un code à saisir ;
|
||||||
@@ -780,12 +793,11 @@ class MainWindow(QMainWindow):
|
|||||||
self._btn_access.setVisible(needed)
|
self._btn_access.setVisible(needed)
|
||||||
self._sync_window_height()
|
self._sync_window_height()
|
||||||
if needed:
|
if needed:
|
||||||
# Un code est attendu de façon certaine quand l'identifiant VPN
|
# Un code est attendu de façon certaine quand le serveur a
|
||||||
# est connu ; sinon le bouton reste disponible mais discret, pour
|
# reconnu ce pair ; sinon le bouton reste disponible mais
|
||||||
# ne pas réclamer une action à une installation qui n'a pas de
|
# discret, pour ne pas réclamer une action à une installation
|
||||||
# serveur d'authentification.
|
# sans serveur d'authentification.
|
||||||
self._style_access_button(
|
self._style_access_button(self._access_enrolled is True)
|
||||||
bool((self._cfg.wg.get("vpn_username") or "").strip()))
|
|
||||||
|
|
||||||
if connected:
|
if connected:
|
||||||
if self._connected_since is None:
|
if self._connected_since is None:
|
||||||
@@ -932,11 +944,14 @@ class MainWindow(QMainWindow):
|
|||||||
#
|
#
|
||||||
# Une reconnexion automatique ne fait jamais surgir de fenêtre modale
|
# Une reconnexion automatique ne fait jamais surgir de fenêtre modale
|
||||||
# — personne n'est devant l'écran, l'application resterait figée ; le
|
# — personne n'est devant l'écran, l'application resterait figée ; le
|
||||||
# bouton prend le relais. Sans identifiant VPN configuré non plus :
|
# bouton prend le relais.
|
||||||
# une installation sans serveur d'authentification n'a pas à voir
|
#
|
||||||
# cette fenêtre à chaque connexion.
|
# Le cas « pas de serveur d'authentification » n'est plus tranché ici :
|
||||||
known = bool((self._cfg.wg.get("vpn_username") or "").strip())
|
# il l'était d'après la présence d'un identifiant VPN en configuration,
|
||||||
self._probe_access(prompt_if_closed=(not silent) and known)
|
# ce que le poste ne pouvait pas savoir. C'est `_on_probe_reply` qui
|
||||||
|
# renonce à l'invite quand le serveur répond ne connaître aucun pair à
|
||||||
|
# cette adresse — avec sa réponse en main, plutôt qu'avant de l'avoir.
|
||||||
|
self._probe_access(prompt_if_closed=not silent)
|
||||||
|
|
||||||
def _probe_access(self, prompt_if_closed: bool):
|
def _probe_access(self, prompt_if_closed: bool):
|
||||||
"""Interroge le serveur sur l'état de l'accès, sans bloquer l'écran.
|
"""Interroge le serveur sur l'état de l'accès, sans bloquer l'écran.
|
||||||
@@ -975,6 +990,10 @@ class MainWindow(QMainWindow):
|
|||||||
self._probe_prompt = False
|
self._probe_prompt = False
|
||||||
was_open = self._access_open
|
was_open = self._access_open
|
||||||
self._access_probed_at = datetime.now(timezone.utc)
|
self._access_probed_at = datetime.now(timezone.utc)
|
||||||
|
# Seule une réponse du serveur renseigne l'enrôlement ; son silence
|
||||||
|
# laisse le constat précédent en place plutôt que de le contredire.
|
||||||
|
if result is not None and result.authoritative:
|
||||||
|
self._access_enrolled = result.enrolled
|
||||||
|
|
||||||
if result is not None and result.ok:
|
if result is not None and result.ok:
|
||||||
self._access_open = True
|
self._access_open = True
|
||||||
@@ -1032,6 +1051,15 @@ class MainWindow(QMainWindow):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self._probe_retried = False
|
self._probe_retried = False
|
||||||
|
if prompt and self._access_enrolled is False:
|
||||||
|
# Le serveur ne connaît aucun pair à cette adresse : lui réclamer
|
||||||
|
# un code serait absurde, aucun ne l'ouvrira. Le bouton reste
|
||||||
|
# atteignable — discret — car le poste peut être enrôlé plus tard
|
||||||
|
# sans que rien ici ne l'apprenne.
|
||||||
|
self._add_log("Ce poste n'est pas enrôlé sur le serveur "
|
||||||
|
"d'authentification", "warning")
|
||||||
|
self._refresh_status()
|
||||||
|
return
|
||||||
if prompt:
|
if prompt:
|
||||||
self._open_access_dialog()
|
self._open_access_dialog()
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user