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:
@@ -51,9 +51,7 @@ func TestPhase09MigrationsFreshRollback(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := admin.RollbackLast(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rollbackBackendAdmin(t, admin)
|
||||
for _, name := range []string{"backend_users", "backend_user_roles", "backend_jwt_blacklist"} {
|
||||
if gdb.Migrator().HasTable(name) {
|
||||
t.Fatalf("%s survived admin rollback", name)
|
||||
@@ -205,9 +203,7 @@ func TestBackendAdminRollback(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.RollbackLast(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rollbackBackendAdmin(t, m)
|
||||
for _, name := range []string{"backend_users", "backend_user_roles", "backend_jwt_blacklist"} {
|
||||
if gdb.Migrator().HasTable(name) {
|
||||
t.Fatalf("%s survived admin rollback", name)
|
||||
@@ -443,3 +439,71 @@ func assertNoAutoMigrate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rollbackBackendAdmin rolls back every BackendAdminMigrations entry, newest
|
||||
// first, so the identity tables are dropped as a set.
|
||||
func rollbackBackendAdmin(t *testing.T, m *gormigrate.Gormigrate) {
|
||||
t.Helper()
|
||||
for range BackendAdminMigrations {
|
||||
if err := m.RollbackLast(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackendAdminEmailCaseInsensitiveUnique(t *testing.T) {
|
||||
db, _ := dedicatedDB(t, "lagoon_admin_email_ci")
|
||||
gdb, err := Use(t.Context(), db)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := Migrate(gdb, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := gdb.Exec(`INSERT INTO backend_users (login, email, password) VALUES ('ada', 'Ada@Example.Test', 'x')`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := gdb.Exec(`INSERT INTO backend_users (login, email, password) VALUES ('ada-2', 'ada@example.test', 'x')`).Error; err == nil {
|
||||
t.Fatal("an email differing only in case was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackendAdminEmailIndexRefusesCaseDuplicates(t *testing.T) {
|
||||
db, _ := dedicatedDB(t, "lagoon_admin_email_dupes")
|
||||
gdb, err := Use(t.Context(), db)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := Migrate(gdb, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m, err := migrator(gdb, "summercms.cabana", BackendAdminMigrations)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.RollbackLast(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A Winter copy can hold emails that differ only in case.
|
||||
for _, row := range [][2]string{{"ada", "Ada@Example.Test"}, {"ada-old", "ada@example.test"}} {
|
||||
if err := gdb.Exec(`INSERT INTO backend_users (login, email, password) VALUES (?, ?, 'x')`, row[0], row[1]).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
err = Migrate(gdb, nil)
|
||||
if err == nil {
|
||||
t.Fatal("migrate created the unique email index over case duplicates")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "ada@example.test (logins: ada, ada-old)") {
|
||||
t.Fatalf("error does not name the duplicates: %v", err)
|
||||
}
|
||||
if err := gdb.Exec(`UPDATE backend_users SET email = 'ada.old@example.test' WHERE login = 'ada-old'`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := Migrate(gdb, nil); err != nil {
|
||||
t.Fatalf("migrate after resolving duplicates: %v", err)
|
||||
}
|
||||
if !indexOn(indexDefs(t, gdb, "backend_users"), "lower(email)") {
|
||||
t.Fatal("unique lower(email) index missing")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user