Bug principal : l'SSHAuthenticator est créé au démarrage avec host="" (DB vide avant installation). Après configure, il gardait host vide. Le login lisait maintenant le ssh_host depuis la DB à chaque requête. Logs ajoutés : - ssh_auth.go : dial SSH, succès, échec avec détail d'erreur - auth.go : host SSH utilisé, résultat auth à chaque login - updates.go : credentials SSH, démarrage/fin de job - terminal.go : ouverture/échec session SSH Frontend : - auth.store.ts : gère les réponses non-JSON sur erreur HTTP Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
134 lines
4.2 KiB
Go
134 lines
4.2 KiB
Go
// 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"
|
|
"log"
|
|
"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
|
|
log.Printf("[ssh_auth] Dial %s@%s...", username, a.host)
|
|
client, err := ssh.Dial("tcp", a.host, config)
|
|
if err != nil {
|
|
log.Printf("[ssh_auth] Échec dial %s@%s : %v", username, a.host, err)
|
|
// 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()
|
|
log.Printf("[ssh_auth] Connexion établie — %s@%s", username, a.host)
|
|
|
|
// Vérifier l'appartenance aux groupes sudo/wheel via la commande `id`
|
|
isAdmin, err := checkSudoGroup(client)
|
|
if err != nil {
|
|
log.Printf("[ssh_auth] Avertissement vérification sudo — user=%s : %v", username, err)
|
|
isAdmin = false
|
|
}
|
|
log.Printf("[ssh_auth] Authentifié — user=%s admin=%v", username, isAdmin)
|
|
|
|
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
|
|
}
|