// Package auth — Authentification PAM via SSH. // Au lieu de monter les fichiers système du host (/etc/shadow), on tente une connexion // SSH avec les credentials de l'utilisateur. Si elle réussit, les credentials sont valides. // L'appartenance au groupe sudo/wheel détermine le niveau admin. package auth import ( "fmt" "net" "strings" "time" "golang.org/x/crypto/ssh" ) // UserInfo contient les informations d'un utilisateur authentifié. type UserInfo struct { Username string IsAdmin bool } // SSHAuthenticator gère l'authentification des utilisateurs via SSH vers le host Proxmox. type SSHAuthenticator struct { host string // ex: "10.0.0.1:2244" } // NewSSHAuthenticator crée un authentificateur SSH pour le host donné. func NewSSHAuthenticator(host string) *SSHAuthenticator { return &SSHAuthenticator{host: host} } // Authenticate tente une connexion SSH avec les credentials fournis. // Si la connexion réussit, retourne les informations de l'utilisateur. // Vérifie l'appartenance au groupe sudo ou wheel pour déterminer le niveau admin. func (a *SSHAuthenticator) Authenticate(username, password string) (*UserInfo, error) { config := &ssh.ClientConfig{ User: username, Auth: []ssh.AuthMethod{ ssh.Password(password), }, // Timeout court pour l'authentification Timeout: 10 * time.Second, // Accepter n'importe quelle clé host (le host est sur le réseau interne de confiance) HostKeyCallback: ssh.InsecureIgnoreHostKey(), } // Tentative de connexion SSH client, err := ssh.Dial("tcp", a.host, config) if err != nil { // Distinguer les erreurs d'authentification des erreurs réseau if strings.Contains(err.Error(), "unable to authenticate") || strings.Contains(err.Error(), "ssh: handshake failed") || strings.Contains(err.Error(), "no supported methods remain") { return nil, fmt.Errorf("identifiants invalides") } return nil, fmt.Errorf("connexion SSH impossible : %w", err) } defer client.Close() // Vérifier l'appartenance aux groupes sudo/wheel via la commande `id` isAdmin, err := checkSudoGroup(client) if err != nil { // En cas d'erreur de vérification des groupes, l'utilisateur est authentifié mais pas admin isAdmin = false } return &UserInfo{ Username: username, IsAdmin: isAdmin, }, nil } // TestConnectivity teste la connexion SSH sans authentification complète. // Utilisé pendant l'installation pour valider les paramètres de connexion. func TestConnectivity(host string, timeout time.Duration) error { conn, err := net.DialTimeout("tcp", host, timeout) if err != nil { return fmt.Errorf("impossible de joindre %s : %w", host, err) } conn.Close() return nil } // TestSSHAuth teste une connexion SSH complète avec credentials. // Retourne nil si la connexion réussit, une erreur explicite sinon. func TestSSHAuth(host, username, password string) error { config := &ssh.ClientConfig{ User: username, Auth: []ssh.AuthMethod{ ssh.Password(password), }, Timeout: 10 * time.Second, HostKeyCallback: ssh.InsecureIgnoreHostKey(), } client, err := ssh.Dial("tcp", host, config) if err != nil { if strings.Contains(err.Error(), "unable to authenticate") { return fmt.Errorf("identifiants SSH invalides") } return fmt.Errorf("connexion SSH échouée : %w", err) } client.Close() return nil } // checkSudoGroup exécute `id -nG` sur la session SSH et vérifie la présence // des groupes "sudo" ou "wheel" dans la liste des groupes de l'utilisateur. func checkSudoGroup(client *ssh.Client) (bool, error) { session, err := client.NewSession() if err != nil { return false, fmt.Errorf("ouverture session SSH : %w", err) } defer session.Close() output, err := session.Output("id -nG") if err != nil { return false, fmt.Errorf("exécution `id -nG` : %w", err) } groups := strings.Fields(strings.TrimSpace(string(output))) for _, g := range groups { if g == "sudo" || g == "wheel" { return true, nil } } return false, nil }