Par defaut, inchange : les mots de passe sont toujours redemandes a chaque
connexion. Nouvelle case a cocher optionnelle dans l'editeur de session,
visible seulement en authentification par mot de passe : "Memoriser le mot
de passe de maniere securisee".
- app/credentials.py : abstraction CredentialStore (set_secret/get_secret/
delete_secret/is_available), pour supporter plus tard d'autres systemes que
Windows. WindowsCredentialStore utilise Windows Credential Manager
(CredWrite/CredRead/CredDelete) via pywin32-ctypes - deja present dans
l'environnement (dependance de PyInstaller), mais pas encore declare comme
dependance d'execution : ajoute a requirements.txt (marque sys_platform ==
"win32"), sinon la fonctionnalite aurait silencieusement ete indisponible
sur une installation propre. NullCredentialStore en repli si indisponible :
jamais d'ecriture en clair a la place.
- Session.password_storage ("none" par defaut | "secure") : seulement le mode
est stocke dans sessions.json, jamais le secret. Le mot de passe est ecrit/
lu dans le trousseau via une cle applicative (session:<id>), et supprime
automatiquement si l'utilisateur desactive la memorisation ou supprime la
session.
- Un bastion en mode "session PySSH" reutilise un mot de passe deja memorise
pour cette session (lecture seule depuis ce chemin : l'ecriture ne se fait
que via l'ouverture directe de la session concernee).
tests/test_credentials.py : NullCredentialStore, aller-retour reel avec
Windows Credential Manager (nettoye et verifie apres coup, aucune entree
residuelle), integration ConnectionController (memorise puis reutilise sans
redemander), suppression en cascade, reutilisation par un bastion, et
verification que le secret n'apparait jamais dans la serialisation JSON de
la session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
482 lines
24 KiB
Python
482 lines
24 KiB
Python
"""Boite de dialogue de creation/edition d'une session SSH."""
|
|
from __future__ import annotations
|
|
|
|
from PySide6.QtCore import Qt
|
|
from PySide6.QtWidgets import (
|
|
QDialog, QFormLayout, QLineEdit, QSpinBox, QComboBox, QDialogButtonBox,
|
|
QHBoxLayout, QPushButton, QTextEdit, QFileDialog, QWidget, QVBoxLayout,
|
|
QCheckBox, QMessageBox, QGroupBox, QStackedWidget, QCompleter,
|
|
)
|
|
|
|
from app.session_manager import (
|
|
Session, SessionStore, BastionConfig, AUTH_AUTO, AUTH_KEY, AUTH_PASSWORD, AUTH_AGENT,
|
|
BASTION_NONE, BASTION_SESSION, BASTION_OPENSSH, BASTION_MANUAL,
|
|
PASSWORD_STORAGE_NONE, PASSWORD_STORAGE_SECURE,
|
|
)
|
|
from app.key_manager import KeyManager
|
|
from app.openssh.config import load_openssh_hosts
|
|
from app.groups import GroupSettingsStore
|
|
from app.credentials import CredentialStore, NullCredentialStore, session_credential_key
|
|
|
|
|
|
class SessionDialog(QDialog):
|
|
"""Formulaire de session. En mode `connect_label`, il sert de connexion rapide :
|
|
on saisit les infos et on se connecte directement, l'enregistrement etant optionnel."""
|
|
|
|
def __init__(
|
|
self, key_manager: KeyManager, session: Session | None = None,
|
|
parent=None, connect_label: str = "", title: str = "",
|
|
session_store: SessionStore | None = None, group_store: GroupSettingsStore | None = None,
|
|
credential_store: CredentialStore | None = None,
|
|
):
|
|
super().__init__(parent)
|
|
self.key_manager = key_manager
|
|
self.session_store = session_store
|
|
self.group_store = group_store
|
|
self.credential_store = credential_store or NullCredentialStore()
|
|
self.session = session or Session()
|
|
self.quick_mode = bool(connect_label)
|
|
self.setWindowTitle(title or ("Nouvelle session" if session is None else "Modifier la session"))
|
|
self.setMinimumWidth(460)
|
|
|
|
layout = QVBoxLayout(self)
|
|
form = QFormLayout()
|
|
|
|
self.name_edit = QLineEdit(self.session.name)
|
|
self.group_edit = QLineEdit(self.session.group)
|
|
self.group_edit.setPlaceholderText("ex: Production/Web")
|
|
self.inherit_group_btn = QPushButton("↓ Heriter du groupe")
|
|
self.inherit_group_btn.setToolTip(
|
|
"Pre-remplit ce formulaire avec les valeurs par defaut enregistrees pour ce dossier"
|
|
" (Parametres du groupe..., dans le panneau des sessions)."
|
|
)
|
|
self.inherit_group_btn.clicked.connect(self._apply_group_defaults)
|
|
group_row = QWidget()
|
|
group_layout = QHBoxLayout(group_row)
|
|
group_layout.setContentsMargins(0, 0, 0, 0)
|
|
group_layout.addWidget(self.group_edit, 1)
|
|
group_layout.addWidget(self.inherit_group_btn)
|
|
|
|
self.tags_edit = QLineEdit(", ".join(self.session.tags))
|
|
self.tags_edit.setPlaceholderText("ex: prod, docker, client-x (separes par des virgules)")
|
|
if self.session_store is not None:
|
|
completer = QCompleter(self.session_store.tags(), self.tags_edit)
|
|
completer.setCaseSensitivity(Qt.CaseInsensitive)
|
|
self.tags_edit.setCompleter(completer)
|
|
|
|
self.host_edit = QLineEdit(self.session.host)
|
|
self.port_spin = QSpinBox()
|
|
self.port_spin.setRange(1, 65535)
|
|
self.port_spin.setValue(self.session.port or 22)
|
|
self.user_edit = QLineEdit(self.session.username)
|
|
|
|
self.auth_combo = QComboBox()
|
|
self.auth_combo.addItem("Automatique (comme la commande ssh)", AUTH_AUTO)
|
|
self.auth_combo.addItem("Cle SSH choisie", AUTH_KEY)
|
|
self.auth_combo.addItem("Mot de passe (saisi a la connexion)", AUTH_PASSWORD)
|
|
self.auth_combo.addItem("Agent SSH", AUTH_AGENT)
|
|
self.auth_combo.setToolTip(
|
|
"Automatique : utilise l'agent SSH et vos cles ~/.ssh comme le fait la commande ssh,\n"
|
|
"et demande un mot de passe seulement si aucune cle ne convient."
|
|
)
|
|
idx = self.auth_combo.findData(self.session.auth_type)
|
|
self.auth_combo.setCurrentIndex(idx if idx >= 0 else 0)
|
|
|
|
self.key_combo = QComboBox()
|
|
self._reload_keys()
|
|
self.browse_key_btn = QPushButton("Parcourir...")
|
|
|
|
key_row = QWidget()
|
|
key_layout = QHBoxLayout(key_row)
|
|
key_layout.setContentsMargins(0, 0, 0, 0)
|
|
key_layout.addWidget(self.key_combo, 1)
|
|
key_layout.addWidget(self.browse_key_btn)
|
|
|
|
self.notes_edit = QTextEdit(self.session.notes)
|
|
self.notes_edit.setFixedHeight(60)
|
|
|
|
form.addRow("Nom :", self.name_edit)
|
|
form.addRow("Dossier :", group_row)
|
|
form.addRow("Tags :", self.tags_edit)
|
|
self.remember_password_check = QCheckBox("Memoriser le mot de passe de maniere securisee")
|
|
self.remember_password_check.setChecked(self.session.password_storage == PASSWORD_STORAGE_SECURE)
|
|
if self.credential_store.is_available():
|
|
self.remember_password_check.setToolTip(
|
|
"Stocke dans le trousseau securise du systeme (Windows Credential Manager), "
|
|
"jamais en clair dans les fichiers de PySSH."
|
|
)
|
|
else:
|
|
self.remember_password_check.setEnabled(False)
|
|
self.remember_password_check.setChecked(False)
|
|
self.remember_password_check.setToolTip(
|
|
"Aucun trousseau securise disponible sur ce systeme : le mot de passe sera "
|
|
"toujours redemande a la connexion."
|
|
)
|
|
|
|
form.addRow("Hote :", self.host_edit)
|
|
form.addRow("Port :", self.port_spin)
|
|
form.addRow("Utilisateur :", self.user_edit)
|
|
form.addRow("Authentification :", self.auth_combo)
|
|
form.addRow("Cle privee :", key_row)
|
|
form.addRow("", self.remember_password_check)
|
|
form.addRow("Notes :", self.notes_edit)
|
|
|
|
self.form = form
|
|
layout.addLayout(form)
|
|
layout.addWidget(self._build_bastion_group())
|
|
layout.addWidget(self._build_keepalive_group())
|
|
layout.addWidget(self._build_reconnect_group())
|
|
|
|
self.save_check = QCheckBox("Enregistrer dans mes sessions (optionnel)")
|
|
if self.quick_mode:
|
|
# decoche par defaut : les serveurs de test changent d'IP (DHCP), les
|
|
# enregistrer systematiquement encombre la liste pour rien
|
|
self.save_check.setChecked(False)
|
|
layout.addWidget(self.save_check)
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
if self.quick_mode:
|
|
buttons.button(QDialogButtonBox.Ok).setText(connect_label)
|
|
buttons.accepted.connect(self._on_accept)
|
|
buttons.rejected.connect(self.reject)
|
|
layout.addWidget(buttons)
|
|
|
|
self.auth_combo.currentIndexChanged.connect(self._update_key_row)
|
|
self.browse_key_btn.clicked.connect(self._browse_key)
|
|
self._update_key_row()
|
|
|
|
if self.session.key_path:
|
|
existing = self.key_combo.findData(self.session.key_path)
|
|
if existing >= 0:
|
|
self.key_combo.setCurrentIndex(existing)
|
|
else:
|
|
self.key_combo.addItem(self.session.key_path, self.session.key_path)
|
|
self.key_combo.setCurrentIndex(self.key_combo.count() - 1)
|
|
|
|
def _reload_keys(self) -> None:
|
|
self.key_combo.clear()
|
|
for entry in self.key_manager.keys:
|
|
self.key_combo.addItem(f"{entry.name} ({entry.key_type})", entry.private_path)
|
|
|
|
def _update_key_row(self) -> None:
|
|
is_key_auth = self.auth_combo.currentData() == AUTH_KEY
|
|
self.key_combo.setEnabled(is_key_auth)
|
|
self.browse_key_btn.setEnabled(is_key_auth)
|
|
self.form.setRowVisible(self.remember_password_check, self.auth_combo.currentData() == AUTH_PASSWORD)
|
|
|
|
def _browse_key(self) -> None:
|
|
path, _ = QFileDialog.getOpenFileName(self, "Choisir une cle privee")
|
|
if path:
|
|
self.key_combo.addItem(path, path)
|
|
self.key_combo.setCurrentIndex(self.key_combo.count() - 1)
|
|
|
|
# ---------- heritage des parametres du groupe ----------
|
|
def _apply_group_defaults(self) -> None:
|
|
group_path = self.group_edit.text().strip()
|
|
if not group_path:
|
|
QMessageBox.information(self, "Heriter du groupe", "Indiquez d'abord un dossier.")
|
|
return
|
|
settings = self.group_store.get(group_path) if self.group_store else None
|
|
if settings is None:
|
|
QMessageBox.information(
|
|
self, "Heriter du groupe",
|
|
f"Aucun parametre par defaut enregistre pour « {group_path} ».\n\n"
|
|
"Definissez-en depuis le panneau des sessions : clic droit sur le dossier "
|
|
"-> Parametres du groupe...",
|
|
)
|
|
return
|
|
if settings.username:
|
|
self.user_edit.setText(settings.username)
|
|
if settings.port:
|
|
self.port_spin.setValue(settings.port)
|
|
auth_idx = self.auth_combo.findData(settings.auth_type)
|
|
if auth_idx >= 0:
|
|
self.auth_combo.setCurrentIndex(auth_idx)
|
|
if settings.key_path:
|
|
existing = self.key_combo.findData(settings.key_path)
|
|
if existing >= 0:
|
|
self.key_combo.setCurrentIndex(existing)
|
|
else:
|
|
self.key_combo.addItem(settings.key_path, settings.key_path)
|
|
self.key_combo.setCurrentIndex(self.key_combo.count() - 1)
|
|
if settings.bastion.mode != BASTION_NONE:
|
|
mode_idx = self.bastion_mode_combo.findData(settings.bastion.mode)
|
|
if mode_idx >= 0:
|
|
self.bastion_mode_combo.setCurrentIndex(mode_idx)
|
|
if settings.bastion.mode == BASTION_SESSION:
|
|
found = self.bastion_session_combo.findData(settings.bastion.session_id)
|
|
if found >= 0:
|
|
self.bastion_session_combo.setCurrentIndex(found)
|
|
elif settings.bastion.mode == BASTION_OPENSSH:
|
|
found = self.bastion_openssh_combo.findData(settings.bastion.openssh_alias)
|
|
if found >= 0:
|
|
self.bastion_openssh_combo.setCurrentIndex(found)
|
|
elif settings.bastion.mode == BASTION_MANUAL:
|
|
self.bastion_host_edit.setText(settings.bastion.host)
|
|
self.bastion_port_spin.setValue(settings.bastion.port or 22)
|
|
self.bastion_user_edit.setText(settings.bastion.username)
|
|
self.keepalive_check.setChecked(settings.keepalive_enabled)
|
|
self.keepalive_interval_spin.setValue(settings.keepalive_interval or 30)
|
|
self.keepalive_count_spin.setValue(settings.keepalive_count_max or 3)
|
|
|
|
# ---------- Bastion / Jump Host ----------
|
|
def _build_bastion_group(self) -> QGroupBox:
|
|
"""Un seul saut pour l'instant (voir BastionConfig) : Aucun / une session
|
|
PySSH existante / un Host de ~/.ssh/config / une configuration manuelle."""
|
|
bastion = self.session.bastion
|
|
group = QGroupBox("Bastion / Jump Host")
|
|
v = QVBoxLayout(group)
|
|
|
|
self.bastion_mode_combo = QComboBox()
|
|
self.bastion_mode_combo.addItem("Aucun", BASTION_NONE)
|
|
self.bastion_mode_combo.addItem("Une session PySSH existante", BASTION_SESSION)
|
|
self.bastion_mode_combo.addItem("Un Host de ~/.ssh/config", BASTION_OPENSSH)
|
|
self.bastion_mode_combo.addItem("Configuration manuelle", BASTION_MANUAL)
|
|
v.addWidget(self.bastion_mode_combo)
|
|
|
|
self.bastion_stack = QStackedWidget()
|
|
v.addWidget(self.bastion_stack)
|
|
|
|
self.bastion_stack.addWidget(QWidget()) # BASTION_NONE : rien a afficher
|
|
|
|
session_page = QWidget()
|
|
session_form = QFormLayout(session_page)
|
|
session_form.setContentsMargins(0, 4, 0, 0)
|
|
self.bastion_session_combo = QComboBox()
|
|
others = [
|
|
s for s in (self.session_store.sessions if self.session_store else [])
|
|
if s.id != self.session.id
|
|
]
|
|
if others:
|
|
for s in sorted(others, key=lambda s: s.name.lower()):
|
|
self.bastion_session_combo.addItem(f"{s.name} ({s.username}@{s.host})", s.id)
|
|
else:
|
|
self.bastion_session_combo.addItem("(aucune autre session enregistree)", "")
|
|
self.bastion_session_combo.setEnabled(False)
|
|
session_form.addRow("Session :", self.bastion_session_combo)
|
|
self.bastion_stack.addWidget(session_page)
|
|
|
|
openssh_page = QWidget()
|
|
openssh_form = QFormLayout(openssh_page)
|
|
openssh_form.setContentsMargins(0, 4, 0, 0)
|
|
self.bastion_openssh_combo = QComboBox()
|
|
hosts = load_openssh_hosts()
|
|
if hosts:
|
|
for h in hosts:
|
|
self.bastion_openssh_combo.addItem(f"{h.alias} ({h.display_target})", h.alias)
|
|
else:
|
|
self.bastion_openssh_combo.addItem("(aucun host dans ~/.ssh/config)", "")
|
|
self.bastion_openssh_combo.setEnabled(False)
|
|
openssh_form.addRow("Host :", self.bastion_openssh_combo)
|
|
self.bastion_stack.addWidget(openssh_page)
|
|
|
|
manual_page = QWidget()
|
|
manual_form = QFormLayout(manual_page)
|
|
manual_form.setContentsMargins(0, 4, 0, 0)
|
|
self.bastion_host_edit = QLineEdit(bastion.host if bastion.mode == BASTION_MANUAL else "")
|
|
self.bastion_port_spin = QSpinBox()
|
|
self.bastion_port_spin.setRange(1, 65535)
|
|
self.bastion_port_spin.setValue(bastion.port or 22)
|
|
self.bastion_user_edit = QLineEdit(bastion.username if bastion.mode == BASTION_MANUAL else "")
|
|
self.bastion_auth_combo = QComboBox()
|
|
self.bastion_auth_combo.addItem("Automatique", AUTH_AUTO)
|
|
self.bastion_auth_combo.addItem("Cle SSH choisie", AUTH_KEY)
|
|
self.bastion_auth_combo.addItem("Mot de passe (saisi a la connexion)", AUTH_PASSWORD)
|
|
self.bastion_auth_combo.addItem("Agent SSH", AUTH_AGENT)
|
|
self.bastion_key_combo = QComboBox()
|
|
for entry in self.key_manager.keys:
|
|
self.bastion_key_combo.addItem(f"{entry.name} ({entry.key_type})", entry.private_path)
|
|
self.bastion_browse_key_btn = QPushButton("Parcourir...")
|
|
bastion_key_row = QWidget()
|
|
bastion_key_layout = QHBoxLayout(bastion_key_row)
|
|
bastion_key_layout.setContentsMargins(0, 0, 0, 0)
|
|
bastion_key_layout.addWidget(self.bastion_key_combo, 1)
|
|
bastion_key_layout.addWidget(self.bastion_browse_key_btn)
|
|
|
|
manual_form.addRow("Hote :", self.bastion_host_edit)
|
|
manual_form.addRow("Port :", self.bastion_port_spin)
|
|
manual_form.addRow("Utilisateur :", self.bastion_user_edit)
|
|
manual_form.addRow("Authentification :", self.bastion_auth_combo)
|
|
manual_form.addRow("Cle privee :", bastion_key_row)
|
|
self.bastion_stack.addWidget(manual_page)
|
|
|
|
self.bastion_auth_combo.currentIndexChanged.connect(self._update_bastion_key_row)
|
|
self.bastion_browse_key_btn.clicked.connect(self._browse_bastion_key)
|
|
self.bastion_mode_combo.currentIndexChanged.connect(self.bastion_stack.setCurrentIndex)
|
|
|
|
mode_idx = self.bastion_mode_combo.findData(bastion.mode)
|
|
self.bastion_mode_combo.setCurrentIndex(mode_idx if mode_idx >= 0 else 0)
|
|
self.bastion_stack.setCurrentIndex(self.bastion_mode_combo.currentIndex())
|
|
|
|
if bastion.mode == BASTION_SESSION:
|
|
found = self.bastion_session_combo.findData(bastion.session_id)
|
|
if found >= 0:
|
|
self.bastion_session_combo.setCurrentIndex(found)
|
|
elif bastion.mode == BASTION_OPENSSH:
|
|
found = self.bastion_openssh_combo.findData(bastion.openssh_alias)
|
|
if found >= 0:
|
|
self.bastion_openssh_combo.setCurrentIndex(found)
|
|
elif bastion.mode == BASTION_MANUAL:
|
|
auth_idx = self.bastion_auth_combo.findData(bastion.auth_type)
|
|
self.bastion_auth_combo.setCurrentIndex(auth_idx if auth_idx >= 0 else 0)
|
|
if bastion.key_path:
|
|
existing = self.bastion_key_combo.findData(bastion.key_path)
|
|
if existing >= 0:
|
|
self.bastion_key_combo.setCurrentIndex(existing)
|
|
else:
|
|
self.bastion_key_combo.addItem(bastion.key_path, bastion.key_path)
|
|
self.bastion_key_combo.setCurrentIndex(self.bastion_key_combo.count() - 1)
|
|
|
|
self._update_bastion_key_row()
|
|
return group
|
|
|
|
def _update_bastion_key_row(self) -> None:
|
|
is_key_auth = self.bastion_auth_combo.currentData() == AUTH_KEY
|
|
self.bastion_key_combo.setEnabled(is_key_auth)
|
|
self.bastion_browse_key_btn.setEnabled(is_key_auth)
|
|
|
|
def _browse_bastion_key(self) -> None:
|
|
path, _ = QFileDialog.getOpenFileName(self, "Choisir une cle privee (bastion)")
|
|
if path:
|
|
self.bastion_key_combo.addItem(path, path)
|
|
self.bastion_key_combo.setCurrentIndex(self.bastion_key_combo.count() - 1)
|
|
|
|
def _result_bastion(self) -> BastionConfig:
|
|
mode = self.bastion_mode_combo.currentData()
|
|
bastion = BastionConfig(mode=mode)
|
|
if mode == BASTION_SESSION:
|
|
bastion.session_id = self.bastion_session_combo.currentData() or ""
|
|
elif mode == BASTION_OPENSSH:
|
|
bastion.openssh_alias = self.bastion_openssh_combo.currentData() or ""
|
|
elif mode == BASTION_MANUAL:
|
|
bastion.host = self.bastion_host_edit.text().strip()
|
|
bastion.port = self.bastion_port_spin.value()
|
|
bastion.username = self.bastion_user_edit.text().strip()
|
|
bastion.auth_type = self.bastion_auth_combo.currentData()
|
|
bastion.key_path = self.bastion_key_combo.currentData() or ""
|
|
return bastion
|
|
|
|
# ---------- Keepalive ----------
|
|
def _build_keepalive_group(self) -> QGroupBox:
|
|
session = self.session
|
|
group = QGroupBox("Keepalive")
|
|
form = QFormLayout(group)
|
|
|
|
self.keepalive_check = QCheckBox("Active")
|
|
self.keepalive_check.setChecked(session.keepalive_enabled)
|
|
self.keepalive_interval_spin = QSpinBox()
|
|
self.keepalive_interval_spin.setRange(5, 3600)
|
|
self.keepalive_interval_spin.setSuffix(" s")
|
|
self.keepalive_interval_spin.setValue(session.keepalive_interval or 30)
|
|
self.keepalive_count_spin = QSpinBox()
|
|
self.keepalive_count_spin.setRange(1, 20)
|
|
self.keepalive_count_spin.setValue(session.keepalive_count_max or 3)
|
|
|
|
form.addRow(self.keepalive_check)
|
|
form.addRow("Intervalle :", self.keepalive_interval_spin)
|
|
form.addRow("Nombre d'echecs avant abandon :", self.keepalive_count_spin)
|
|
|
|
self.keepalive_check.toggled.connect(self.keepalive_interval_spin.setEnabled)
|
|
self.keepalive_check.toggled.connect(self.keepalive_count_spin.setEnabled)
|
|
self.keepalive_interval_spin.setEnabled(session.keepalive_enabled)
|
|
self.keepalive_count_spin.setEnabled(session.keepalive_enabled)
|
|
return group
|
|
|
|
# ---------- Reconnexion automatique ----------
|
|
def _build_reconnect_group(self) -> QGroupBox:
|
|
session = self.session
|
|
group = QGroupBox("Reconnexion automatique")
|
|
form = QFormLayout(group)
|
|
|
|
self.reconnect_check = QCheckBox("Reconnexion automatique")
|
|
self.reconnect_check.setChecked(session.reconnect_enabled)
|
|
self.reconnect_delay_spin = QSpinBox()
|
|
self.reconnect_delay_spin.setRange(1, 300)
|
|
self.reconnect_delay_spin.setSuffix(" s")
|
|
self.reconnect_delay_spin.setValue(session.reconnect_initial_delay or 5)
|
|
self.reconnect_attempts_spin = QSpinBox()
|
|
self.reconnect_attempts_spin.setRange(0, 1000)
|
|
self.reconnect_attempts_spin.setSpecialValueText("Illimite")
|
|
self.reconnect_attempts_spin.setValue(session.reconnect_max_attempts)
|
|
|
|
form.addRow(self.reconnect_check)
|
|
form.addRow("Delai initial :", self.reconnect_delay_spin)
|
|
form.addRow("Tentatives maximum (0 = illimite) :", self.reconnect_attempts_spin)
|
|
|
|
self.reconnect_check.toggled.connect(self.reconnect_delay_spin.setEnabled)
|
|
self.reconnect_check.toggled.connect(self.reconnect_attempts_spin.setEnabled)
|
|
self.reconnect_delay_spin.setEnabled(session.reconnect_enabled)
|
|
self.reconnect_attempts_spin.setEnabled(session.reconnect_enabled)
|
|
return group
|
|
|
|
def should_save(self) -> bool:
|
|
"""En connexion rapide, indique si la session doit aussi etre enregistree."""
|
|
return self.save_check.isChecked() if self.quick_mode else True
|
|
|
|
def _on_accept(self) -> None:
|
|
if not self.host_edit.text().strip():
|
|
QMessageBox.warning(self, "Hote manquant", "Indiquez l'adresse du serveur (nom d'hote ou IP).")
|
|
self.host_edit.setFocus()
|
|
return
|
|
if not self.user_edit.text().strip():
|
|
QMessageBox.warning(self, "Utilisateur manquant", "Indiquez le nom d'utilisateur.")
|
|
self.user_edit.setFocus()
|
|
return
|
|
if self.auth_combo.currentData() == AUTH_KEY and not self.key_combo.currentData():
|
|
QMessageBox.warning(
|
|
self, "Cle manquante",
|
|
"Aucune cle privee n'est selectionnee.\n\n"
|
|
"Choisissez une cle (bouton « Parcourir... »), ou passez l'authentification "
|
|
"sur « Automatique » — qui utilisera l'agent SSH et vos cles ~/.ssh comme le "
|
|
"fait la commande ssh.",
|
|
)
|
|
self.auth_combo.setFocus()
|
|
return
|
|
|
|
bastion_mode = self.bastion_mode_combo.currentData()
|
|
if bastion_mode == BASTION_SESSION and not self.bastion_session_combo.currentData():
|
|
QMessageBox.warning(self, "Bastion incomplet", "Choisissez une session PySSH a utiliser comme bastion.")
|
|
return
|
|
if bastion_mode == BASTION_OPENSSH and not self.bastion_openssh_combo.currentData():
|
|
QMessageBox.warning(self, "Bastion incomplet", "Choisissez un Host de ~/.ssh/config a utiliser comme bastion.")
|
|
return
|
|
if bastion_mode == BASTION_MANUAL and not self.bastion_host_edit.text().strip():
|
|
QMessageBox.warning(self, "Bastion incomplet", "Indiquez l'adresse du bastion.")
|
|
self.bastion_host_edit.setFocus()
|
|
return
|
|
|
|
self.session.name = self.name_edit.text().strip() or self.host_edit.text().strip()
|
|
self.session.group = self.group_edit.text().strip()
|
|
self.session.host = self.host_edit.text().strip()
|
|
self.session.port = self.port_spin.value()
|
|
self.session.username = self.user_edit.text().strip()
|
|
self.session.auth_type = self.auth_combo.currentData()
|
|
self.session.key_path = self.key_combo.currentData() or ""
|
|
self.session.notes = self.notes_edit.toPlainText()
|
|
seen = set()
|
|
tags = []
|
|
for raw in self.tags_edit.text().split(","):
|
|
tag = raw.strip()
|
|
if tag and tag.lower() not in seen:
|
|
seen.add(tag.lower())
|
|
tags.append(tag)
|
|
self.session.tags = tags
|
|
remember = self.auth_combo.currentData() == AUTH_PASSWORD and self.remember_password_check.isChecked()
|
|
new_storage = PASSWORD_STORAGE_SECURE if remember else PASSWORD_STORAGE_NONE
|
|
if self.session.password_storage == PASSWORD_STORAGE_SECURE and new_storage == PASSWORD_STORAGE_NONE:
|
|
# la memorisation vient d'etre desactivee (ou l'authentification changee) :
|
|
# ne pas laisser un secret perimer dans le trousseau
|
|
self.credential_store.delete_secret(session_credential_key(self.session.id))
|
|
self.session.password_storage = new_storage
|
|
self.session.bastion = self._result_bastion()
|
|
self.session.keepalive_enabled = self.keepalive_check.isChecked()
|
|
self.session.keepalive_interval = self.keepalive_interval_spin.value()
|
|
self.session.keepalive_count_max = self.keepalive_count_spin.value()
|
|
self.session.reconnect_enabled = self.reconnect_check.isChecked()
|
|
self.session.reconnect_initial_delay = self.reconnect_delay_spin.value()
|
|
self.session.reconnect_max_attempts = self.reconnect_attempts_spin.value()
|
|
self.accept()
|
|
|
|
def result_session(self) -> Session:
|
|
return self.session
|