app: logins folded to lower case, unique without regard to case (audit H4, 0008)
Postfix's reject_sender_login_mismatch compares the SASL login with the sender map without regard to case - seen on the e2e stand this time (a one-off probe, not committed: map rewritten to "APP1", client authenticated as "app1", RCPT accepted). The panel stored logins as typed under a BINARY UNIQUE, so "Ops" and "ops" were two applications to the panel and one sender to Postfix: a tenant could send with another tenant's envelope sender. - app.foldLogin: Service.Create lower-cases the login (owner: fold, do not refuse); CreateWithSettings rolls back the stored (folded) login. - ImportApplication keeps the login as written: folding it would rename the SASL account and break the imported application's working credentials; the index below refuses a case clash there too. - Migration 0008 (DDL): CREATE UNIQUE INDEX idx_applications_login_nocase ON applications (login COLLATE NOCASE). The column's own BINARY UNIQUE stays (SQLite cannot change a collation without a table rebuild, which a migration transaction cannot do with foreign keys on). Checked on the live installation first: 4 logins, no case clash; "Quieter3C" keeps its case. 0008 is not run anywhere yet; send_log.domain_id joins it later in stage 2. Tests: "Ops" created as ops, SASL account and map binding in lower case, OPS in another domain and an imported oPs refused; the store refuses ops beside Ops; schema at user_version 8 with the index. CHANGELOG [Unreleased], audit plan line ticked, roadmap 15/61. Evidence on the test server for this tree (with the probe): go vet clean, go test ./... 23 packages ok, make e2e exit 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,7 @@ func (s *Service) Get(id int64) (store.Application, error) {
|
||||
// the map rebuild fails, everything is rolled back so we never leave an
|
||||
// application the panel cannot fully account for.
|
||||
func (s *Service) Create(domainID int64, login, mode string, rawAddresses []string) (store.Application, string, error) {
|
||||
login = foldLogin(login)
|
||||
addresses, err := s.validateForDomain(domainID, login, mode, rawAddresses)
|
||||
if err != nil {
|
||||
return store.Application{}, "", err
|
||||
@@ -313,7 +314,7 @@ func (s *Service) CreateWithSettings(domainID int64, login string, set Settings,
|
||||
}
|
||||
if err := s.applyAccess(a.ID, set, recalc); err != nil {
|
||||
_ = s.ClearRateLimit(a.ID)
|
||||
s.rollbackCreate(a.ID, login)
|
||||
s.rollbackCreate(a.ID, a.Login) // the login as stored, folded
|
||||
_ = s.Resync()
|
||||
return store.Application{}, "", err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user