// Package auth kapselt die Benutzerauthentifizierung hinter einem schmalen // Interface, damit später weitere Backends (OIDC/Entra ID) ergänzt werden können. package auth import ( "context" "fmt" "strings" ) // Identity ist die kanonische Identität eines authentifizierten Benutzers. type Identity struct { // Username ist der aus dem Verzeichnis gelesene sAMAccountName in // Kleinschreibung — niemals die Benutzereingabe. Username string // Groups enthält die Gruppen, die für Rollenentscheidungen relevant sind. // v1 füllt hier nur die VPN-Gruppe; das Feld hält den Weg zu einer // späteren VPN-Portal-Admins-Gruppe offen. Groups []string } // HasGroup prüft Gruppenzugehörigkeit ohne Rücksicht auf Groß-/Kleinschreibung. func (i *Identity) HasGroup(name string) bool { for _, g := range i.Groups { if strings.EqualFold(g, name) { return true } } return false } // Authenticator prüft Zugangsdaten und liefert eine kanonische Identität. type Authenticator interface { Authenticate(ctx context.Context, username, password string) (*Identity, error) } // Reason-Codes landen unverändert im Audit-Log. const ( ReasonInvalidCredentials = "invalid_credentials" ReasonAccountDisabled = "account_disabled" ReasonAccountLocked = "account_locked" ReasonPasswordExpired = "password_expired" ReasonPasswordChangeRequired = "password_change_required" ReasonNotInVPNGroup = "not_in_vpn_group" ReasonUserNotFound = "user_not_found" ReasonBackendUnavailable = "backend_unavailable" ) // Error trägt den Audit-Reason und den technischen Ursprungsfehler. type Error struct { Reason string Err error } func (e *Error) Error() string { if e.Err == nil { return e.Reason } return fmt.Sprintf("%s: %v", e.Reason, e.Err) } func (e *Error) Unwrap() error { return e.Err } // UserVisible meldet, ob dem Benutzer eine spezifische statt der generischen // Fehlermeldung gezeigt werden darf. Nur das abgelaufene Passwort ist eine // Ausnahme — alles andere wäre ein Enumerationsorakel. func (e *Error) UserVisible() bool { return e.Reason == ReasonPasswordExpired }