From 1356912b3f32ecbe89f1f493c243daa46eca5d1a Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 4 Dec 2024 15:28:40 +0100 Subject: [PATCH] Make emails case insensitive (#886) * Make emails case insensitive * Set license-file * Update .sqlx * Bump version --- ...4e2a8f128cb3e0bda8a5bddb2b0b91f5167e.json} | 4 +- Cargo.lock | 80 +++++++++---------- Cargo.toml | 4 +- ...1204113104_case_insensitive_email.down.sql | 2 + ...241204113104_case_insensitive_email.up.sql | 2 + src/db/models/user.rs | 55 ++++++++++--- 6 files changed, 90 insertions(+), 57 deletions(-) rename .sqlx/{query-3017740cad14426beaca494cdf133e525b75984aa636f7cc8cc17a406e64b759.json => query-575b8069ccc2ad1d295da8e7ac004e2a8f128cb3e0bda8a5bddb2b0b91f5167e.json} (96%) create mode 100644 migrations/20241204113104_case_insensitive_email.down.sql create mode 100644 migrations/20241204113104_case_insensitive_email.up.sql diff --git a/.sqlx/query-3017740cad14426beaca494cdf133e525b75984aa636f7cc8cc17a406e64b759.json b/.sqlx/query-575b8069ccc2ad1d295da8e7ac004e2a8f128cb3e0bda8a5bddb2b0b91f5167e.json similarity index 96% rename from .sqlx/query-3017740cad14426beaca494cdf133e525b75984aa636f7cc8cc17a406e64b759.json rename to .sqlx/query-575b8069ccc2ad1d295da8e7ac004e2a8f128cb3e0bda8a5bddb2b0b91f5167e.json index c64ac829c..74524ea9d 100644 --- a/.sqlx/query-3017740cad14426beaca494cdf133e525b75984aa636f7cc8cc17a406e64b759.json +++ b/.sqlx/query-575b8069ccc2ad1d295da8e7ac004e2a8f128cb3e0bda8a5bddb2b0b91f5167e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub FROM \"user\" WHERE email = $1", + "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub FROM \"user\" WHERE email ILIKE $1", "describe": { "columns": [ { @@ -121,5 +121,5 @@ true ] }, - "hash": "3017740cad14426beaca494cdf133e525b75984aa636f7cc8cc17a406e64b759" + "hash": "575b8069ccc2ad1d295da8e7ac004e2a8f128cb3e0bda8a5bddb2b0b91f5167e" } diff --git a/Cargo.lock b/Cargo.lock index 89adde174..923106fc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,9 +145,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.93" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "arbitrary" @@ -312,7 +312,7 @@ dependencies = [ "axum-core 0.4.5", "bytes", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "http-body-util", "hyper 1.5.1", @@ -373,7 +373,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "http-body-util", "mime", @@ -398,7 +398,7 @@ dependencies = [ "fastrand", "futures-util", "headers", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "http-body-util", "mime", @@ -718,9 +718,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.21" +version = "4.5.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b" dependencies = [ "clap_builder", "clap_derive", @@ -728,9 +728,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.21" +version = "4.5.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1" dependencies = [ "anstream", "anstyle", @@ -1093,7 +1093,7 @@ dependencies = [ [[package]] name = "defguard" -version = "1.1.3" +version = "1.1.4" dependencies = [ "anyhow", "argon2", @@ -1937,7 +1937,7 @@ dependencies = [ "base64 0.21.7", "bytes", "headers-core", - "http 1.1.0", + "http 1.2.0", "httpdate", "mime", "sha1", @@ -1949,7 +1949,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 1.1.0", + "http 1.2.0", ] [[package]] @@ -2015,9 +2015,9 @@ dependencies = [ [[package]] name = "http" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", @@ -2042,7 +2042,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.1.0", + "http 1.2.0", ] [[package]] @@ -2053,7 +2053,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "pin-project-lite", ] @@ -2124,7 +2124,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "httparse", "httpdate", @@ -2156,7 +2156,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", - "http 1.1.0", + "http 1.2.0", "hyper 1.5.1", "hyper-util", "rustls 0.23.19", @@ -2201,7 +2201,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "hyper 1.5.1", "pin-project-lite", @@ -2842,7 +2842,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http 1.1.0", + "http 1.2.0", "httparse", "memchr", "mime", @@ -3246,14 +3246,14 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.7.0" +version = "3.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8781a75c6205af67215f382092b6e0a4ff3734798523e69073d4bcd294ec767b" +checksum = "d830939c76d294956402033aee57a6da7b438f2294eb94864c37b0569053a42c" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.90", + "syn 1.0.109", ] [[package]] @@ -3780,7 +3780,7 @@ dependencies = [ "rustc-hash", "rustls 0.23.19", "socket2", - "thiserror 2.0.3", + "thiserror 2.0.4", "tokio", "tracing", ] @@ -3799,7 +3799,7 @@ dependencies = [ "rustls 0.23.19", "rustls-pki-types", "slab", - "thiserror 2.0.3", + "thiserror 2.0.4", "tinyvec", "tracing", "web-time", @@ -3992,7 +3992,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "http-body-util", "hyper 1.5.1", @@ -5216,11 +5216,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +checksum = "2f49a1853cf82743e3b7950f77e0f4d622ca36cf4317cba00c767838bac8d490" dependencies = [ - "thiserror-impl 2.0.3", + "thiserror-impl 2.0.4", ] [[package]] @@ -5236,9 +5236,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +checksum = "8381894bb3efe0c4acac3ded651301ceee58a15d47c2e34885ed1908ad667061" dependencies = [ "proc-macro2", "quote", @@ -5257,9 +5257,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -5280,9 +5280,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -5324,9 +5324,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.41.1" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", @@ -5416,9 +5416,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -5559,7 +5559,7 @@ dependencies = [ "bitflags 2.6.0", "bytes", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "http-body-util", "http-range-header", diff --git a/Cargo.toml b/Cargo.toml index 4155794cd..2bd02980b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "defguard" -version = "1.1.3" +version = "1.1.4" edition = "2021" -license = "Apache-2.0" +license-file = "LICENSE.md" homepage = "https://defguard.net/" repository = "https://github.com/DefGuard/defguard" rust-version = "1.80" diff --git a/migrations/20241204113104_case_insensitive_email.down.sql b/migrations/20241204113104_case_insensitive_email.down.sql new file mode 100644 index 000000000..6babbd215 --- /dev/null +++ b/migrations/20241204113104_case_insensitive_email.down.sql @@ -0,0 +1,2 @@ +DROP INDEX email_unique_idx; +ALTER TABLE "user" ADD CONSTRAINT "user_email_key" UNIQUE (email); diff --git a/migrations/20241204113104_case_insensitive_email.up.sql b/migrations/20241204113104_case_insensitive_email.up.sql new file mode 100644 index 000000000..a4f407550 --- /dev/null +++ b/migrations/20241204113104_case_insensitive_email.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE "user" DROP CONSTRAINT "user_email_key"; +CREATE UNIQUE INDEX email_unique_idx on "user" (LOWER(email)); diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 6315d5d81..1fb18459d 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -584,7 +584,7 @@ impl User { } /// Verify recovery code. If it is valid, consume it, so it can't be used again. - pub async fn verify_recovery_code( + pub(crate) async fn verify_recovery_code( &mut self, pool: &PgPool, code: &str, @@ -626,7 +626,10 @@ impl User { .await } - pub async fn find_by_email<'e, E>(executor: E, email: &str) -> Result, SqlxError> + pub(crate) async fn find_by_email<'e, E>( + executor: E, + email: &str, + ) -> Result, SqlxError> where E: PgExecutor<'e>, { @@ -635,7 +638,7 @@ impl User { "SELECT id, username, password_hash, last_name, first_name, email, phone, \ mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub \ - FROM \"user\" WHERE email = $1", + FROM \"user\" WHERE email ILIKE $1", email ) .fetch_optional(executor) @@ -643,7 +646,10 @@ impl User { } // FIXME: Remove `LIMIT 1` when `openid_sub` is unique. - pub async fn find_by_sub<'e, E>(executor: E, sub: &str) -> Result, SqlxError> + pub(crate) async fn find_by_sub<'e, E>( + executor: E, + sub: &str, + ) -> Result, SqlxError> where E: PgExecutor<'e>, { @@ -659,7 +665,7 @@ impl User { .await } - pub async fn member_of_names<'e, E>(&self, executor: E) -> Result, SqlxError> + pub(crate) async fn member_of_names<'e, E>(&self, executor: E) -> Result, SqlxError> where E: PgExecutor<'e>, { @@ -672,7 +678,7 @@ impl User { .await } - pub async fn member_of<'e, E>(&self, executor: E) -> Result>, SqlxError> + pub(crate) async fn member_of<'e, E>(&self, executor: E) -> Result>, SqlxError> where E: PgExecutor<'e>, { @@ -689,7 +695,7 @@ impl User { /// Returns a vector of [`UserDevice`]s (hence the name). /// [`UserDevice`] is a struct containing additional network info about a device. /// If you only need [`Device`]s, use [`User::devices()`] instead. - pub async fn user_devices(&self, pool: &PgPool) -> Result, SqlxError> { + pub(crate) async fn user_devices(&self, pool: &PgPool) -> Result, SqlxError> { let devices = self.devices(pool).await?; let mut user_devices = Vec::new(); for device in devices { @@ -703,7 +709,7 @@ impl User { /// Returns a vector of [`Device`]s related to a user. If you want to get [`UserDevice`]s (which contain additional network info), /// use [`User::user_devices()`] instead. - pub async fn devices<'e, E>(&self, executor: E) -> Result>, SqlxError> + pub(crate) async fn devices<'e, E>(&self, executor: E) -> Result>, SqlxError> where E: PgExecutor<'e>, { @@ -717,7 +723,7 @@ impl User { .await } - pub async fn wallets<'e, E>(&self, executor: E) -> Result, SqlxError> + pub(crate) async fn wallets<'e, E>(&self, executor: E) -> Result, SqlxError> where E: PgExecutor<'e>, { @@ -731,7 +737,7 @@ impl User { .await } - pub async fn oauth2authorizedapps<'e, E>( + pub(crate) async fn oauth2authorizedapps<'e, E>( &self, executor: E, ) -> Result, SqlxError> @@ -751,7 +757,7 @@ impl User { .await } - pub async fn security_keys(&self, pool: &PgPool) -> Result, SqlxError> { + pub(crate) async fn security_keys(&self, pool: &PgPool) -> Result, SqlxError> { query_as!( SecurityKey, "SELECT id \"id!\", name FROM webauthn WHERE user_id = $1", @@ -777,7 +783,7 @@ impl User { Ok(()) } - pub async fn remove_from_group<'e, E>( + pub(crate) async fn remove_from_group<'e, E>( &self, executor: E, group: &Group, @@ -797,7 +803,7 @@ impl User { } /// Remove authorized apps by their client id's from user - pub async fn remove_oauth2_authorized_apps<'e, E>( + pub(crate) async fn remove_oauth2_authorized_apps<'e, E>( &self, executor: E, app_client_ids: &[i64], @@ -1006,4 +1012,27 @@ mod test { } assert_eq!(user.recovery_codes.len(), 0); } + + #[sqlx::test] + async fn test_email_case_insensitivity(pool: PgPool) { + let harry = User::new( + "hpotter", + Some("pass123"), + "Potter", + "Harry", + "h.potter@hogwart.edu.uk", + None, + ); + assert!(harry.save(&pool).await.is_ok()); + + let henry = User::new( + "h.potter", + Some("pass123"), + "Potter", + "Henry", + "h.potter@hogwart.edu.uk", + None, + ); + assert!(henry.save(&pool).await.is_err()); + } }