check / check (push) Successful in 5m15s
A login stores the username in the session cookie, which cannot carry a value past about 4096 bytes, so a username of about 2 KB or more could never log in and the login answered 500. Usernames are now limited to 1024 bytes, about half of what the cookie can carry. The User model rejects a longer one with ErrUsernameTooLong, and a check constraint on the users table rejects it for any path that bypasses the model. The comment on MaxUsernameBytes gives the arithmetic. Model: opus-5-5
58 lines
1.9 KiB
Go
58 lines
1.9 KiB
Go
package database
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// MaxUsernameBytes is the longest username, in bytes, that a user may
|
||
// have. The same number appears in the check constraint on
|
||
// User.Username, because a struct tag cannot reference a constant.
|
||
//
|
||
// A login stores the username in the session cookie, and both
|
||
// securecookie and browsers refuse a cookie value past about 4096
|
||
// bytes. That value is the session base64-encoded twice, so it holds
|
||
// 4096 × 3/4 × 3/4 = 2304 bytes of session, and the signature,
|
||
// timestamp and the session's other values take about 270 of those: a
|
||
// username longer than about 2030 bytes can never log in. The limit is
|
||
// about half that, so the session can carry more values later without
|
||
// locking out an account whose username is already at the limit.
|
||
const MaxUsernameBytes = 1024
|
||
|
||
// ErrUsernameTooLong is returned when a user is saved with a username
|
||
// longer than MaxUsernameBytes.
|
||
var ErrUsernameTooLong = errors.New("username is too long")
|
||
|
||
// User represents a user of the webhooker service
|
||
//
|
||
//nolint:lll // a struct tag cannot wrap
|
||
type User struct {
|
||
BaseModel
|
||
|
||
Username string `gorm:"uniqueIndex;not null;check:length(CAST(username AS BLOB)) <= 1024" json:"username"`
|
||
Password string `gorm:"not null" json:"-"` // Argon2 hashed
|
||
|
||
// Relations
|
||
Webhooks []Webhook `json:"webhooks,omitempty"`
|
||
APIKeys []APIKey `json:"apiKeys,omitempty"`
|
||
}
|
||
|
||
// BeforeSave rejects a username longer than MaxUsernameBytes, so every
|
||
// path that saves a user through GORM gets ErrUsernameTooLong rather
|
||
// than the database's constraint error. The check constraint behind it
|
||
// holds for any path that writes the table without this model.
|
||
func (u *User) BeforeSave(_ *gorm.DB) error {
|
||
if len(u.Username) > MaxUsernameBytes {
|
||
return fmt.Errorf(
|
||
"%w: %d bytes, limit is %d",
|
||
ErrUsernameTooLong,
|
||
len(u.Username),
|
||
MaxUsernameBytes,
|
||
)
|
||
}
|
||
|
||
return nil
|
||
}
|