Files
pyssh-client/app/bastion.py
Florian c39ef9c038 Qualite : pyproject.toml (ruff/pytest/pyright) + CI Gitea (lint/tests/build)
pyproject.toml devient le point d'entree unique de configuration des outils
(pytest.ini supprime, sa config y est deplacee). Les dependances restent dans
requirements.txt/requirements-dev.txt (deja utilises par setup.bat) plutot que
dupliquees ici : ce projet se distribue par PyInstaller, pas par "pip install".

ruff : configure puis passe sur tout app/ + tests/ (154 corrections
automatiques, purement mecaniques - tri des imports, imports multiples sur une
ligne separes, alias depreciees) suivies de 7 corrections manuelles ciblees :
  - app/ssh_connection.py : `raise ... from None` pour le refus explicite de
    passphrase (ce n'est pas un echec a diagnostiquer).
  - app/controllers/workspace_manager.py : zip() sans strict= documente en
    commentaire (deliberement tolerant a un workspace legerement perime).
  - tests/test_ai_redaction.py : nom de variable ambigu renomme.
  - E702 (construction compacte "widget = X(); widget.setY(...)" dans
    sftp_panel.py/tunnel_dialog.py) et E402 (quelques tests qui importent un
    symbole juste avant la section qui l'utilise) : ignores explicitement, ce
    sont des styles deliberes preexistants, pas des oublis.
`ruff check app main.py tests` est desormais propre.

pyright : mode "basic" (le code n'a jamais ete type). Plus de 200 signalements
sur ~300 etaient le meme faux positif connu de l'ecosysteme PySide6 (acces
plat aux enums Qt, ex. Qt.UserRole, valide a l'execution mais que les stubs ne
reconnaissent que sous leur forme imbriquee) : desactive avec justification en
commentaire. Reste un socle de 77 signalements reels (surtout des acces
Optional non filtres dans app/ui/tabs.py et app/ssh_connection.py) : non
corrige dans cette phase - resserrer le typage la ou pyright a raison est un
travail a part, pas une consequence automatique de l'ajout de l'outil. La CI
le lance en mode informatif (continue-on-error) tant que ce socle n'est pas
resorbe.

.gitea/workflows/ci.yml : lint (ruff, bloquant), tests (pytest, bloquant),
typecheck (pyright, informatif), build (PyInstaller, sur main uniquement,
artefact du dossier portable). Necessite un runner Gitea Actions Windows
enregistre - PySide6/pywinpty/Credential Manager/ACL NTFS sont Windows
uniquement, comme l'application elle-meme ; non verifiable depuis ce poste.

Suite complete revalidee apres coup (33/33) pour confirmer qu'aucune des 154
corrections automatiques n'a modifie de comportement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 07:46:50 +02:00

89 lines
3.4 KiB
Python

"""Resolution d'un BastionConfig (session/openssh/manuel) en parametres de
connexion concrets, independamment de la source choisie dans l'editeur.
La connexion elle-meme (ouverture du canal direct-tcpip a travers le bastion)
est geree par SSHConnection (app.ssh_connection), qui consomme un BastionTarget
sans se soucier de sa provenance.
"""
from __future__ import annotations
from dataclasses import dataclass
from app.credentials import session_credential_key
from app.session_manager import (
AUTH_AUTO,
AUTH_KEY,
BASTION_MANUAL,
BASTION_NONE,
BASTION_OPENSSH,
BASTION_SESSION,
BastionConfig,
SessionStore,
)
class BastionResolutionError(Exception):
"""Le bastion configure ne peut pas etre resolu (reference cassee, config
invalide...). Empeche de lancer une connexion vouee a l'echec."""
@dataclass
class BastionTarget:
"""Parametres de connexion resolus pour le saut a franchir avant la cible."""
host: str
port: int
username: str
auth_type: str
key_path: str = ""
# Non vide seulement pour un bastion en mode "session PySSH" : permet de
# reutiliser un mot de passe deja memorise pour cette session (voir
# app.credentials), sans que ce module ait besoin de connaitre le
# CredentialStore lui-meme.
credential_key: str = ""
@property
def label(self) -> str:
return f"{self.username}@{self.host}" if self.username else self.host
def resolve_bastion_target(
bastion: BastionConfig | None, session_store: SessionStore | None = None,
) -> BastionTarget | None:
"""Renvoie None si aucun bastion n'est configure. Leve BastionResolutionError
si le mode choisi ne peut pas etre resolu en ce moment (session supprimee,
alias OpenSSH introuvable, config manuelle incomplete...)."""
if bastion is None or bastion.mode in (BASTION_NONE, ""):
return None
if bastion.mode == BASTION_MANUAL:
if not bastion.host.strip():
raise BastionResolutionError("Bastion manuel : aucune adresse renseignee.")
return BastionTarget(bastion.host.strip(), bastion.port, bastion.username, bastion.auth_type, bastion.key_path)
if bastion.mode == BASTION_SESSION:
if session_store is None:
raise BastionResolutionError("Bastion vers une session PySSH : aucun registre de sessions disponible.")
target = session_store.get(bastion.session_id)
if target is None:
raise BastionResolutionError(
"La session utilisee comme bastion est introuvable (supprimee ou renommee)."
)
return BastionTarget(
target.host, target.port, target.username, target.auth_type, target.key_path,
credential_key=session_credential_key(target.id),
)
if bastion.mode == BASTION_OPENSSH:
from app.openssh.config import load_openssh_hosts
host = next((h for h in load_openssh_hosts() if h.alias == bastion.openssh_alias), None)
if host is None:
raise BastionResolutionError(
f"Host OpenSSH « {bastion.openssh_alias} » introuvable dans ~/.ssh/config."
)
key_path = host.identity_files[0] if host.identity_files else ""
auth_type = AUTH_KEY if key_path else AUTH_AUTO
return BastionTarget(host.hostname or host.alias, host.port, host.user, auth_type, key_path)
raise BastionResolutionError(f"Mode de bastion inconnu : {bastion.mode}")