// Package auth enthält Identität, Anmeldung, Sitzungen und rollenbasierte // Zugriffssteuerung der Control Plane. // // Grundsätze (PROMPT.md §41–§45): // - Berechtigungen werden immer serverseitig geprüft. // - Fehlermeldungen der Anmeldung geben niemals preis, ob ein Konto existiert. // - Ein zweiter Faktor ist für privilegierte Konten erzwingbar. package auth import ( "errors" "time" "github.com/google/uuid" ) // UserStatus beschreibt den Zustand eines Benutzerkontos. type UserStatus string const ( // UserStatusActive erlaubt die Anmeldung. UserStatusActive UserStatus = "active" // UserStatusDisabled verbietet die Anmeldung dauerhaft. UserStatusDisabled UserStatus = "disabled" // UserStatusLocked verbietet die Anmeldung bis zur Entsperrung. UserStatusLocked UserStatus = "locked" ) // User ist ein Benutzerkonto. // // Das Passwort erscheint hier bewusst nicht: der Hash verlässt die // Datenzugriffsschicht nur dort, wo er zur Prüfung gebraucht wird. type User struct { // ID ist der öffentliche Bezeichner. ID uuid.UUID `json:"id"` // Username ist der Anmeldename. Username string `json:"username"` // Email ist die optionale Mailadresse. Email string `json:"email,omitempty"` // Status ist der Zustand des Kontos. Status UserStatus `json:"status"` // MFAEnabled meldet, ob ein bestätigter zweiter Faktor vorliegt. MFAEnabled bool `json:"mfa_enabled"` // Roles sind die zugewiesenen Rollennamen. Roles []string `json:"roles"` // Permissions sind die aus den Rollen abgeleiteten Berechtigungen. Permissions []string `json:"permissions"` // CreatedAt ist der Anlagezeitpunkt in UTC. CreatedAt time.Time `json:"created_at"` // UpdatedAt ist der Zeitpunkt der letzten Änderung in UTC. UpdatedAt time.Time `json:"updated_at"` // LastLoginAt ist der Zeitpunkt der letzten erfolgreichen Anmeldung. LastLoginAt *time.Time `json:"last_login_at,omitempty"` } // HasPermission meldet, ob der Benutzer eine Berechtigung besitzt. func (user User) HasPermission(requiredPermission string) bool { for _, grantedPermission := range user.Permissions { if grantedPermission == requiredPermission { return true } } return false } // IsPrivileged meldet, ob das Konto weitreichende Rechte besitzt. // // Für solche Konten kann ein zweiter Faktor erzwungen werden (PROMPT.md §43). func (user User) IsPrivileged() bool { // Schreibrechte an Benutzern, Rollen oder Sicherheitseinstellungen erlauben // es, sich selbst weitere Rechte zu verschaffen. for _, privilegedPermission := range []string{"users.write", "roles.write", "security.write"} { if user.HasPermission(privilegedPermission) { return true } } return false } // Role ist eine Rolle der Zugriffssteuerung. type Role struct { // ID ist der Bezeichner der Rolle. ID uuid.UUID `json:"id"` // Name ist der eindeutige technische Name. Name string `json:"name"` // Description erklärt den Zweck der Rolle. Description string `json:"description,omitempty"` // IsSystem kennzeichnet mitgelieferte Rollen, die nicht gelöscht werden dürfen. IsSystem bool `json:"is_system"` // Permissions sind die zugeordneten Berechtigungen. Permissions []string `json:"permissions"` } // Permission ist eine Einzelberechtigung. type Permission struct { // Name ist der technische Name im Format bereich.aktion. Name string `json:"name"` // Description erklärt, was die Berechtigung erlaubt. Description string `json:"description"` } // Session ist eine aktive Anmeldesitzung. type Session struct { // ID ist der Bezeichner der Sitzung. ID uuid.UUID // UserID ist der angemeldete Benutzer. UserID uuid.UUID // AccessExpiresAt ist die Ablaufzeit des Zugriffstokens in UTC. AccessExpiresAt time.Time // RefreshExpiresAt ist die Ablaufzeit des Erneuerungstokens in UTC. RefreshExpiresAt time.Time } // TokenPair sind die bei einer Anmeldung ausgegebenen Tokens. // // Die Klartextwerte existieren ausschließlich in dieser Struktur und in der // Antwort an den Aufrufer; gespeichert wird nur ihr Hash. type TokenPair struct { // AccessToken ist das kurzlebige Zugriffstoken. AccessToken string `json:"access_token"` // RefreshToken ist das langlebigere Erneuerungstoken. RefreshToken string `json:"refresh_token"` // AccessExpiresAt ist die Ablaufzeit des Zugriffstokens in UTC. AccessExpiresAt time.Time `json:"access_expires_at"` // RefreshExpiresAt ist die Ablaufzeit des Erneuerungstokens in UTC. RefreshExpiresAt time.Time `json:"refresh_expires_at"` } // LoginResult ist das Ergebnis eines Anmeldeversuchs. // // Entweder ist die Anmeldung abgeschlossen (Tokens gesetzt) oder es fehlt noch // der zweite Faktor (MFARequired gesetzt). Beides gleichzeitig gibt es nicht. type LoginResult struct { // MFARequired meldet, dass noch ein zweiter Faktor nötig ist. MFARequired bool `json:"mfa_required"` // ChallengeID benennt die offene MFA-Herausforderung. ChallengeID *uuid.UUID `json:"challenge_id,omitempty"` // Tokens sind die ausgegebenen Tokens, sofern die Anmeldung abgeschlossen ist. Tokens *TokenPair `json:"tokens,omitempty"` // User ist der angemeldete Benutzer, sofern die Anmeldung abgeschlossen ist. User *User `json:"user,omitempty"` } // MFAEnrollment sind die Daten zur Einrichtung eines zweiten Faktors. // // Secret und Wiederherstellungscodes werden genau einmal ausgeliefert und // danach nie wieder abrufbar (PROMPT.md §43). type MFAEnrollment struct { // Secret ist das TOTP-Secret in Base32. Secret string `json:"secret"` // ProvisioningURI ist die otpauth-URI für Authenticator-Apps. ProvisioningURI string `json:"provisioning_uri"` // RecoveryCodes sind die einmalig verwendbaren Wiederherstellungscodes. RecoveryCodes []string `json:"recovery_codes"` } // Fehler der Anmeldung. // // ErrInvalidCredentials wird bewusst für unbekannte Benutzer und falsche // Passwörter gleichermaßen verwendet: eine Unterscheidung erlaubte es, gültige // Anmeldenamen zu ermitteln (Benutzer-Enumeration). var ( // ErrInvalidCredentials meldet falsche Anmeldedaten. ErrInvalidCredentials = errors.New("benutzername oder passwort ist falsch") // ErrAccountLocked meldet ein wegen zu vieler Fehlversuche gesperrtes Konto. ErrAccountLocked = errors.New("das konto ist vorübergehend gesperrt") // ErrAccountDisabled meldet ein deaktiviertes Konto. ErrAccountDisabled = errors.New("das konto ist deaktiviert") // ErrInvalidMFACode meldet einen falschen Code des zweiten Faktors. ErrInvalidMFACode = errors.New("der code ist falsch oder bereits verwendet") // ErrChallengeNotFound meldet eine unbekannte, abgelaufene oder bereits // eingelöste MFA-Herausforderung. ErrChallengeNotFound = errors.New("die anmeldung ist abgelaufen, bitte erneut anmelden") // ErrChallengeAttemptsExceeded meldet zu viele Fehlversuche beim zweiten Faktor. ErrChallengeAttemptsExceeded = errors.New("zu viele fehlversuche, bitte erneut anmelden") // ErrSessionInvalid meldet ein unbekanntes, abgelaufenes oder widerrufenes Token. ErrSessionInvalid = errors.New("die sitzung ist ungültig oder abgelaufen") // ErrUserNotFound meldet einen nicht vorhandenen Benutzer. ErrUserNotFound = errors.New("der benutzer existiert nicht") // ErrUsernameTaken meldet einen bereits vergebenen Anmeldenamen. ErrUsernameTaken = errors.New("der benutzername ist bereits vergeben") // ErrEmailTaken meldet eine bereits vergebene Mailadresse. ErrEmailTaken = errors.New("die mailadresse ist bereits vergeben") // ErrRoleNotFound meldet eine nicht vorhandene Rolle. ErrRoleNotFound = errors.New("die rolle existiert nicht") // ErrSystemRoleImmutable meldet den Versuch, eine mitgelieferte Rolle zu ändern. ErrSystemRoleImmutable = errors.New("mitgelieferte rollen können nicht geändert oder gelöscht werden") // ErrRoleInUse meldet den Versuch, eine zugewiesene Rolle zu löschen. ErrRoleInUse = errors.New("die rolle ist noch Benutzern zugewiesen") // ErrMFAAlreadyEnabled meldet einen bereits eingerichteten zweiten Faktor. ErrMFAAlreadyEnabled = errors.New("für dieses konto ist bereits ein zweiter faktor eingerichtet") // ErrMFANotEnrolled meldet einen fehlenden zweiten Faktor. ErrMFANotEnrolled = errors.New("für dieses konto ist kein zweiter faktor eingerichtet") // ErrLastAdministrator verhindert das Entfernen des letzten Administrators. ErrLastAdministrator = errors.New("der letzte administrator kann nicht entfernt oder entrechtet werden") // ErrWeakPassword meldet ein zu schwaches Passwort. ErrWeakPassword = errors.New("das passwort erfüllt die mindestanforderungen nicht") )