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>
84 lines
3.4 KiB
Python
84 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.session_manager import (
|
|
SessionStore, BastionConfig,
|
|
BASTION_NONE, BASTION_SESSION, BASTION_OPENSSH, BASTION_MANUAL,
|
|
AUTH_AUTO, AUTH_KEY,
|
|
)
|
|
from app.credentials import session_credential_key
|
|
|
|
|
|
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}")
|