fix(cabana): enforce the 4-character password minimum on the Administrators form

The form's min:4 rule never applied: the model validator skips string
min and max. storePassword now refuses a typed password under 4
characters with a 422 on the field, next to the 72-byte check (D-11).
Found by TestAdministratorsPasswordAndIdentity.
This commit is contained in:
Jakub Zych
2026-10-22 12:00:18 +02:00
parent 536ea8c1bf
commit 461cb21b3e

View File

@@ -8,6 +8,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"unicode/utf8"
"git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bouncer" "git.golem15.com/golem15/summercms/modules/bouncer"
@@ -841,13 +842,24 @@ func submittedPassword(ctx context.Context) string {
return plain return plain
} }
// adminPasswordMinChars is the shortest password the Administrators form
// accepts, in characters (D-11).
const adminPasswordMinChars = 4
// storePassword hashes a typed password onto user and reports whether one // storePassword hashes a typed password onto user and reports whether one
// was typed. A password over bcrypt's 72-byte limit is a 422 on the field. // was typed. A password under adminPasswordMinChars characters or over
// bcrypt's 72-byte limit is a 422 on the field. The length is checked here
// because the form's min:4 rule is a string rule, which the model validator
// does not apply.
func (c usersAdmin) storePassword(ctx context.Context, user *BackendUser) (bool, error) { func (c usersAdmin) storePassword(ctx context.Context, user *BackendUser) (bool, error) {
plain := submittedPassword(ctx) plain := submittedPassword(ctx)
if plain == "" { if plain == "" {
return false, nil return false, nil
} }
if utf8.RuneCountInString(plain) < adminPasswordMinChars {
message := accountMessage(ctx, c.translator(), "min.string", "password", map[string]string{"min": strconv.Itoa(adminPasswordMinChars)})
return false, &ValidationError{Details: map[string]any{"password": []string{message}}}
}
if len(plain) > passwordMaxBytes { if len(plain) > passwordMaxBytes {
message := accountMessage(ctx, c.translator(), "max.string", "password", map[string]string{"max": strconv.Itoa(passwordMaxBytes)}) message := accountMessage(ctx, c.translator(), "max.string", "password", map[string]string{"max": strconv.Itoa(passwordMaxBytes)})
return false, &ValidationError{Details: map[string]any{"password": []string{message}}} return false, &ValidationError{Details: map[string]any{"password": []string{message}}}