internal/pwpolicy: Validate ist eine schmale Regelschicht VOR dem bcrypt-Hashing aus IAM-02, kein eigenes Policy-Framework. Prueft Mindestlaenge, Zeichenklassen (Gross-/Kleinbuchstaben, Ziffern, Sonderzeichen je nach Policy) und eine eingebettete Sperrliste haeufig verwendeter Passwoerter (case-insensitive) — Akzeptanzkriterium 1 + 2. Store haelt die Richtlinie als eine Zeile je Tenant-Datenbank (Singleton, Modell C), DefaultPolicy() greift, solange kein Tenant eine eigene gesetzt hat. LoginAndCheckPolicy komponiert IAM-02s LoginService, OHNE ihn zu veraendern: der Login selbst schlaegt bei einem alten, nicht mehr konformen Passwort NICHT fehl (Akzeptanzkriterium 3 — kein rueckwirkendes Aussperren), die Funktion liefert zusaetzlich mustChangePassword=true. Die Pruefung ist nur im Login-Moment moeglich, da dort kurzzeitig das Klartext-Passwort vorliegt — der gespeicherte bcrypt-Hash laesst sich nicht rueckwirkend gegen eine neue Richtlinie pruefen. Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Zu kurzes/zu einfaches Passwort bei Registrierung/Aenderung abgelehnt — TestValidate_RejectsTooShortOrSimple. PASS. 2. Passwort aus Sperrliste abgelehnt — TestValidate_RejectsBlocklistedPassword (inkl. Gross-/Kleinschreibung). PASS. 3. Bestehender Nutzer mit altem, nicht-konformem Passwort kann sich noch einloggen, wird aber zur Aenderung aufgefordert — TestLoginAndCheckPolicy_FlagsNonConformantExistingPassword: Login mit schwachem Altpasswort gelingt, mustChangePassword=true; mit konformem Passwort mustChangePassword=false. PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
113 lines
3.6 KiB
Go
113 lines
3.6 KiB
Go
// Package pwpolicy implementiert Core IAM-14: konfigurierbare Passwort-
|
|
// Komplexitätsanforderungen pro Tenant — eine schmale Regelschicht VOR dem
|
|
// bcrypt-Hashing aus IAM-02, kein eigenes Policy-Framework.
|
|
package pwpolicy
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// Policy sind die Mindestanforderungen (Akzeptanzkriterium 1).
|
|
type Policy struct {
|
|
MinLength int
|
|
RequireUpper bool
|
|
RequireLower bool
|
|
RequireDigit bool
|
|
RequireSpecial bool
|
|
}
|
|
|
|
// DefaultPolicy gilt, solange ein Tenant keine eigene Konfiguration gesetzt hat.
|
|
func DefaultPolicy() Policy {
|
|
return Policy{MinLength: 12, RequireUpper: true, RequireLower: true, RequireDigit: true, RequireSpecial: false}
|
|
}
|
|
|
|
var ErrPasswordTooWeak = errors.New("pwpolicy: passwort erfuellt die richtlinie nicht")
|
|
|
|
// Validate prueft ein Passwort gegen policy UND die Sperrliste
|
|
// (Akzeptanzkriterium 2). Liefert bei Verstoss ErrPasswordTooWeak,
|
|
// gewrappt mit einer fuer Menschen lesbaren Begruendung.
|
|
func Validate(policy Policy, password string) error {
|
|
if commonPasswords[strings.ToLower(password)] {
|
|
return fmt.Errorf("%w: passwort steht auf der sperrliste haeufig verwendeter passwoerter", ErrPasswordTooWeak)
|
|
}
|
|
if len(password) < policy.MinLength {
|
|
return fmt.Errorf("%w: mindestens %d zeichen erforderlich", ErrPasswordTooWeak, policy.MinLength)
|
|
}
|
|
|
|
var hasUpper, hasLower, hasDigit, hasSpecial bool
|
|
for _, r := range password {
|
|
switch {
|
|
case unicode.IsUpper(r):
|
|
hasUpper = true
|
|
case unicode.IsLower(r):
|
|
hasLower = true
|
|
case unicode.IsDigit(r):
|
|
hasDigit = true
|
|
case unicode.IsPunct(r) || unicode.IsSymbol(r):
|
|
hasSpecial = true
|
|
}
|
|
}
|
|
|
|
if policy.RequireUpper && !hasUpper {
|
|
return fmt.Errorf("%w: mindestens ein grossbuchstabe erforderlich", ErrPasswordTooWeak)
|
|
}
|
|
if policy.RequireLower && !hasLower {
|
|
return fmt.Errorf("%w: mindestens ein kleinbuchstabe erforderlich", ErrPasswordTooWeak)
|
|
}
|
|
if policy.RequireDigit && !hasDigit {
|
|
return fmt.Errorf("%w: mindestens eine ziffer erforderlich", ErrPasswordTooWeak)
|
|
}
|
|
if policy.RequireSpecial && !hasSpecial {
|
|
return fmt.Errorf("%w: mindestens ein sonderzeichen erforderlich", ErrPasswordTooWeak)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Store verwaltet die pro-Tenant konfigurierte Richtlinie (eine Zeile je
|
|
// Tenant-Datenbank, Modell C).
|
|
type Store struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
func NewStore(pool *pgxpool.Pool) *Store {
|
|
return &Store{pool: pool}
|
|
}
|
|
|
|
// Get liefert die konfigurierte Richtlinie, oder DefaultPolicy() wenn noch
|
|
// keine gesetzt wurde.
|
|
func (s *Store) Get(ctx context.Context) (Policy, error) {
|
|
var p Policy
|
|
err := s.pool.QueryRow(ctx, `
|
|
SELECT min_length, require_upper, require_lower, require_digit, require_special
|
|
FROM password_policy WHERE id = true
|
|
`).Scan(&p.MinLength, &p.RequireUpper, &p.RequireLower, &p.RequireDigit, &p.RequireSpecial)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return DefaultPolicy(), nil
|
|
}
|
|
return Policy{}, fmt.Errorf("richtlinie lesen: %w", err)
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// Set legt die Richtlinie fuer diesen Tenant fest (Akzeptanzkriterium 1).
|
|
func (s *Store) Set(ctx context.Context, p Policy) error {
|
|
_, err := s.pool.Exec(ctx, `
|
|
INSERT INTO password_policy (id, min_length, require_upper, require_lower, require_digit, require_special, updated_at)
|
|
VALUES (true, $1, $2, $3, $4, $5, now())
|
|
ON CONFLICT (id) DO UPDATE SET
|
|
min_length = $1, require_upper = $2, require_lower = $3, require_digit = $4, require_special = $5, updated_at = now()
|
|
`, p.MinLength, p.RequireUpper, p.RequireLower, p.RequireDigit, p.RequireSpecial)
|
|
if err != nil {
|
|
return fmt.Errorf("richtlinie speichern: %w", err)
|
|
}
|
|
return nil
|
|
}
|