Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
67 lines
2.1 KiB
Go
67 lines
2.1 KiB
Go
// 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 }
|