// 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 }