feat: ajouter le Credential Vault (memorisation securisee optionnelle)
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>
This commit is contained in:
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from PySide6.QtCore import Qt, Signal, QObject
|
||||
from PySide6.QtWidgets import QTabWidget, QWidget, QMenu, QMessageBox, QInputDialog
|
||||
|
||||
from app.session_manager import SessionStore, Session, AUTH_PASSWORD
|
||||
from app.session_manager import SessionStore, Session, AUTH_PASSWORD, PASSWORD_STORAGE_SECURE
|
||||
from app.key_manager import KeyManager
|
||||
from app.local_terminal import DEFAULT_SHELL, SHELL_LABELS
|
||||
from app.dialogs.session_dialog import SessionDialog
|
||||
@@ -13,6 +13,7 @@ from app.dialogs.connect_prompt import ask_secret
|
||||
from app.services.ssh_target import detect_ssh_target
|
||||
from app.openssh.config import OpenSSHHost, session_from_openssh_host
|
||||
from app.bastion import BastionTarget, resolve_bastion_target, BastionResolutionError
|
||||
from app.credentials import CredentialStore, NullCredentialStore, session_credential_key
|
||||
from app.controllers.broadcast_manager import BroadcastManager
|
||||
from app.ui.tabs import BaseTerminalTab, SessionTabWidget, SFTPTabWidget, LocalTerminalTabWidget
|
||||
from app.ui.split_pane import SplitContainer, PaneSlot, LAYOUT_LABELS
|
||||
@@ -31,6 +32,7 @@ class ConnectionController(QObject):
|
||||
key_manager: KeyManager,
|
||||
broadcast: BroadcastManager,
|
||||
snippet_store=None,
|
||||
credential_store: CredentialStore | None = None,
|
||||
parent_widget: QWidget | None = None,
|
||||
):
|
||||
super().__init__(parent_widget)
|
||||
@@ -38,6 +40,7 @@ class ConnectionController(QObject):
|
||||
self.key_manager = key_manager
|
||||
self.broadcast = broadcast
|
||||
self.snippet_store = snippet_store
|
||||
self.credential_store = credential_store or NullCredentialStore()
|
||||
self._parent_widget = parent_widget
|
||||
|
||||
self.tabs = QTabWidget(parent_widget)
|
||||
@@ -62,6 +65,13 @@ class ConnectionController(QObject):
|
||||
return True, None, ""
|
||||
if target.auth_type != AUTH_PASSWORD:
|
||||
return True, target, ""
|
||||
# Un bastion "session PySSH" reutilise un mot de passe deja memorise pour
|
||||
# cette session (s'il en existe un) : jamais ecrit depuis ce chemin, on ne
|
||||
# fait que lire ce que l'ouverture directe de cette session aurait memorise.
|
||||
if target.credential_key:
|
||||
stored = self.credential_store.get_secret(target.credential_key)
|
||||
if stored is not None:
|
||||
return True, target, stored
|
||||
text, ok = ask_secret(
|
||||
self._parent_widget, "Mot de passe du bastion", f"Mot de passe pour {target.label} (bastion) :",
|
||||
)
|
||||
@@ -72,9 +82,17 @@ class ConnectionController(QObject):
|
||||
def _ask_session_password(self, session: Session) -> tuple[bool, str]:
|
||||
if session.auth_type != AUTH_PASSWORD:
|
||||
return True, ""
|
||||
remember = session.password_storage == PASSWORD_STORAGE_SECURE
|
||||
key = session_credential_key(session.id)
|
||||
if remember:
|
||||
stored = self.credential_store.get_secret(key)
|
||||
if stored is not None:
|
||||
return True, stored
|
||||
text, ok = ask_secret(
|
||||
self._parent_widget, "Mot de passe", f"Mot de passe pour {session.username}@{session.host} :"
|
||||
)
|
||||
if ok and remember:
|
||||
self.credential_store.set_secret(key, text)
|
||||
return ok, (text if ok else "")
|
||||
|
||||
def build_session_tab(self, session: Session) -> SessionTabWidget | None:
|
||||
@@ -176,6 +194,7 @@ class ConnectionController(QObject):
|
||||
connect_label="Se connecter",
|
||||
title="Connexion SFTP" if sftp else "Connexion SSH",
|
||||
session_store=self.session_store,
|
||||
credential_store=self.credential_store,
|
||||
)
|
||||
if not dialog.exec():
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user