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:
tuxgyver
2026-09-08 11:10:42 +02:00
co-authored by Claude Opus 5
parent b3c0515244
commit 023ec81e03
5 changed files with 120 additions and 66 deletions
+42 -14
View File
@@ -76,6 +76,11 @@ class MainWindow(QMainWindow):
self._access_probed_at: datetime | None = None
self._access_denials = 0
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
# devait être proposée : au-delà, le bouton prend le relais plutôt que
# 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
# laisser croire qu'aucun code n'a jamais été saisi.
return "Autorisation expirée (code requis)"
if (self._cfg.wg.get("vpn_username") or "").strip():
return "Quarantaine (code requis)"
return "Non vérifié"
if self._access_enrolled is False:
# Le serveur affirme ne connaître aucun pair à cette adresse :
# 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):
wg = self._cfg.wg
@@ -768,6 +780,7 @@ class MainWindow(QMainWindow):
self._access_denials = 0
self._access_message = ""
self._probe_retried = False
self._access_enrolled = None
self._info_labels["access"].setText(self._access_state())
# 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._sync_window_height()
if needed:
# Un code est attendu de façon certaine quand l'identifiant VPN
# est connu ; sinon le bouton reste disponible mais discret, pour
# ne pas réclamer une action à une installation qui n'a pas de
# serveur d'authentification.
self._style_access_button(
bool((self._cfg.wg.get("vpn_username") or "").strip()))
# Un code est attendu de façon certaine quand le serveur a
# reconnu ce pair ; sinon le bouton reste disponible mais
# discret, pour ne pas réclamer une action à une installation
# sans serveur d'authentification.
self._style_access_button(self._access_enrolled is True)
if connected:
if self._connected_since is None:
@@ -932,11 +944,14 @@ class MainWindow(QMainWindow):
#
# Une reconnexion automatique ne fait jamais surgir de fenêtre modale
# — personne n'est devant l'écran, l'application resterait figée ; le
# bouton prend le relais. Sans identifiant VPN configuré non plus :
# une installation sans serveur d'authentification n'a pas à voir
# cette fenêtre à chaque connexion.
known = bool((self._cfg.wg.get("vpn_username") or "").strip())
self._probe_access(prompt_if_closed=(not silent) and known)
# bouton prend le relais.
#
# Le cas « pas de serveur d'authentification » n'est plus tranché ici :
# il l'était d'après la présence d'un identifiant VPN en configuration,
# 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):
"""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
was_open = self._access_open
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:
self._access_open = True
@@ -1032,6 +1051,15 @@ class MainWindow(QMainWindow):
return
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:
self._open_access_dialog()
return