fix(09): WR-11 enforce case-insensitive unique backend user emails

Add a backend admin migration that creates a unique index on
lower(backend_users.email). Rows copied from WinterCMS may hold emails
that differ only in case, so the migration refuses to run and names the
clashing logins instead of choosing an account to drop.
This commit is contained in:
Jakub Zych
2026-10-01 23:12:29 +02:00
parent 5d79f7d0bb
commit 2da8112dbb
4 changed files with 108 additions and 12 deletions

View File

@@ -1,6 +1,9 @@
package lagoon
import (
"fmt"
"strings"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
@@ -9,10 +12,11 @@ import (
// It is distinct from the frontend jwt_blacklist table.
const BackendJWTBlacklistTable = "backend_jwt_blacklist"
// BackendAdminMigrations creates Winter-shaped backend identity tables and
// seeds the developer and publisher system roles. History is isolated under
// the summercms.cabana plugin id. DDL is re-runnable so the system-role seed
// stays idempotent if the history row is removed.
// BackendAdminMigrations creates Winter-shaped backend identity tables, seeds
// the developer and publisher system roles and makes backend user emails
// unique case-insensitively. History is isolated under the summercms.cabana
// plugin id. DDL is re-runnable so the system-role seed stays idempotent if
// the history row is removed.
var BackendAdminMigrations = []*gormigrate.Migration{
{
ID: "202609240001_backend_admin_identity",
@@ -85,4 +89,32 @@ ON CONFLICT (name) DO NOTHING`,
return nil
},
},
{
// Emails are matched case-insensitively at login and by admin:create,
// so the table enforces the same rule. Rows copied from Winter may
// hold two emails that differ only in case; the migration refuses to
// run and names them instead of picking an account to drop.
ID: "202610010001_backend_users_email_ci_unique",
Migrate: func(tx *gorm.DB) error {
var dupes []struct {
Email string
Logins string
}
if err := tx.Raw(`SELECT lower(email) AS email, string_agg(login, ', ' ORDER BY id) AS logins
FROM backend_users GROUP BY lower(email) HAVING count(*) > 1 ORDER BY 1`).Scan(&dupes).Error; err != nil {
return err
}
if len(dupes) > 0 {
parts := make([]string, len(dupes))
for i, d := range dupes {
parts[i] = fmt.Sprintf("%s (logins: %s)", d.Email, d.Logins)
}
return fmt.Errorf("lagoon: backend_users has emails that differ only in case: %s; change or remove the duplicates, then run migrate again", strings.Join(parts, "; "))
}
return tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS backend_users_email_lower_unique ON backend_users (lower(email))`).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Exec(`DROP INDEX IF EXISTS backend_users_email_lower_unique`).Error
},
},
}