From 7566b9ca09f4dd9dbdef945066e13c9d2c75bac7 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 7 Dec 2023 10:06:21 +0100 Subject: [PATCH 01/26] Update axum to 0.7, jsonwebtoken to 9.2, lettre to 0.11, secp256k1 to 0.28; fix testing; fix clippy lints (#465) --- Cargo.lock | 335 ++++++++++++++++------ Cargo.toml | 9 +- model-derive/src/lib.rs | 10 +- src/auth/mod.rs | 2 +- src/db/models/settings.rs | 1 + src/db/models/wireguard.rs | 2 +- src/handlers/auth.rs | 14 +- src/handlers/mail.rs | 6 +- src/handlers/openid_flow.rs | 16 +- src/handlers/settings.rs | 27 +- src/handlers/ssh_authorized_keys.rs | 8 +- src/headers.rs | 1 + src/ldap/mod.rs | 2 + src/lib.rs | 25 +- tests/auth.rs | 11 +- tests/common/client.rs | 41 +-- tests/common/mod.rs | 14 +- tests/enrollment.rs | 2 +- tests/forward_auth.rs | 12 +- tests/oauth.rs | 5 +- tests/openid.rs | 31 +- tests/settings.rs | 2 +- tests/user.rs | 3 +- tests/webhook.rs | 2 +- tests/wireguard.rs | 2 +- tests/wireguard_network_allowed_groups.rs | 2 +- tests/wireguard_network_import.rs | 2 +- tests/wireguard_network_stats.rs | 2 +- tests/worker.rs | 2 +- 29 files changed, 382 insertions(+), 209 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db6259ecc..de5d7e60b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -283,14 +283,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.3.4", "bitflags 1.3.2", "bytes", "futures-util", - "headers", - "http", - "http-body", - "hyper", + "http 0.2.11", + "http-body 0.4.5", + "hyper 0.14.27", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202651474fe73c62d9e0a56c6133f7a0ff1dc1c8cf7a5b03381af2a26553ac9d" +dependencies = [ + "async-trait", + "axum-core 0.4.1", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.0.1", + "hyper-util", "itoa", "matchit", "memchr", @@ -311,11 +339,11 @@ dependencies = [ [[package]] name = "axum-client-ip" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ef117890a418b7832678d9ea1e1c08456dd7b2fd1dadb9676cd6f0fe7eb4b21" +checksum = "0f5ffe4637708b326c621d5494ab6c91dcf62ee440fa6ee967d289315a9c6f81" dependencies = [ - "axum", + "axum 0.7.2", "forwarded-header-value", "serde", ] @@ -329,14 +357,56 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 0.2.11", + "http-body 0.4.5", "mime", "rustversion", "tower-layer", "tower-service", ] +[[package]] +name = "axum-core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77cb22c689c44d4c07b0ab44ebc25d69d8ae601a2f28fb8d672d344178fa17aa" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-extra" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523ae92256049a3b02d3bb4df80152386cd97ddba0c8c5077619bdc8c4b1859b" +dependencies = [ + "axum 0.7.2", + "axum-core 0.4.1", + "bytes", + "futures-util", + "headers", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -686,9 +756,9 @@ dependencies = [ [[package]] name = "cookie" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8" dependencies = [ "aes-gcm", "base64 0.21.5", @@ -924,8 +994,9 @@ version = "0.8.0" dependencies = [ "anyhow", "argon2", - "axum", + "axum 0.7.2", "axum-client-ip", + "axum-extra", "base64 0.21.5", "bincode", "bytes", @@ -1589,7 +1660,26 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.11", + "indexmap 2.1.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 1.0.0", "indexmap 2.1.0", "slab", "tokio", @@ -1630,14 +1720,14 @@ dependencies = [ [[package]] name = "headers" -version = "0.3.9" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ "base64 0.21.5", "bytes", "headers-core", - "http", + "http 1.0.0", "httpdate", "mime", "sha1", @@ -1645,11 +1735,11 @@ dependencies = [ [[package]] name = "headers-core" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http", + "http 1.0.0", ] [[package]] @@ -1722,6 +1812,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.5" @@ -1729,15 +1830,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", - "http", + "http 0.2.11", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.0.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +dependencies = [ + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", "pin-project-lite", ] [[package]] name = "http-range-header" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" +checksum = "3ce4ef31cda248bbdb6e6820603b82dfcd9e833db65a43e997a0ccec777d11fe" [[package]] name = "httparse" @@ -1776,9 +1900,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.22", + "http 0.2.11", + "http-body 0.4.5", "httparse", "httpdate", "itoa", @@ -1790,6 +1914,25 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f9214f3e703236b221f1a9cd88ec8b4adfa5296de01ab96216361f4692f56" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.0", + "http 1.0.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "tokio", +] + [[package]] name = "hyper-rustls" version = "0.24.2" @@ -1797,8 +1940,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", - "hyper", + "http 0.2.11", + "hyper 0.14.27", "rustls", "tokio", "tokio-rustls", @@ -1810,7 +1953,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper", + "hyper 0.14.27", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -1823,12 +1966,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper", + "hyper 0.14.27", "native-tls", "tokio", "tokio-native-tls", ] +[[package]] +name = "hyper-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca339002caeb0d159cc6e023dff48e199f081e42fa039895c7c6f38b37f2e9d" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "hyper 1.0.1", + "pin-project-lite", + "socket2 0.5.5", + "tokio", + "tower", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.58" @@ -2007,6 +2170,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -2031,7 +2203,7 @@ dependencies = [ "base64 0.21.5", "js-sys", "pem", - "ring 0.17.6", + "ring 0.17.7", "serde", "serde_json", "simple_asn1", @@ -2275,9 +2447,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi", @@ -2437,7 +2609,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c11e44798ad209ccdd91fc192f0526a369a01234f7373e1b141c96d7cee4f0e" dependencies = [ - "proc-macro-crate 2.0.0", + "proc-macro-crate 2.0.1", "proc-macro2", "quote", "syn 2.0.39", @@ -2452,7 +2624,7 @@ dependencies = [ "base64 0.13.1", "chrono", "getrandom", - "http", + "http 0.2.11", "rand", "serde", "serde_json", @@ -2528,7 +2700,7 @@ dependencies = [ "dyn-clone", "ed25519-dalek", "hmac", - "http", + "http 0.2.11", "itertools 0.10.5", "log", "oauth2", @@ -2673,7 +2845,7 @@ version = "3.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be30eaf4b0a9fba5336683b38de57bb86d179a35862ba6bfcf57625d006bde5b" dependencies = [ - "proc-macro-crate 2.0.0", + "proc-macro-crate 2.0.1", "proc-macro2", "quote", "syn 1.0.109", @@ -2980,11 +3152,12 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" dependencies = [ - "toml_edit 0.20.7", + "toml_datetime", + "toml_edit 0.20.2", ] [[package]] @@ -3253,10 +3426,10 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.22", + "http 0.2.11", + "http-body 0.4.5", + "hyper 0.14.27", "hyper-rustls", "hyper-tls", "ipnet", @@ -3315,9 +3488,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.6" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "684d5e6e18f669ccebf64a92236bb7db9a34f07be010e3627368182027180866" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", "getrandom", @@ -3439,7 +3612,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" dependencies = [ "log", - "ring 0.17.6", + "ring 0.17.7", "rustls-webpki", "sct", ] @@ -3471,7 +3644,7 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.6", + "ring 0.17.7", "untrusted 0.9.0", ] @@ -3541,7 +3714,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.6", + "ring 0.17.7", "untrusted 0.9.0", ] @@ -3571,9 +3744,9 @@ dependencies = [ [[package]] name = "secp256k1-sys" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e67c467c38fd24bd5499dc9a18183b31575c12ee549197e3e20d57aa4fe3b7" +checksum = "4dd97a086ec737e30053fd5c46f097465d25bb81dd3608825f65298c4c98be83" dependencies = [ "cc", ] @@ -3903,11 +4076,11 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" +checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" dependencies = [ - "itertools 0.11.0", + "itertools 0.12.0", "nom", "unicode_categories", ] @@ -4475,9 +4648,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" [[package]] name = "toml_edit" @@ -4492,9 +4665,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.20.7" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ "indexmap 2.1.0", "toml_datetime", @@ -4509,14 +4682,14 @@ checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" dependencies = [ "async-stream", "async-trait", - "axum", + "axum 0.6.20", "base64 0.21.5", "bytes", "flate2", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.22", + "http 0.2.11", + "http-body 0.4.5", + "hyper 0.14.27", "hyper-timeout", "percent-encoding", "pin-project", @@ -4568,15 +4741,15 @@ dependencies = [ [[package]] name = "tower-cookies" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40f38d941a2ffd8402b36e02ae407637a9caceb693aaf2edc910437db0f36984" +checksum = "4fd0118512cf0b3768f7fcccf0bef1ae41d68f2b45edc1e77432b36c97c56c6d" dependencies = [ "async-trait", - "axum-core", - "cookie 0.17.0", + "axum-core 0.4.1", + "cookie 0.18.0", "futures-util", - "http", + "http 1.0.0", "parking_lot", "pin-project-lite", "tower-layer", @@ -4585,16 +4758,16 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.4.4" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" +checksum = "09e12e6351354851911bdf8c2b8f2ab15050c567d70a8b9a37ae7b8301a4080d" dependencies = [ "bitflags 2.4.1", "bytes", - "futures-core", "futures-util", - "http", - "http-body", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", "http-range-header", "httpdate", "mime", @@ -4793,9 +4966,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] name = "unicode-ident" @@ -5291,9 +5464,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.24" +version = "0.5.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0383266b19108dfc6314a56047aa545a1b4d1be60e799b4dbdd407b56402704b" +checksum = "b7e87b8dfbe3baffbe687eef2e164e32286eff31a5ee16463ce03d991643ec94" dependencies = [ "memchr", ] @@ -5358,18 +5531,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.28" +version = "0.7.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d6f15f7ade05d2a4935e34a457b936c23dc70a05cc1d97133dc99e7a3fe0f0e" +checksum = "5d075cf85bbb114e933343e087b92f2146bac0d55b534cbb8188becf0039948e" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.28" +version = "0.7.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbbad221e3f78500350ecbd7dfa4e63ef945c05f4c61cb7f4d3f84cd0bba649b" +checksum = "86cd5ca076997b97ef09d3ad65efe811fa68c9e874cb636ccb211223a813b0c2" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 63b96f516..3be2b52cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,9 @@ edition = "2021" model_derive = { path = "model-derive" } anyhow = "1.0" argon2 = { version = "0.5", features = ["std"] } -axum = { version = "0.6", features = ["headers"] } -axum-client-ip = "0.4" +axum = { version = "0.7" } +axum-client-ip = "0.5" +axum-extra = { version = "0.9", features = ["typed-header"] } base64 = "0.21" bincode = "1.3" chrono = { version = "0.4", default-features = false, features = [ @@ -71,8 +72,8 @@ tokio = { version = "1", features = [ ] } tokio-stream = "0.1" tonic = { version = "0.10", features = ["gzip", "tls", "tls-roots"] } -tower-cookies = { version = "0.9", features = ["private"] } -tower-http = { version = "0.4", features = ["fs", "trace"] } +tower-cookies = { version = "0.10", features = ["private"] } +tower-http = { version = "0.5", features = ["fs", "trace"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } uaparser = "0.6" diff --git a/model-derive/src/lib.rs b/model-derive/src/lib.rs index 3b277bffc..238120dac 100644 --- a/model-derive/src/lib.rs +++ b/model-derive/src/lib.rs @@ -77,13 +77,11 @@ pub fn derive(input: TokenStream) -> TokenStream { } } - let fields = if let Data::Struct(DataStruct { + let Data::Struct(DataStruct { fields: Fields::Named(FieldsNamed { named, .. }), .. }) = ast.data - { - named - } else { + else { // fail for other but `struct` unimplemented!(); }; @@ -99,7 +97,7 @@ pub fn derive(input: TokenStream) -> TokenStream { let mut add_comma = false; let mut value_number = 1; - fields.iter().for_each(|field| { + named.iter().for_each(|field| { if let Some(name) = &field.ident { if name != "id" { if add_comma { @@ -144,7 +142,7 @@ pub fn derive(input: TokenStream) -> TokenStream { // TODO: handle fields wrapped in Option // field arguments for queries - let insert_args = fields.iter().filter_map(|field| { + let insert_args = named.iter().filter_map(|field| { if let Some(name) = &field.ident { if name != "id" { if let Some(tokens) = model_attr(field) { diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 5d0d0cea3..7cb02a292 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -130,7 +130,7 @@ where Ok(Some(session)) => { if session.expired() { let _result = session.delete(&appstate.pool).await; - cookies.remove(Cookie::named("defguard_session")); + cookies.remove(Cookie::from("defguard_session")); Err(WebError::Authorization("Session expired".into())) } else { Ok(session) diff --git a/src/db/models/settings.rs b/src/db/models/settings.rs index ebf2b9315..cf2097b5f 100644 --- a/src/db/models/settings.rs +++ b/src/db/models/settings.rs @@ -98,6 +98,7 @@ impl Settings { /// Check if all required SMTP options are configured. /// /// Meant to be used to check if sending emails is enabled in current instance. + #[must_use] pub fn smtp_configured(&self) -> bool { self.smtp_server.is_some() && self.smtp_port.is_some() diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 83547ac70..6d17dd9ff 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -361,7 +361,7 @@ impl WireguardNetwork { .await?; Ok(wireguard_network_device) } else { - error!("Device {} not allowed in network {}", device, self); + error!("Device {device} not allowed in network {self}"); Err(WireguardNetworkError::DeviceNotAllowed(format!( "{}", device diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index 68f01d871..2bde95a19 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -1,10 +1,9 @@ use axum::{ extract::{Json, State}, - headers::UserAgent, http::StatusCode, - TypedHeader, }; use axum_client_ip::{InsecureClientIp, LeftmostXForwardedFor}; +use axum_extra::{headers::UserAgent, TypedHeader}; use secrecy::ExposeSecret; use serde_json::json; use sqlx::types::Uuid; @@ -103,7 +102,7 @@ pub async fn authenticate( }; let server_config = SERVER_CONFIG.get().ok_or(WebError::ServerConfigMissing)?; - let auth_cookie = Cookie::build(SESSION_COOKIE_NAME, session.clone().id) + let auth_cookie = Cookie::build((SESSION_COOKIE_NAME, session.clone().id)) .domain( server_config .cookie_domain @@ -114,9 +113,8 @@ pub async fn authenticate( .http_only(true) .secure(!server_config.cookie_insecure) .same_site(SameSite::Lax) - .max_age(max_age) - .finish(); - cookies.add(auth_cookie); + .max_age(max_age); + cookies.add(auth_cookie.into()); let login_event_type = "AUTHENTICATION".to_string(); @@ -187,7 +185,7 @@ pub async fn logout( State(appstate): State, ) -> ApiResult { // remove auth cookie - cookies.remove(Cookie::named(SESSION_COOKIE_NAME)); + cookies.remove(Cookie::from(SESSION_COOKIE_NAME)); // remove stored session session.delete(&appstate.pool).await?; Ok(ApiResponse::default()) @@ -205,7 +203,7 @@ pub async fn mfa_enable( user.enable_mfa(&appstate.pool).await?; if user.mfa_enabled { info!("Enabled MFA for user {}", user.username); - cookies.remove(Cookie::named("defguard_sesssion")); + cookies.remove(Cookie::from("defguard_sesssion")); session.delete(&appstate.pool).await?; debug!( "Removed auth session for user {} after enabling MFA", diff --git a/src/handlers/mail.rs b/src/handlers/mail.rs index 6729a28e6..573308094 100644 --- a/src/handlers/mail.rs +++ b/src/handlers/mail.rs @@ -216,8 +216,8 @@ pub async fn send_gateway_disconnected_email( .ok_or(WebError::ServerConfigMissing)? .admin_groupname; let admin_users = User::find_by_group_name(pool, admin_group_name).await?; - let gateway_name = gateway_name.unwrap_or("".into()); - for user in admin_users.into_iter() { + let gateway_name = gateway_name.unwrap_or_default(); + for user in admin_users { let mail = Mail { to: user.email, subject: GATEWAY_DISCONNECTED.to_string(), @@ -232,7 +232,7 @@ pub async fn send_gateway_disconnected_email( let to = mail.to.clone(); match mail_tx.send(mail) { - Ok(_) => { + Ok(()) => { info!("Sent gateway disconnected notification to {}", &to); } Err(err) => { diff --git a/src/handlers/openid_flow.rs b/src/handlers/openid_flow.rs index 55a310757..0b247ff5e 100644 --- a/src/handlers/openid_flow.rs +++ b/src/handlers/openid_flow.rs @@ -331,10 +331,13 @@ async fn login_redirect( ) -> Result<(StatusCode, HeaderMap), WebError> { let server_config = SERVER_CONFIG.get().ok_or(WebError::ServerConfigMissing)?; let base_url = server_config.url.join("api/v1/oauth/authorize").unwrap(); - let cookie = Cookie::build( + let cookie = Cookie::build(( SIGN_IN_COOKIE_NAME, - format!("{base_url}?{}", serde_urlencoded::to_string(data).unwrap()), - ) + format!( + "{base_url}?{}", + serde_urlencoded::to_string(data).unwrap_or_default() + ), + )) .domain( server_config .cookie_domain @@ -345,11 +348,10 @@ async fn login_redirect( .secure(!server_config.cookie_insecure) .same_site(SameSite::Lax) .http_only(true) - .max_age(Duration::minutes(10)) - .finish(); + .max_age(Duration::minutes(10)); let key = Key::from(server_config.secret_key.expose_secret().as_bytes()); let private_cookies = cookies.private(&key); - private_cookies.add(cookie); + private_cookies.add(cookie.into()); Ok(redirect_to("/login")) } @@ -410,7 +412,7 @@ pub async fn authorization( appstate.config.secret_key.expose_secret().as_bytes(), ); let private_cookies = cookies.private(&key); - private_cookies.remove(Cookie::named(SIGN_IN_COOKIE_NAME)); + private_cookies.remove(Cookie::from(SIGN_IN_COOKIE_NAME)); let location = generate_auth_code_redirect( appstate, data, diff --git a/src/handlers/settings.rs b/src/handlers/settings.rs index b0ef02623..bcf633700 100644 --- a/src/handlers/settings.rs +++ b/src/handlers/settings.rs @@ -96,20 +96,17 @@ pub async fn patch_settings( pub async fn test_ldap_settings(_admin: AdminRole, State(appstate): State) -> ApiResult { debug!("Testing LDAP connection"); - match LDAPConnection::create(&appstate.pool).await { - Ok(_) => { - debug!("LDAP connected succesfully"); - Ok(ApiResponse { - json: json!({}), - status: StatusCode::OK, - }) - } - Err(_) => { - debug!("LDAP connection rejected"); - Ok(ApiResponse { - json: json!({}), - status: StatusCode::BAD_REQUEST, - }) - } + if LDAPConnection::create(&appstate.pool).await.is_ok() { + debug!("LDAP connected succesfully"); + Ok(ApiResponse { + json: json!({}), + status: StatusCode::OK, + }) + } else { + debug!("LDAP connection rejected"); + Ok(ApiResponse { + json: json!({}), + status: StatusCode::BAD_REQUEST, + }) } } diff --git a/src/handlers/ssh_authorized_keys.rs b/src/handlers/ssh_authorized_keys.rs index 0696bcd8c..067fa39de 100644 --- a/src/handlers/ssh_authorized_keys.rs +++ b/src/handlers/ssh_authorized_keys.rs @@ -62,7 +62,7 @@ pub async fn get_authorized_keys( debug!("User {username} is not a member of group {group_name}",); } } else { - debug!("Specified user does not exist") + debug!("Specified user does not exist"); } } None => { @@ -70,12 +70,12 @@ pub async fn get_authorized_keys( // fetch all users in group let users = group.fetch_all_members(&appstate.pool).await?; for user in users { - add_user_keys_to_list(user) + add_user_keys_to_list(user); } } } } else { - debug!("Specified group does not exist") + debug!("Specified group does not exist"); } } None => { @@ -86,7 +86,7 @@ pub async fn get_authorized_keys( if let Some(user) = User::find_by_username(&appstate.pool, username).await? { add_user_keys_to_list(user); } else { - debug!("Specified user does not exist") + debug!("Specified user does not exist"); } } } diff --git a/src/headers.rs b/src/headers.rs index d9380509d..810c36fe2 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -31,6 +31,7 @@ pub fn parse_user_agent<'a>( } } +#[must_use] pub fn get_device_info( user_agent_parser: &Arc, user_agent: &str, diff --git a/src/ldap/mod.rs b/src/ldap/mod.rs index 5f801629f..62459a938 100644 --- a/src/ldap/mod.rs +++ b/src/ldap/mod.rs @@ -36,6 +36,7 @@ pub struct LDAPConfig { impl LDAPConfig { /// Constructs user distinguished name. + #[must_use] pub fn user_dn(&self, username: &str) -> String { format!( "{}={},{}", @@ -44,6 +45,7 @@ impl LDAPConfig { } /// Constructs group distinguished name. + #[must_use] pub fn group_dn(&self, groupname: &str) -> String { format!( "{}={},{}", diff --git a/src/lib.rs b/src/lib.rs index 6e39d69dd..8b2057e9a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,14 +10,17 @@ use axum::{ handler::HandlerWithoutStateExt, http::{Request, StatusCode}, routing::{delete, get, patch, post, put}, - Extension, Router, Server, + serve, Extension, Router, }; use handlers::settings::{get_settings_essentials, patch_settings, test_ldap_settings}; use secrecy::ExposeSecret; -use tokio::sync::{ - broadcast::Sender, - mpsc::{UnboundedReceiver, UnboundedSender}, - OnceCell, +use tokio::{ + net::TcpListener, + sync::{ + broadcast::Sender, + mpsc::{UnboundedReceiver, UnboundedSender}, + OnceCell, + }, }; use tower_cookies::CookieManagerLayer; use tower_http::{ @@ -344,11 +347,13 @@ pub async fn run_web_server( ); info!("Started web services"); let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), config.http_port); - // TODO: map_err() and remove `hyper` as depenency from Cargo.toml - Server::bind(&addr) - .serve(webapp.into_make_service_with_connect_info::()) - .await - .map_err(|err| anyhow!("Web server can't be started {}", err)) + let listener = TcpListener::bind(&addr).await?; + serve( + listener, + webapp.into_make_service_with_connect_info::(), + ) + .await + .map_err(|err| anyhow!("Web server can't be started {err}")) } /// Automates test objects creation to easily setup development environment. diff --git a/tests/auth.rs b/tests/auth.rs index 111c58096..2fbe0a79c 100644 --- a/tests/auth.rs +++ b/tests/auth.rs @@ -1,7 +1,7 @@ mod common; -use crate::common::ClientState; -use axum::http::StatusCode; +use std::{str::FromStr, time::SystemTime}; + use claims::assert_err; use defguard::{ auth::TOTP_CODE_VALIDITY_PERIOD, @@ -12,16 +12,15 @@ use defguard::{ }; use ethers_core::types::transaction::eip712::{Eip712, TypedData}; use otpauth::TOTP; -use reqwest::header::USER_AGENT; +use reqwest::{header::USER_AGENT, StatusCode}; use secp256k1::{rand::rngs::OsRng, All, Message, Secp256k1, SecretKey}; use serde::Deserialize; use serde_json::json; use sqlx::query; -use std::{str::FromStr, time::SystemTime}; use webauthn_authenticator_rs::{prelude::Url, softpasskey::SoftPasskey, WebauthnAuthenticator}; use webauthn_rs::prelude::{CreationChallengeResponse, RequestChallengeResponse}; -use self::common::{client::TestClient, make_test_client}; +use self::common::{client::TestClient, make_test_client, ClientState, X_FORWARDED_FOR}; #[derive(Deserialize)] pub struct RecoveryCodes { @@ -1061,7 +1060,7 @@ async fn test_login_ip_headers() { let response = client .post("/api/v1/auth") .header(USER_AGENT, user_agent_header_iphone) - .header("X-Forwarded-For", "10.0.0.20, 10.1.1.10") + .header(X_FORWARDED_FOR, "10.0.0.20, 10.1.1.10") .json(&auth) .send() .await; diff --git a/tests/common/client.rs b/tests/common/client.rs index 91b5809ea..e6a8fe07b 100644 --- a/tests/common/client.rs +++ b/tests/common/client.rs @@ -1,22 +1,14 @@ -use axum::{ - http::{ - self, - header::{HeaderMap, HeaderName, HeaderValue}, - StatusCode, - }, - Router, Server, -}; +use std::{net::SocketAddr, sync::Arc}; + +use axum::{serve, Router}; use bytes::Bytes; use reqwest::{ cookie::{Cookie, Jar}, + header::{HeaderMap, HeaderName}, redirect::Policy, - Client, Url, -}; -use std::{ - convert::TryFrom, - net::{SocketAddr, TcpListener}, - sync::Arc, + Client, StatusCode, Url, }; +use tokio::net::TcpListener; pub struct TestClient { client: Client, @@ -27,14 +19,17 @@ pub struct TestClient { #[allow(dead_code)] impl TestClient { #[must_use] - pub fn new(app: Router) -> Self { - let listener = TcpListener::bind("127.0.0.1:0").expect("Could not bind ephemeral socket"); + pub async fn new(app: Router) -> Self { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("Could not bind ephemeral socket"); let port = listener.local_addr().unwrap().port(); tokio::spawn(async move { - let server = Server::from_tcp(listener) - .unwrap() - .serve(app.into_make_service_with_connect_info::()); + let server = serve( + listener, + app.into_make_service_with_connect_info::(), + ); server.await.expect("server error"); }); @@ -144,13 +139,7 @@ impl RequestBuilder { self } - pub fn header(mut self, key: K, value: V) -> Self - where - HeaderName: TryFrom, - >::Error: Into, - HeaderValue: TryFrom, - >::Error: Into, - { + pub fn header(mut self, key: HeaderName, value: &str) -> Self { self.builder = self.builder.header(key, value); self } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index e00fef02a..b46517f0b 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -2,7 +2,6 @@ pub(crate) mod client; use std::sync::{Arc, Mutex}; -use axum::http::StatusCode; use defguard::{ auth::failed_login::FailedLoginMap, build_webapp, @@ -13,16 +12,23 @@ use defguard::{ mail::Mail, SERVER_CONFIG, }; +use reqwest::{header::HeaderName, StatusCode}; use secrecy::ExposeSecret; use sqlx::{postgres::PgConnectOptions, query, types::Uuid}; -use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::{ broadcast::{self, Receiver}, - mpsc::unbounded_channel, + mpsc::{unbounded_channel, UnboundedReceiver}, }; use self::client::TestClient; +#[allow(dead_code)] +pub const X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host"); +#[allow(dead_code)] +pub const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for"); +#[allow(dead_code)] +pub const X_FORWARDED_URI: HeaderName = HeaderName::from_static("x-forwarded-uri"); + pub async fn init_test_db() -> (DbPool, DefGuardConfig) { let config = DefGuardConfig::new_test_config(); let _ = SERVER_CONFIG.set(config.clone()); @@ -150,7 +156,7 @@ pub async fn make_base_client(pool: DbPool, config: DefGuardConfig) -> (TestClie user_agent_parser, failed_logins, ); - (TestClient::new(webapp), client_state) + (TestClient::new(webapp).await, client_state) } #[allow(dead_code)] diff --git a/tests/enrollment.rs b/tests/enrollment.rs index 6594dafc5..f7767ec0b 100644 --- a/tests/enrollment.rs +++ b/tests/enrollment.rs @@ -1,10 +1,10 @@ mod common; -use axum::http::StatusCode; use defguard::{ db::{models::enrollment::Enrollment, DbPool}, handlers::{AddUserData, Auth}, }; +use reqwest::StatusCode; use serde::Deserialize; use serde_json::json; diff --git a/tests/forward_auth.rs b/tests/forward_auth.rs index 8df3f46c8..6c7f9debc 100644 --- a/tests/forward_auth.rs +++ b/tests/forward_auth.rs @@ -1,9 +1,9 @@ mod common; -use axum::http::StatusCode; use defguard::{db::Wallet, handlers::Auth, SERVER_CONFIG}; +use reqwest::StatusCode; -use self::common::{client::TestClient, make_test_client}; +use self::common::{client::TestClient, make_test_client, X_FORWARDED_HOST, X_FORWARDED_URI}; async fn make_client() -> TestClient { let (client, client_state) = make_test_client().await; @@ -27,8 +27,8 @@ async fn test_forward_auth() { // auth request from reverse proxy let response = client .get("/api/v1/forward_auth") - .header("x-forwarded-host", "app.example.com") - .header("x-forwarded-uri", "/test") + .header(X_FORWARDED_HOST, "app.example.com") + .header(X_FORWARDED_URI, "/test") .send() .await; assert_eq!(response.status(), StatusCode::TEMPORARY_REDIRECT); @@ -57,8 +57,8 @@ async fn test_forward_auth() { client.set_cookie(&auth_cookie); let response = client .get("/api/v1/forward_auth") - .header("x-forwarded-host", "app.example.com") - .header("x-forwarded-uri", "/test") + .header(X_FORWARDED_HOST, "app.example.com") + .header(X_FORWARDED_URI, "/test") .send() .await; assert_eq!(response.status(), StatusCode::OK); diff --git a/tests/oauth.rs b/tests/oauth.rs index ef0fcf615..7186206da 100644 --- a/tests/oauth.rs +++ b/tests/oauth.rs @@ -2,7 +2,6 @@ mod common; use std::borrow::Cow; -use axum::http::StatusCode; use defguard::{ db::{ models::{ @@ -13,7 +12,7 @@ use defguard::{ }, handlers::Auth, }; -use reqwest::Url; +use reqwest::{header::CONTENT_TYPE, StatusCode, Url}; use serde_json::json; use self::common::{client::TestClient, make_test_client}; @@ -413,7 +412,7 @@ async fn test_token_client_credentials() { let response = client .post("/api/v1/oauth/token") - .header("Content-Type", "application/x-www-form-urlencoded") + .header(CONTENT_TYPE, "application/x-www-form-urlencoded") .body("client_id=WrongClient&client_secret=WrongSecret&grant_type=code") .send() .await; diff --git a/tests/openid.rs b/tests/openid.rs index 818a041bf..bd58b222e 100644 --- a/tests/openid.rs +++ b/tests/openid.rs @@ -1,4 +1,6 @@ -use axum::http::{header::ToStrError, StatusCode}; +use std::str::FromStr; + +use axum::http::header::ToStrError; use claims::assert_err; use defguard::{ config::DefGuardConfig, @@ -17,7 +19,10 @@ use openidconnect::{ EmptyAdditionalClaims, HttpRequest, HttpResponse, IssuerUrl, Nonce, OAuth2TokenResponse, PkceCodeChallenge, RedirectUrl, Scope, UserInfoClaims, }; -use reqwest::header::USER_AGENT; +use reqwest::{ + header::{HeaderName, AUTHORIZATION, CONTENT_TYPE, USER_AGENT}, + StatusCode, +}; use rsa::RsaPrivateKey; use serde::Deserialize; @@ -200,7 +205,7 @@ async fn test_openid_flow() { // exchange wrong code for token should fail let response = client .post("/api/v1/oauth/token") - .header("Content-Type", "application/x-www-form-urlencoded") + .header(CONTENT_TYPE, "application/x-www-form-urlencoded") .body(format!( "grant_type=authorization_code&\ code=ncuoew2323&\ @@ -216,7 +221,7 @@ async fn test_openid_flow() { // exchange correct code for token let response = client .post("/api/v1/oauth/token") - .header("Content-Type", "application/x-www-form-urlencoded") + .header(CONTENT_TYPE, "application/x-www-form-urlencoded") .body(format!( "grant_type=authorization_code&\ code={}&\ @@ -232,21 +237,16 @@ async fn test_openid_flow() { // make sure access token cannot be used to manage defguard server itself client.post("/api/v1/auth/logout").send().await; let token_response: CoreTokenResponse = response.json().await; + let bearer = format!("Bearer {}", token_response.access_token().secret()); let response = client .get("/api/v1/network") - .header( - "Authorization", - format!("Bearer {}", token_response.access_token().secret()), - ) + .header(AUTHORIZATION, &bearer) .send() .await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); let response = client .get("/api/v1/user") - .header( - "Authorization", - format!("Bearer {}", token_response.access_token().secret()), - ) + .header(AUTHORIZATION, &bearer) .send() .await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); @@ -259,7 +259,7 @@ async fn test_openid_flow() { // check code cannot be reused let response = client .post("/api/v1/oauth/token") - .header("Content-Type", "application/x-www-form-urlencoded") + .header(CONTENT_TYPE, "application/x-www-form-urlencoded") .body(format!( "grant_type=authorization_code&\ code={}&\ @@ -383,7 +383,10 @@ async fn http_client( _ => unimplemented!(), }; for (key, value) in &request.headers { - test_request = test_request.header(key.as_str(), value.to_str()?); + test_request = test_request.header( + HeaderName::from_str(key.as_str()).unwrap(), + value.to_str().unwrap(), + ); } let response = test_request.body(request.body).send().await; diff --git a/tests/settings.rs b/tests/settings.rs index c3e22d6f2..2c38e4a2c 100644 --- a/tests/settings.rs +++ b/tests/settings.rs @@ -1,11 +1,11 @@ mod common; -use axum::http::StatusCode; use common::ClientState; use defguard::{ db::models::settings::{Settings, SettingsPatch}, handlers::Auth, }; +use reqwest::StatusCode; use self::common::{client::TestClient, make_test_client}; diff --git a/tests/user.rs b/tests/user.rs index 5d0d3e96b..c28af9d4f 100644 --- a/tests/user.rs +++ b/tests/user.rs @@ -1,6 +1,5 @@ mod common; -use axum::http::StatusCode; use defguard::{ db::{ models::{oauth2client::OAuth2Client, wallet::keccak256, NewOpenIDClient}, @@ -10,7 +9,7 @@ use defguard::{ hex::to_lower_hex, }; use ethers_core::types::transaction::eip712::{Eip712, TypedData}; -use reqwest::header::USER_AGENT; +use reqwest::{header::USER_AGENT, StatusCode}; use secp256k1::{rand::rngs::OsRng, Message, Secp256k1}; use serde_json::{json, Value}; use tokio_stream::{self as stream, StreamExt}; diff --git a/tests/webhook.rs b/tests/webhook.rs index bb56756d6..eab72420c 100644 --- a/tests/webhook.rs +++ b/tests/webhook.rs @@ -1,7 +1,7 @@ mod common; -use axum::http::StatusCode; use defguard::{db::WebHook, handlers::Auth}; +use reqwest::StatusCode; use self::common::{client::TestClient, make_test_client}; diff --git a/tests/wireguard.rs b/tests/wireguard.rs index 60df675e8..1ec6bc110 100644 --- a/tests/wireguard.rs +++ b/tests/wireguard.rs @@ -1,11 +1,11 @@ mod common; -use axum::http::StatusCode; use defguard::{ db::{models::device::WireguardNetworkDevice, Device, GatewayEvent, WireguardNetwork}, handlers::{wireguard::WireguardNetworkData, Auth}, }; use matches::assert_matches; +use reqwest::StatusCode; use serde_json::{json, Value}; use self::common::make_test_client; diff --git a/tests/wireguard_network_allowed_groups.rs b/tests/wireguard_network_allowed_groups.rs index 717c7ed23..8c34295aa 100644 --- a/tests/wireguard_network_allowed_groups.rs +++ b/tests/wireguard_network_allowed_groups.rs @@ -1,12 +1,12 @@ mod common; -use axum::http::StatusCode; use claims::assert_err; use defguard::{ db::{DbPool, Device, GatewayEvent, Group, User, WireguardNetwork}, handlers::{wireguard::ImportedNetworkData, Auth}, }; use matches::assert_matches; +use reqwest::StatusCode; use serde_json::json; use self::common::{fetch_user_details, make_test_client}; diff --git a/tests/wireguard_network_import.rs b/tests/wireguard_network_import.rs index 5cdbee0a5..cedaa0587 100644 --- a/tests/wireguard_network_import.rs +++ b/tests/wireguard_network_import.rs @@ -1,11 +1,11 @@ mod common; -use axum::http::StatusCode; use defguard::{ db::{models::device::UserDevice, Device, GatewayEvent, WireguardNetwork}, handlers::{wireguard::ImportedNetworkData, Auth}, }; use matches::assert_matches; +use reqwest::StatusCode; use serde_json::json; use tokio::sync::broadcast::error::TryRecvError; diff --git a/tests/wireguard_network_stats.rs b/tests/wireguard_network_stats.rs index 12caf1e50..4df73325b 100644 --- a/tests/wireguard_network_stats.rs +++ b/tests/wireguard_network_stats.rs @@ -1,6 +1,5 @@ mod common; -use axum::http::StatusCode; use chrono::{Datelike, Duration, NaiveDate, SubsecRound, Timelike, Utc}; use defguard::{ db::{ @@ -11,6 +10,7 @@ use defguard::{ }, handlers::Auth, }; +use reqwest::StatusCode; use serde_json::{json, Value}; use self::common::make_test_client; diff --git a/tests/worker.rs b/tests/worker.rs index a6436c57f..844bd38c0 100644 --- a/tests/worker.rs +++ b/tests/worker.rs @@ -2,7 +2,6 @@ mod common; use std::sync::{Arc, Mutex}; -use axum::http::StatusCode; use defguard::{ grpc::{worker::JobStatus, WorkerDetail, WorkerState}, handlers::{ @@ -10,6 +9,7 @@ use defguard::{ Auth, }, }; +use reqwest::StatusCode; use self::common::{client::TestClient, make_test_client}; From 7510da9dcdb1d81b7b31ece6bced1d30f83667c8 Mon Sep 17 00:00:00 2001 From: blazej-teonite <104985522+blazej-teonite@users.noreply.github.com> Date: Tue, 12 Dec 2023 21:10:29 +0100 Subject: [PATCH 02/26] feat: password reset functionality (#470) * Password reset functionality --- ...861c744c520880d33815044babe0eff1d644.json} | 4 +- ...dba022ef520a90d23903c84595f0502e86e1d.json | 131 ++++++++++ ...0fe9162b8ae22444b29379c85fe0b9ca01799.json | 14 + ...3716a6383680dee11e01682597e24bf3ce53.json} | 12 +- ...302ecae6a6379c64b7686fdd7e6a6d32a9ed2.json | 21 ++ ...1bd32df7c4f5af1457dfa5406b3c82fb77231.json | 14 - ...05c6de14f5f73248769f9faad9c28c411fad8.json | 14 + ...6153215f982b9c8e43f68ebb62597df31d8f8.json | 20 -- ...335ae28f92e6653abcfd001118a965cb5d36.json} | 12 +- build.rs | 2 + ...07160414_enrollment_table_generic.down.sql | 3 + ...1207160414_enrollment_table_generic.up.sql | 3 + proto | 2 +- src/config.rs | 16 ++ src/db/models/enrollment.rs | 199 +++++++++------ src/db/models/user.rs | 13 + src/error.rs | 32 +-- src/grpc/enrollment.rs | 115 +++++---- src/grpc/mod.rs | 19 +- src/grpc/password_reset.rs | 241 ++++++++++++++++++ src/handlers/mail.rs | 42 ++- src/handlers/user.rs | 88 ++++++- src/lib.rs | 6 +- src/templates.rs | 29 +++ templates/mail_password_reset_start.tera | 38 +++ tests/enrollment.rs | 10 +- web/src/i18n/en/index.ts | 2 + web/src/i18n/i18n-types.ts | 16 ++ .../UserEditButton/ResetPasswordButton.tsx | 42 +++ .../UserEditButton/UserEditButton.tsx | 2 + web/src/shared/hooks/useApi.tsx | 5 + web/src/shared/mutations.ts | 1 + web/src/shared/types.ts | 5 + 33 files changed, 981 insertions(+), 192 deletions(-) rename .sqlx/{query-727ed093b693f0f48e83779f9ee58de725cbec807ae286ae5900f426f2492f9d.json => query-05c7cfa839d4411955c207095336861c744c520880d33815044babe0eff1d644.json} (55%) create mode 100644 .sqlx/query-06888dd28b82c3b72612889932adba022ef520a90d23903c84595f0502e86e1d.json create mode 100644 .sqlx/query-0fa037b89c50e40ad95ae203cf70fe9162b8ae22444b29379c85fe0b9ca01799.json rename .sqlx/{query-71e9c3921543a990ba6c6a8a0a9254757a1208ea9bfb2e0570626a39f8463467.json => query-2444fb3a5c12155737d934d156a63716a6383680dee11e01682597e24bf3ce53.json} (79%) create mode 100644 .sqlx/query-43f040a77856123f0dadfef6aad302ecae6a6379c64b7686fdd7e6a6d32a9ed2.json delete mode 100644 .sqlx/query-71465915aa72eed5d35fb82149a1bd32df7c4f5af1457dfa5406b3c82fb77231.json create mode 100644 .sqlx/query-a2a9f1e0388ce6705deba02473a05c6de14f5f73248769f9faad9c28c411fad8.json delete mode 100644 .sqlx/query-effe03b1e22929191b7d07e51586153215f982b9c8e43f68ebb62597df31d8f8.json rename .sqlx/{query-49749227cacd46dd9a392d58233a511ee03c0df12cb28ce247d574923fee31a9.json => query-f6cb6d9cdf5db43582cdb77f3c60335ae28f92e6653abcfd001118a965cb5d36.json} (79%) create mode 100644 migrations/20231207160414_enrollment_table_generic.down.sql create mode 100644 migrations/20231207160414_enrollment_table_generic.up.sql create mode 100644 src/grpc/password_reset.rs create mode 100644 templates/mail_password_reset_start.tera create mode 100644 web/src/pages/users/UsersOverview/components/UserEditButton/ResetPasswordButton.tsx diff --git a/.sqlx/query-727ed093b693f0f48e83779f9ee58de725cbec807ae286ae5900f426f2492f9d.json b/.sqlx/query-05c7cfa839d4411955c207095336861c744c520880d33815044babe0eff1d644.json similarity index 55% rename from .sqlx/query-727ed093b693f0f48e83779f9ee58de725cbec807ae286ae5900f426f2492f9d.json rename to .sqlx/query-05c7cfa839d4411955c207095336861c744c520880d33815044babe0eff1d644.json index 3bb8b2509..e656542bd 100644 --- a/.sqlx/query-727ed093b693f0f48e83779f9ee58de725cbec807ae286ae5900f426f2492f9d.json +++ b/.sqlx/query-05c7cfa839d4411955c207095336861c744c520880d33815044babe0eff1d644.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE enrollment SET used_at = $1 WHERE id = $2", + "query": "UPDATE token SET used_at = $1 WHERE id = $2", "describe": { "columns": [], "parameters": { @@ -11,5 +11,5 @@ }, "nullable": [] }, - "hash": "727ed093b693f0f48e83779f9ee58de725cbec807ae286ae5900f426f2492f9d" + "hash": "05c7cfa839d4411955c207095336861c744c520880d33815044babe0eff1d644" } diff --git a/.sqlx/query-06888dd28b82c3b72612889932adba022ef520a90d23903c84595f0502e86e1d.json b/.sqlx/query-06888dd28b82c3b72612889932adba022ef520a90d23903c84595f0502e86e1d.json new file mode 100644 index 000000000..68d237eba --- /dev/null +++ b/.sqlx/query-06888dd28b82c3b72612889932adba022ef520a90d23903c84595f0502e86e1d.json @@ -0,0 +1,131 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id \"id?\", username, password_hash, last_name, first_name, email, phone, ssh_key, pgp_key, pgp_cert_id, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes FROM \"user\" WHERE email = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id?", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "password_hash", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "last_name", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "first_name", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "email", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "phone", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "ssh_key", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "pgp_key", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "pgp_cert_id", + "type_info": "Text" + }, + { + "ordinal": 10, + "name": "mfa_enabled", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "totp_enabled", + "type_info": "Bool" + }, + { + "ordinal": 12, + "name": "email_mfa_enabled", + "type_info": "Bool" + }, + { + "ordinal": 13, + "name": "totp_secret", + "type_info": "Bytea" + }, + { + "ordinal": 14, + "name": "email_mfa_secret", + "type_info": "Bytea" + }, + { + "ordinal": 15, + "name": "mfa_method: _", + "type_info": { + "Custom": { + "name": "mfa_method", + "kind": { + "Enum": [ + "none", + "one_time_password", + "webauthn", + "web3", + "email" + ] + } + } + } + }, + { + "ordinal": 16, + "name": "recovery_codes", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + true, + true, + true, + true, + false, + false, + false, + true, + true, + false, + false + ] + }, + "hash": "06888dd28b82c3b72612889932adba022ef520a90d23903c84595f0502e86e1d" +} diff --git a/.sqlx/query-0fa037b89c50e40ad95ae203cf70fe9162b8ae22444b29379c85fe0b9ca01799.json b/.sqlx/query-0fa037b89c50e40ad95ae203cf70fe9162b8ae22444b29379c85fe0b9ca01799.json new file mode 100644 index 000000000..c2c883025 --- /dev/null +++ b/.sqlx/query-0fa037b89c50e40ad95ae203cf70fe9162b8ae22444b29379c85fe0b9ca01799.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM token\n WHERE user_id = $1\n AND used_at IS NULL", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "0fa037b89c50e40ad95ae203cf70fe9162b8ae22444b29379c85fe0b9ca01799" +} diff --git a/.sqlx/query-71e9c3921543a990ba6c6a8a0a9254757a1208ea9bfb2e0570626a39f8463467.json b/.sqlx/query-2444fb3a5c12155737d934d156a63716a6383680dee11e01682597e24bf3ce53.json similarity index 79% rename from .sqlx/query-71e9c3921543a990ba6c6a8a0a9254757a1208ea9bfb2e0570626a39f8463467.json rename to .sqlx/query-2444fb3a5c12155737d934d156a63716a6383680dee11e01682597e24bf3ce53.json index 6095c37cf..94fdee55e 100644 --- a/.sqlx/query-71e9c3921543a990ba6c6a8a0a9254757a1208ea9bfb2e0570626a39f8463467.json +++ b/.sqlx/query-2444fb3a5c12155737d934d156a63716a6383680dee11e01682597e24bf3ce53.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, user_id, admin_id, email, created_at, expires_at, used_at FROM enrollment WHERE id = $1", + "query": "SELECT id, user_id, admin_id, email, created_at, expires_at, used_at, token_type FROM token WHERE id = $1", "describe": { "columns": [ { @@ -37,6 +37,11 @@ "ordinal": 6, "name": "used_at", "type_info": "Timestamp" + }, + { + "ordinal": 7, + "name": "token_type", + "type_info": "Text" } ], "parameters": { @@ -47,12 +52,13 @@ "nullable": [ false, false, - false, + true, true, false, false, + true, true ] }, - "hash": "71e9c3921543a990ba6c6a8a0a9254757a1208ea9bfb2e0570626a39f8463467" + "hash": "2444fb3a5c12155737d934d156a63716a6383680dee11e01682597e24bf3ce53" } diff --git a/.sqlx/query-43f040a77856123f0dadfef6aad302ecae6a6379c64b7686fdd7e6a6d32a9ed2.json b/.sqlx/query-43f040a77856123f0dadfef6aad302ecae6a6379c64b7686fdd7e6a6d32a9ed2.json new file mode 100644 index 000000000..5ca41ad85 --- /dev/null +++ b/.sqlx/query-43f040a77856123f0dadfef6aad302ecae6a6379c64b7686fdd7e6a6d32a9ed2.json @@ -0,0 +1,21 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO token (id, user_id, admin_id, email, created_at, expires_at, used_at, token_type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Int8", + "Int8", + "Text", + "Timestamp", + "Timestamp", + "Timestamp", + "Text" + ] + }, + "nullable": [] + }, + "hash": "43f040a77856123f0dadfef6aad302ecae6a6379c64b7686fdd7e6a6d32a9ed2" +} diff --git a/.sqlx/query-71465915aa72eed5d35fb82149a1bd32df7c4f5af1457dfa5406b3c82fb77231.json b/.sqlx/query-71465915aa72eed5d35fb82149a1bd32df7c4f5af1457dfa5406b3c82fb77231.json deleted file mode 100644 index 60581bbde..000000000 --- a/.sqlx/query-71465915aa72eed5d35fb82149a1bd32df7c4f5af1457dfa5406b3c82fb77231.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM enrollment\n WHERE user_id = $1\n AND used_at IS NULL", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - }, - "hash": "71465915aa72eed5d35fb82149a1bd32df7c4f5af1457dfa5406b3c82fb77231" -} diff --git a/.sqlx/query-a2a9f1e0388ce6705deba02473a05c6de14f5f73248769f9faad9c28c411fad8.json b/.sqlx/query-a2a9f1e0388ce6705deba02473a05c6de14f5f73248769f9faad9c28c411fad8.json new file mode 100644 index 000000000..adb908c90 --- /dev/null +++ b/.sqlx/query-a2a9f1e0388ce6705deba02473a05c6de14f5f73248769f9faad9c28c411fad8.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM token\n WHERE user_id = $1\n AND token_type = 'PASSWORD_RESET'\n AND used_at IS NULL", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "a2a9f1e0388ce6705deba02473a05c6de14f5f73248769f9faad9c28c411fad8" +} diff --git a/.sqlx/query-effe03b1e22929191b7d07e51586153215f982b9c8e43f68ebb62597df31d8f8.json b/.sqlx/query-effe03b1e22929191b7d07e51586153215f982b9c8e43f68ebb62597df31d8f8.json deleted file mode 100644 index 40aebf630..000000000 --- a/.sqlx/query-effe03b1e22929191b7d07e51586153215f982b9c8e43f68ebb62597df31d8f8.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO enrollment (id, user_id, admin_id, email, created_at, expires_at, used_at) VALUES ($1, $2, $3, $4, $5, $6, $7)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Int8", - "Int8", - "Text", - "Timestamp", - "Timestamp", - "Timestamp" - ] - }, - "nullable": [] - }, - "hash": "effe03b1e22929191b7d07e51586153215f982b9c8e43f68ebb62597df31d8f8" -} diff --git a/.sqlx/query-49749227cacd46dd9a392d58233a511ee03c0df12cb28ce247d574923fee31a9.json b/.sqlx/query-f6cb6d9cdf5db43582cdb77f3c60335ae28f92e6653abcfd001118a965cb5d36.json similarity index 79% rename from .sqlx/query-49749227cacd46dd9a392d58233a511ee03c0df12cb28ce247d574923fee31a9.json rename to .sqlx/query-f6cb6d9cdf5db43582cdb77f3c60335ae28f92e6653abcfd001118a965cb5d36.json index 2ac3ac936..b115ecb03 100644 --- a/.sqlx/query-49749227cacd46dd9a392d58233a511ee03c0df12cb28ce247d574923fee31a9.json +++ b/.sqlx/query-f6cb6d9cdf5db43582cdb77f3c60335ae28f92e6653abcfd001118a965cb5d36.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, user_id, admin_id, email, created_at, expires_at, used_at FROM enrollment", + "query": "SELECT id, user_id, admin_id, email, created_at, expires_at, used_at, token_type FROM token", "describe": { "columns": [ { @@ -37,6 +37,11 @@ "ordinal": 6, "name": "used_at", "type_info": "Timestamp" + }, + { + "ordinal": 7, + "name": "token_type", + "type_info": "Text" } ], "parameters": { @@ -45,12 +50,13 @@ "nullable": [ false, false, - false, + true, true, false, false, + true, true ] }, - "hash": "49749227cacd46dd9a392d58233a511ee03c0df12cb28ce247d574923fee31a9" + "hash": "f6cb6d9cdf5db43582cdb77f3c60335ae28f92e6653abcfd001118a965cb5d36" } diff --git a/build.rs b/build.rs index 77a9db993..51800307d 100644 --- a/build.rs +++ b/build.rs @@ -9,12 +9,14 @@ fn main() -> Result<(), Box> { "proto/worker/worker.proto", "proto/wireguard/gateway.proto", "proto/enrollment/enrollment.proto", + "proto/password_reset/password_reset.proto", ], &[ "proto/core", "proto/worker", "proto/wireguard", "proto/enrollment", + "proto/password_reset", ], )?; println!("cargo:rerun-if-changed=proto"); diff --git a/migrations/20231207160414_enrollment_table_generic.down.sql b/migrations/20231207160414_enrollment_table_generic.down.sql new file mode 100644 index 000000000..fd4e54920 --- /dev/null +++ b/migrations/20231207160414_enrollment_table_generic.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE token RENAME TO enrollment; +ALTER TABLE enrollment ALTER admin_id SET NOT NULL; +ALTER TABLE enrollment DROP token_type; diff --git a/migrations/20231207160414_enrollment_table_generic.up.sql b/migrations/20231207160414_enrollment_table_generic.up.sql new file mode 100644 index 000000000..0379924a8 --- /dev/null +++ b/migrations/20231207160414_enrollment_table_generic.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE enrollment ALTER admin_id DROP NOT NULL; +ALTER TABLE enrollment ADD COLUMN token_type text DEFAULT 'ENROLLMENT'; +ALTER TABLE enrollment RENAME TO token; diff --git a/proto b/proto index 50f3791a2..67ce8c13c 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 50f3791a2d0104ad7d9ae69a3abde070743902c2 +Subproject commit 67ce8c13ca9cef8f49b6f8dbcf1dfdf8cc9aa9f4 diff --git a/src/config.rs b/src/config.rs index 7c79a5a35..892b5bb8a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -97,6 +97,14 @@ pub struct DefGuardConfig { #[serde(skip_serializing)] pub enrollment_token_timeout: Duration, + #[arg( + long, + env = "DEFGUARD_PASSWORD_RESET_TOKEN_TIMEOUT", + default_value = "24h" + )] + #[serde(skip_serializing)] + pub password_reset_token_timeout: Duration, + #[arg( long, env = "DEFGUARD_ENROLLMENT_SESSION_TIMEOUT", @@ -105,6 +113,14 @@ pub struct DefGuardConfig { #[serde(skip_serializing)] pub enrollment_session_timeout: Duration, + #[arg( + long, + env = "DEFGUARD_PASSWORD_RESET_SESSION_TIMEOUT", + default_value = "10m" + )] + #[serde(skip_serializing)] + pub password_reset_session_timeout: Duration, + #[arg(long, env = "DEFGUARD_COOKIE_DOMAIN")] pub cookie_domain: Option, diff --git a/src/db/models/enrollment.rs b/src/db/models/enrollment.rs index 5864071a3..2ea912aeb 100644 --- a/src/db/models/enrollment.rs +++ b/src/db/models/enrollment.rs @@ -13,11 +13,14 @@ use thiserror::Error; use tokio::sync::mpsc::UnboundedSender; use tonic::{Code, Status}; +pub static ENROLLMENT_TOKEN_TYPE: &str = "ENROLLMENT"; +pub static PASSWORD_RESET_TOKEN_TYPE: &str = "PASSWORD_RESET"; + const ENROLLMENT_START_MAIL_SUBJECT: &str = "Defguard user enrollment"; const DESKTOP_START_MAIL_SUBJECT: &str = "Defguard desktop client configuration"; #[derive(Error, Debug)] -pub enum EnrollmentError { +pub enum TokenError { #[error(transparent)] DbError(#[from] SqlxError), #[error("Enrollment token not found")] @@ -46,23 +49,23 @@ pub enum EnrollmentError { TemplateError(#[from] TemplateError), } -impl From for Status { - fn from(err: EnrollmentError) -> Self { +impl From for Status { + fn from(err: TokenError) -> Self { error!("{}", err); let (code, msg) = match err { - EnrollmentError::DbError(_) - | EnrollmentError::AdminNotFound - | EnrollmentError::UserNotFound - | EnrollmentError::NotificationError(_) - | EnrollmentError::WelcomeMsgNotConfigured - | EnrollmentError::WelcomeEmailNotConfigured - | EnrollmentError::TemplateError(_) - | EnrollmentError::TemplateErrorInternal(_) => (Code::Internal, "unexpected error"), - EnrollmentError::NotFound - | EnrollmentError::TokenExpired - | EnrollmentError::SessionExpired - | EnrollmentError::TokenUsed => (Code::Unauthenticated, "invalid token"), - EnrollmentError::AlreadyActive => (Code::InvalidArgument, "already active"), + TokenError::DbError(_) + | TokenError::AdminNotFound + | TokenError::UserNotFound + | TokenError::NotificationError(_) + | TokenError::WelcomeMsgNotConfigured + | TokenError::WelcomeEmailNotConfigured + | TokenError::TemplateError(_) + | TokenError::TemplateErrorInternal(_) => (Code::Internal, "unexpected error"), + TokenError::NotFound + | TokenError::TokenExpired + | TokenError::SessionExpired + | TokenError::TokenUsed => (Code::Unauthenticated, "invalid token"), + TokenError::AlreadyActive => (Code::InvalidArgument, "already active"), }; Status::new(code, msg) } @@ -70,23 +73,25 @@ impl From for Status { // Representation of a user enrollment session #[derive(Clone)] -pub struct Enrollment { +pub struct Token { pub id: String, pub user_id: i64, - pub admin_id: i64, + pub admin_id: Option, pub email: Option, pub created_at: NaiveDateTime, pub expires_at: NaiveDateTime, pub used_at: Option, + pub token_type: Option, } -impl Enrollment { +impl Token { #[must_use] pub fn new( user_id: i64, - admin_id: i64, + admin_id: Option, email: Option, token_timeout_seconds: u64, + token_type: Option, ) -> Self { let now = Utc::now(); Self { @@ -97,13 +102,14 @@ impl Enrollment { created_at: now.naive_utc(), expires_at: (now + Duration::seconds(token_timeout_seconds as i64)).naive_utc(), used_at: None, + token_type, } } - pub async fn save(&self, transaction: &mut PgConnection) -> Result<(), EnrollmentError> { + pub async fn save(&self, transaction: &mut PgConnection) -> Result<(), TokenError> { query!( - "INSERT INTO enrollment (id, user_id, admin_id, email, created_at, expires_at, used_at) \ - VALUES ($1, $2, $3, $4, $5, $6, $7)", + "INSERT INTO token (id, user_id, admin_id, email, created_at, expires_at, used_at, token_type) \ + VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", self.id, self.user_id, self.admin_id, @@ -111,6 +117,7 @@ impl Enrollment { self.created_at, self.expires_at, self.used_at, + self.token_type, ) .execute(transaction) .await?; @@ -147,85 +154,83 @@ impl Enrollment { &mut self, transaction: &mut PgConnection, session_timeout_seconds: u64, - ) -> Result { + ) -> Result { // check if token can be used if self.is_expired() { - return Err(EnrollmentError::TokenExpired); + return Err(TokenError::TokenExpired); } if self.is_used() { - return Err(EnrollmentError::TokenUsed); + return Err(TokenError::TokenUsed); } let now = Utc::now().naive_utc(); - query!( - "UPDATE enrollment SET used_at = $1 WHERE id = $2", - now, - self.id - ) - .execute(transaction) - .await?; + query!("UPDATE token SET used_at = $1 WHERE id = $2", now, self.id) + .execute(transaction) + .await?; self.used_at = Some(now); Ok(now + Duration::seconds(session_timeout_seconds as i64)) } - pub async fn find_by_id(pool: &DbPool, id: &str) -> Result { + pub async fn find_by_id(pool: &DbPool, id: &str) -> Result { match query_as!( Self, - "SELECT id, user_id, admin_id, email, created_at, expires_at, used_at \ - FROM enrollment WHERE id = $1", + "SELECT id, user_id, admin_id, email, created_at, expires_at, used_at, token_type \ + FROM token WHERE id = $1", id ) .fetch_optional(pool) .await? { Some(enrollment) => Ok(enrollment), - None => Err(EnrollmentError::NotFound), + None => Err(TokenError::NotFound), } } - pub async fn fetch_all(pool: &DbPool) -> Result, EnrollmentError> { - let enrollments = query_as!( + pub async fn fetch_all(pool: &DbPool) -> Result, TokenError> { + let tokens = query_as!( Self, - "SELECT id, user_id, admin_id, email, created_at, expires_at, used_at \ - FROM enrollment", + "SELECT id, user_id, admin_id, email, created_at, expires_at, used_at, token_type \ + FROM token", ) .fetch_all(pool) .await?; - Ok(enrollments) + Ok(tokens) } - pub async fn fetch_user<'e, E>(&self, executor: E) -> Result + pub async fn fetch_user<'e, E>(&self, executor: E) -> Result where E: sqlx::Executor<'e, Database = sqlx::Postgres>, { debug!("Fetching user for enrollment"); let Some(user) = User::find_by_id(executor, self.user_id).await? else { error!("User not found for enrollment token {}", self.id); - return Err(EnrollmentError::UserNotFound); + return Err(TokenError::UserNotFound); }; Ok(user) } - pub async fn fetch_admin<'e, E>(&self, executor: E) -> Result + pub async fn fetch_admin<'e, E>(&self, executor: E) -> Result, TokenError> where E: sqlx::Executor<'e, Database = sqlx::Postgres>, { debug!("Fetching admin for enrollment"); - let Some(user) = User::find_by_id(executor, self.admin_id).await? else { - error!("Admin not found for enrollment token {}", self.id); - return Err(EnrollmentError::AdminNotFound); - }; + if self.admin_id.is_none() { + return Ok(None); + } + + let admin_id = self.admin_id.unwrap(); + let user = User::find_by_id(executor, admin_id).await?; Ok(user) } pub async fn delete_unused_user_tokens( transaction: &mut PgConnection, user_id: i64, - ) -> Result<(), EnrollmentError> { + ) -> Result<(), TokenError> { debug!("Deleting unused enrollment tokens for user {user_id}"); let result = query!( - r#"DELETE FROM enrollment + r#"DELETE FROM token WHERE user_id = $1 AND used_at IS NULL"#, user_id @@ -240,6 +245,28 @@ impl Enrollment { Ok(()) } + pub async fn delete_unused_user_password_reset_tokens( + transaction: &mut PgConnection, + user_id: i64, + ) -> Result<(), TokenError> { + debug!("Deleting unused password reset tokens for user {user_id}"); + let result = query!( + r#"DELETE FROM token + WHERE user_id = $1 + AND token_type = 'PASSWORD_RESET' + AND used_at IS NULL"#, + user_id + ) + .execute(transaction) + .await?; + debug!( + "Deleted {} unused password reset tokens for user {user_id}", + result.rows_affected() + ); + + Ok(()) + } + /// Prepare context for rendering welcome messages /// Available tags include: /// - first_name @@ -254,7 +281,7 @@ impl Enrollment { pub async fn get_welcome_message_context( &self, transaction: &mut PgConnection, - ) -> Result { + ) -> Result { debug!( "Preparing welcome message context for enrollment token {}", self.id @@ -269,10 +296,13 @@ impl Enrollment { context.insert("username", &user.username); context.insert("defguard_url", &SERVER_CONFIG.get().unwrap().url); context.insert("defguard_version", &VERSION); - context.insert("admin_first_name", &admin.first_name); - context.insert("admin_last_name", &admin.last_name); - context.insert("admin_email", &admin.email); - context.insert("admin_phone", &admin.phone); + + if let Some(admin) = admin { + context.insert("admin_first_name", &admin.first_name); + context.insert("admin_last_name", &admin.last_name); + context.insert("admin_email", &admin.email); + context.insert("admin_phone", &admin.phone); + } Ok(context) } @@ -282,7 +312,7 @@ impl Enrollment { pub async fn get_welcome_page_content( &self, transaction: &mut PgConnection, - ) -> Result { + ) -> Result { let settings = Settings::get_settings(&mut *transaction).await?; // load configured content as template @@ -300,7 +330,7 @@ impl Enrollment { transaction: &mut PgConnection, ip_address: String, device_info: Option, - ) -> Result { + ) -> Result { let settings = Settings::get_settings(&mut *transaction).await?; // load configured content as template @@ -331,13 +361,13 @@ impl User { enrollment_service_url: Url, send_user_notification: bool, mail_tx: UnboundedSender, - ) -> Result { + ) -> Result { info!( "User {} starting enrollment for user {}, notification enabled: {send_user_notification}", admin.username, self.username ); if self.has_password() { - return Err(EnrollmentError::AlreadyActive); + return Err(TokenError::AlreadyActive); } let user_id = self.id.expect("User without ID"); @@ -346,7 +376,13 @@ impl User { self.clear_unused_enrollment_tokens(&mut *transaction) .await?; - let enrollment = Enrollment::new(user_id, admin_id, email.clone(), token_timeout_seconds); + let enrollment = Token::new( + user_id, + Some(admin_id), + email.clone(), + token_timeout_seconds, + Some(ENROLLMENT_TOKEN_TYPE.to_string()), + ); enrollment.save(&mut *transaction).await?; if send_user_notification { @@ -366,7 +402,7 @@ impl User { enrollment_service_url, &enrollment.id, ) - .map_err(|err| EnrollmentError::NotificationError(err.to_string()))?, + .map_err(|err| TokenError::NotificationError(err.to_string()))?, attachments: Vec::new(), result_tx: None, }; @@ -379,7 +415,7 @@ impl User { } Err(err) => { error!("Error sending mail: {err}"); - return Err(EnrollmentError::NotificationError(err.to_string())); + return Err(TokenError::NotificationError(err.to_string())); } } } @@ -399,7 +435,7 @@ impl User { enrollment_service_url: Url, send_user_notification: bool, mail_tx: UnboundedSender, - ) -> Result { + ) -> Result { info!( "User {} starting desktop configuration for user {}, notification enabled: {send_user_notification}", admin.username, self.username @@ -411,7 +447,13 @@ impl User { self.clear_unused_enrollment_tokens(&mut *transaction) .await?; - let enrollment = Enrollment::new(user_id, admin_id, email.clone(), token_timeout_seconds); + let enrollment = Token::new( + user_id, + Some(admin_id), + email.clone(), + token_timeout_seconds, + Some(ENROLLMENT_TOKEN_TYPE.to_string()), + ); enrollment.save(&mut *transaction).await?; if send_user_notification { @@ -431,7 +473,7 @@ impl User { &enrollment_service_url, &enrollment.id, ) - .map_err(|err| EnrollmentError::NotificationError(err.to_string()))?, + .map_err(|err| TokenError::NotificationError(err.to_string()))?, attachments: Vec::new(), result_tx: None, }; @@ -456,30 +498,43 @@ impl User { async fn clear_unused_enrollment_tokens( &self, transaction: &mut PgConnection, - ) -> Result<(), EnrollmentError> { + ) -> Result<(), TokenError> { info!( "Removing unused enrollment tokens for user {}", self.username ); - Enrollment::delete_unused_user_tokens(transaction, self.id.expect("Missing user ID")).await + Token::delete_unused_user_tokens(transaction, self.id.expect("Missing user ID")).await } + + // pub async fn request_password_reset( + // &self, + // transaction: &mut PgConnection, + // admin: &User, + // // email: Option, + // token_timeout_seconds: u64, + // // enrollment_service_url: Url, + // // send_user_notification: bool, + // mail_tx: UnboundedSender, + // ) -> Result { + + // } } impl Settings { - pub fn enrollment_welcome_message(&self) -> Result { + pub fn enrollment_welcome_message(&self) -> Result { self.enrollment_welcome_message.clone().ok_or_else(|| { error!("Enrollment welcome message not configured"); - EnrollmentError::WelcomeMsgNotConfigured + TokenError::WelcomeMsgNotConfigured }) } - pub fn enrollment_welcome_email(&self) -> Result { + pub fn enrollment_welcome_email(&self) -> Result { if self.enrollment_use_welcome_message_as_email { return self.enrollment_welcome_message(); } self.enrollment_welcome_email.clone().ok_or_else(|| { error!("Enrollment welcome email not configured"); - EnrollmentError::WelcomeEmailNotConfigured + TokenError::WelcomeEmailNotConfigured }) } } diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 5745b308b..4677ba82c 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -551,6 +551,19 @@ impl User { .await } + pub async fn find_by_email(pool: &DbPool, email: &str) -> Result, SqlxError> { + query_as!( + Self, + "SELECT id \"id?\", username, password_hash, last_name, first_name, email, \ + phone, ssh_key, pgp_key, pgp_cert_id, mfa_enabled, totp_enabled, email_mfa_enabled, \ + totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes \ + FROM \"user\" WHERE email = $1", + email + ) + .fetch_optional(pool) + .await + } + pub async fn member_of<'e, E>(&self, executor: E) -> Result, SqlxError> where E: sqlx::Executor<'e, Database = sqlx::Postgres>, diff --git a/src/error.rs b/src/error.rs index cce08e63e..dfc3a9ac6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,7 +5,7 @@ use thiserror::Error; use crate::{ auth::failed_login::FailedLoginError, db::models::{ - device::DeviceError, enrollment::EnrollmentError, error::ModelError, + device::DeviceError, enrollment::TokenError, error::ModelError, wireguard::WireguardNetworkError, }, grpc::GatewayMapError, @@ -126,23 +126,23 @@ impl From for WebError { } } -impl From for WebError { - fn from(err: EnrollmentError) -> Self { +impl From for WebError { + fn from(err: TokenError) -> Self { error!("{}", err); match err { - EnrollmentError::DbError(msg) => WebError::DbError(msg.to_string()), - EnrollmentError::NotFound - | EnrollmentError::UserNotFound - | EnrollmentError::AdminNotFound => WebError::ObjectNotFound(err.to_string()), - EnrollmentError::TokenExpired - | EnrollmentError::SessionExpired - | EnrollmentError::TokenUsed => WebError::Authorization(err.to_string()), - EnrollmentError::AlreadyActive => WebError::BadRequest(err.to_string()), - EnrollmentError::NotificationError(_) - | EnrollmentError::WelcomeMsgNotConfigured - | EnrollmentError::WelcomeEmailNotConfigured - | EnrollmentError::TemplateError(_) - | EnrollmentError::TemplateErrorInternal(_) => { + TokenError::DbError(msg) => WebError::DbError(msg.to_string()), + TokenError::NotFound | TokenError::UserNotFound | TokenError::AdminNotFound => { + WebError::ObjectNotFound(err.to_string()) + } + TokenError::TokenExpired | TokenError::SessionExpired | TokenError::TokenUsed => { + WebError::Authorization(err.to_string()) + } + TokenError::AlreadyActive => WebError::BadRequest(err.to_string()), + TokenError::NotificationError(_) + | TokenError::WelcomeMsgNotConfigured + | TokenError::WelcomeEmailNotConfigured + | TokenError::TemplateError(_) + | TokenError::TemplateErrorInternal(_) => { WebError::Http(StatusCode::INTERNAL_SERVER_ERROR) } } diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 5e32e2ede..c21309a83 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -5,7 +5,7 @@ use crate::{ db::{ models::{ device::{DeviceConfig, DeviceInfo, WireguardNetworkDevice}, - enrollment::{Enrollment, EnrollmentError}, + enrollment::{Token, TokenError, ENROLLMENT_TOKEN_TYPE}, wireguard::WireguardNetwork, }, DbPool, Device, GatewayEvent, Settings, User, @@ -93,7 +93,7 @@ impl EnrollmentServer { async fn validate_session( &self, request: &Request, - ) -> Result { + ) -> Result { debug!("Validating enrollment session token: {request:?}"); let token = if let Some(token) = request.metadata().get("authorization") { token @@ -104,7 +104,7 @@ impl EnrollmentServer { return Err(Status::unauthenticated("Missing authorization header")); }; - let enrollment = Enrollment::find_by_id(&self.pool, token).await?; + let enrollment = Token::find_by_id(&self.pool, token).await?; if enrollment.is_session_valid(self.config.enrollment_session_timeout.as_secs()) { Ok(enrollment) @@ -131,57 +131,67 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { debug!("Starting enrollment session: {request:?}"); let request = request.into_inner(); // fetch enrollment token - let mut enrollment = Enrollment::find_by_id(&self.pool, &request.token).await?; + let mut enrollment = Token::find_by_id(&self.pool, &request.token).await?; - // fetch related users - let user = enrollment.fetch_user(&self.pool).await?; - let admin = enrollment.fetch_admin(&self.pool).await?; - - let mut transaction = self.pool.begin().await.map_err(|_| { - error!("Failed to begin transaction"); - Status::internal("unexpected error") - })?; + if let Some(token_type) = enrollment.clone().token_type { + if token_type != ENROLLMENT_TOKEN_TYPE { + return Err(Status::permission_denied("invalid token")); + } - // validate token & start session - info!("Starting enrollment session for user {}", user.username); - let session_deadline = enrollment - .start_session( - &mut transaction, - self.config.enrollment_session_timeout.as_secs(), - ) - .await?; + // fetch related users + let user = enrollment.fetch_user(&self.pool).await?; + let admin = enrollment.fetch_admin(&self.pool).await?; - let settings = Settings::get_settings(&mut *transaction) - .await - .map_err(|_| { - error!("Failed to get settings"); + let mut transaction = self.pool.begin().await.map_err(|_| { + error!("Failed to begin transaction"); Status::internal("unexpected error") })?; - let user_info = InitialUserInfo::from_user(&self.pool, user) - .await - .map_err(|_| { - error!("Failed to get user info"); - Status::internal("unexpected error") - })?; + // validate token & start session + info!("Starting enrollment session for user {}", user.username); + let session_deadline = enrollment + .start_session( + &mut transaction, + self.config.enrollment_session_timeout.as_secs(), + ) + .await?; - let response = EnrollmentStartResponse { - admin: Some(admin.into()), - user: Some(user_info), - deadline_timestamp: session_deadline.timestamp(), - final_page_content: enrollment - .get_welcome_page_content(&mut transaction) - .await?, - vpn_setup_optional: settings.enrollment_vpn_step_optional, - instance: Some(Instance::new(settings, self.config.url.clone()).into()), - }; + let settings = Settings::get_settings(&mut *transaction) + .await + .map_err(|_| { + error!("Failed to get settings"); + Status::internal("unexpected error") + })?; - transaction.commit().await.map_err(|_| { - error!("Failed to commit transaction"); - Status::internal("unexpected error") - })?; + let user_info = InitialUserInfo::from_user(&self.pool, user) + .await + .map_err(|_| { + error!("Failed to get user info"); + Status::internal("unexpected error") + })?; - Ok(Response::new(response)) + let admin_info = admin.and_then(|v| Some(AdminInfo::from(v))); + + let response = EnrollmentStartResponse { + admin: admin_info, + user: Some(user_info), + deadline_timestamp: session_deadline.timestamp(), + final_page_content: enrollment + .get_welcome_page_content(&mut transaction) + .await?, + vpn_setup_optional: settings.enrollment_vpn_step_optional, + instance: Some(Instance::new(settings, self.config.url.clone()).into()), + }; + + transaction.commit().await.map_err(|_| { + error!("Failed to commit transaction"); + Status::internal("unexpected error") + })?; + + Ok(Response::new(response)) + } else { + return Err(Status::permission_denied("invalid token")); + } } async fn activate_user( @@ -259,7 +269,10 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { // send success notification to admin let admin = enrollment.fetch_admin(&mut *transaction).await?; - Enrollment::send_admin_notification(&self.mail_tx, &admin, &user, ip_address, device_info)?; + + if let Some(admin) = admin { + Token::send_admin_notification(&self.mail_tx, &admin, &user, ip_address, device_info)?; + } transaction.commit().await.map_err(|_| { error!("Failed to commit transaction"); @@ -506,7 +519,7 @@ impl From for ProtoDevice { } } -impl Enrollment { +impl Token { // Send configured welcome email to user after finishing enrollment async fn send_welcome_email( &self, @@ -516,7 +529,7 @@ impl Enrollment { settings: &Settings, ip_address: String, device_info: Option, - ) -> Result<(), EnrollmentError> { + ) -> Result<(), TokenError> { debug!("Sending welcome mail to {}", user.username); let mail = Mail { to: user.email.clone(), @@ -534,7 +547,7 @@ impl Enrollment { } Err(err) => { error!("Error sending welcome mail: {err}"); - Err(EnrollmentError::NotificationError(err.to_string())) + Err(TokenError::NotificationError(err.to_string())) } } } @@ -546,7 +559,7 @@ impl Enrollment { user: &User, ip_address: String, device_info: Option, - ) -> Result<(), EnrollmentError> { + ) -> Result<(), TokenError> { debug!( "Sending enrollment success notification for user {} to {}", user.username, admin.username @@ -573,7 +586,7 @@ impl Enrollment { } Err(err) => { error!("Error sending welcome mail: {err}"); - Err(EnrollmentError::NotificationError(err.to_string())) + Err(TokenError::NotificationError(err.to_string())) } } } diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index b52a646ee..983e3f976 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -29,8 +29,15 @@ use self::{ worker::{worker_service_server::WorkerServiceServer, WorkerServer}, }; use crate::{ - auth::failed_login::FailedLoginMap, config::DefGuardConfig, db::AppEvent, - handlers::mail::send_gateway_disconnected_email, mail::Mail, SERVER_CONFIG, + auth::failed_login::FailedLoginMap, + config::DefGuardConfig, + db::AppEvent, + grpc::password_reset::{ + proto::password_reset_service_server::PasswordResetServiceServer, PasswordResetServer, + }, + handlers::mail::send_gateway_disconnected_email, + mail::Mail, + SERVER_CONFIG, }; #[cfg(feature = "worker")] use crate::{ @@ -44,6 +51,7 @@ pub mod enrollment; pub(crate) mod gateway; #[cfg(any(feature = "wireguard", feature = "worker"))] mod interceptor; +pub mod password_reset; #[cfg(feature = "worker")] pub mod worker; @@ -330,6 +338,12 @@ pub async fn run_grpc_server( pool.clone(), wireguard_tx.clone(), mail_tx.clone(), + user_agent_parser.clone(), + config.clone(), + )); + let password_reset_service = PasswordResetServiceServer::new(PasswordResetServer::new( + pool.clone(), + mail_tx.clone(), user_agent_parser, config.clone(), )); @@ -356,6 +370,7 @@ pub async fn run_grpc_server( let mut builder = builder.tcp_keepalive(Some(Duration::from_secs(10))); let router = builder.add_service(auth_service); let router = router.add_service(enrollment_service); + let router = router.add_service(password_reset_service); #[cfg(feature = "wireguard")] let router = router.add_service(gateway_service); #[cfg(feature = "worker")] diff --git a/src/grpc/password_reset.rs b/src/grpc/password_reset.rs new file mode 100644 index 000000000..056ccbd53 --- /dev/null +++ b/src/grpc/password_reset.rs @@ -0,0 +1,241 @@ +use std::sync::Arc; + +use tokio::sync::mpsc::UnboundedSender; +use tonic::{Request, Response, Status}; +use uaparser::UserAgentParser; + +use crate::{ + config::DefGuardConfig, + db::{ + models::enrollment::{Token, PASSWORD_RESET_TOKEN_TYPE}, + DbPool, User, + }, + handlers::{mail::send_password_reset_email, user::check_password_strength}, + ldap::utils::ldap_change_password, + mail::Mail, +}; + +use self::proto::{ + password_reset_service_server, PasswordResetInitializeRequest, PasswordResetRequest, + PasswordResetStartRequest, PasswordResetStartResponse, +}; + +#[allow(non_snake_case)] +pub mod proto { + tonic::include_proto!("password_reset"); +} + +pub struct PasswordResetServer { + pool: DbPool, + mail_tx: UnboundedSender, + user_agent_parser: Arc, + config: DefGuardConfig, + ldap_feature_active: bool, +} + +impl PasswordResetServer { + #[must_use] + pub fn new( + pool: DbPool, + mail_tx: UnboundedSender, + user_agent_parser: Arc, + config: DefGuardConfig, + ) -> Self { + // FIXME: check if LDAP feature is enabled + let ldap_feature_active = true; + Self { + pool, + mail_tx, + user_agent_parser, + config, + ldap_feature_active, + } + } + + // check if token provided with request corresponds to a valid enrollment session + async fn validate_session( + &self, + request: &Request, + ) -> Result { + debug!("Validating enrollment session token: {request:?}"); + let token = if let Some(token) = request.metadata().get("authorization") { + token + .to_str() + .map_err(|_| Status::unauthenticated("Invalid token"))? + } else { + error!("Missing authorization header in request"); + return Err(Status::unauthenticated("Missing authorization header")); + }; + + let enrollment = Token::find_by_id(&self.pool, token).await?; + + if enrollment.is_session_valid(self.config.enrollment_session_timeout.as_secs()) { + Ok(enrollment) + } else { + error!("Enrollment session expired"); + Err(Status::unauthenticated("Enrollment session expired")) + } + } +} + +#[tonic::async_trait] +impl password_reset_service_server::PasswordResetService for PasswordResetServer { + async fn request_password_reset( + &self, + request: Request, + ) -> Result, Status> { + debug!("Starting password reset request"); + + let ip_address = request + .metadata() + .get("ip_address") + .and_then(|value| value.to_str().map(ToString::to_string).ok()) + .unwrap_or_default(); + + let user_agent = request + .metadata() + .get("user_agent") + .and_then(|value| value.to_str().map(ToString::to_string).ok()) + .unwrap_or_default(); + + let request = request.into_inner(); + let email = request.email; + + let user = User::find_by_email(&self.pool, email.to_string().as_str()) + .await + .map_err(|_| { + error!("Failed to fetch user by email"); + Status::internal("unexpected error") + })?; + + if user.is_none() { + // Do not return information whether user exists + return Ok(Response::new(())); + } + + let user = user.unwrap(); + + // Do not allow password change if user is not active + if !user.has_password() { + return Ok(Response::new(())); + } + + let mut transaction = self.pool.begin().await.map_err(|_| { + error!("Failed to begin transaction"); + Status::internal("unexpected error") + })?; + + Token::delete_unused_user_password_reset_tokens( + &mut transaction, + user.id.expect("Missing user ID"), + ) + .await?; + + let enrollment = Token::new( + user.id.expect("Missing user ID"), + None, + Some(email.clone()), + self.config.password_reset_token_timeout.as_secs(), + Some(PASSWORD_RESET_TOKEN_TYPE.to_string()), + ); + enrollment.save(&mut *transaction).await?; + + transaction.commit().await.map_err(|_| { + error!("Failed to commit transaction"); + Status::internal("unexpected error") + })?; + + send_password_reset_email( + &user, + &self.mail_tx, + self.config.enrollment_url.clone(), + enrollment.id.clone(), + Some(ip_address), + Some(user_agent), + )?; + + Ok(Response::new(())) + } + + async fn start_password_reset( + &self, + request: Request, + ) -> Result, Status> { + debug!("Starting password reset session: {request:?}"); + let request = request.into_inner(); + + let mut enrollment = Token::find_by_id(&self.pool, &request.token).await?; + + if enrollment.token_type != Some("PASSWORD_RESET".to_string()) { + return Err(Status::permission_denied("invalid token")); + } + + let user = enrollment.fetch_user(&self.pool).await?; + + if !user.has_password() { + return Err(Status::permission_denied("user inactive")); + } + + let mut transaction = self.pool.begin().await.map_err(|_| { + error!("Failed to begin transaction"); + Status::internal("unexpected error") + })?; + + let session_deadline = enrollment + .start_session( + &mut transaction, + self.config.password_reset_session_timeout.as_secs(), + ) + .await?; + + let response = PasswordResetStartResponse { + deadline_timestamp: session_deadline.timestamp(), + }; + + transaction.commit().await.map_err(|_| { + error!("Failed to commit transaction"); + Status::internal("unexpected error") + })?; + + Ok(Response::new(response)) + } + + async fn reset_password( + &self, + request: Request, + ) -> Result, Status> { + debug!("Starting password reset: {request:?}"); + let enrollment = self.validate_session(&request).await?; + + let request = request.into_inner(); + if let Err(err) = check_password_strength(&request.password) { + error!("Password not strong enough: {err}"); + return Err(Status::invalid_argument("password not strong enough")); + } + + let mut user = enrollment.fetch_user(&self.pool).await?; + + let mut transaction = self.pool.begin().await.map_err(|_| { + error!("Failed to begin transaction"); + Status::internal("unexpected error") + })?; + + // update user + user.set_password(&request.password); + user.save(&mut *transaction).await.map_err(|err| { + error!("Failed to update user {}: {err}", user.username); + Status::internal("unexpected error") + })?; + + if self.ldap_feature_active { + let _ = ldap_change_password(&self.pool, &user.username, &request.password).await; + }; + + transaction.commit().await.map_err(|_| { + error!("Failed to commit transaction"); + Status::internal("unexpected error") + })?; + + Ok(Response::new(())) + } +} diff --git a/src/handlers/mail.rs b/src/handlers/mail.rs index 573308094..237ec1b73 100644 --- a/src/handlers/mail.rs +++ b/src/handlers/mail.rs @@ -6,6 +6,7 @@ use axum::{ }; use chrono::{NaiveDateTime, Utc}; use lettre::message::header::ContentType; +use reqwest::Url; use serde_json::json; use tokio::{ fs::read_to_string, @@ -17,7 +18,7 @@ use crate::{ appstate::AppState, auth::{AdminRole, SessionInfo}, config::DefGuardConfig, - db::{MFAMethod, Session, User}, + db::{models::enrollment::TokenError, MFAMethod, Session, User}, error::WebError, mail::{Attachment, Mail}, support::dump_config, @@ -37,6 +38,8 @@ static EMAIL_MFA_CODE_EMAIL_SUBJECT: &str = "Your Multi-Factor Authentication Co static GATEWAY_DISCONNECTED: &str = "Defguard: Gateway disconnected"; +pub static EMAIL_PASSOWRD_RESET_START_SUBJECT: &str = "Defguard: Password reset"; + #[derive(Clone, Deserialize)] pub struct TestMail { pub to: String, @@ -412,3 +415,40 @@ pub fn send_email_mfa_code_email( } } } + +pub fn send_password_reset_email( + user: &User, + mail_tx: &UnboundedSender, + service_url: Url, + token: String, + ip_address: Option, + device_info: Option, +) -> Result<(), TokenError> { + debug!("Sending password reset email to {}", user.email); + + let mail = Mail { + to: user.email.clone(), + subject: EMAIL_PASSOWRD_RESET_START_SUBJECT.into(), + content: templates::email_password_reset_mail( + service_url.clone(), + &token.as_str(), + ip_address, + device_info, + )?, + attachments: Vec::new(), + result_tx: None, + }; + + let to = mail.to.clone(); + + match mail_tx.send(mail) { + Ok(()) => { + info!("Password reset email sent to {to}"); + Ok(()) + } + Err(err) => { + error!("Failed to send password reset email to {to} with error:\n{err}"); + Err(TokenError::NotificationError(err.to_string())) + } + } +} diff --git a/src/handlers/user.rs b/src/handlers/user.rs index 0f2217826..05c699c4b 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -13,12 +13,15 @@ use crate::{ appstate::AppState, auth::{AdminRole, SessionInfo}, db::{ + models::enrollment::{Token, PASSWORD_RESET_TOKEN_TYPE}, AppEvent, MFAMethod, OAuth2AuthorizedApp, Settings, User, UserDetails, UserInfo, Wallet, WebAuthn, WireguardNetwork, }, error::WebError, - handlers::mail::send_mfa_configured_email, + handlers::mail::{send_mfa_configured_email, EMAIL_PASSOWRD_RESET_START_SUBJECT}, ldap::utils::{ldap_add_user, ldap_change_password, ldap_delete_user, ldap_modify_user}, + mail::Mail, + templates, }; /// Verify the given username @@ -450,6 +453,89 @@ pub async fn change_password( } } +pub async fn reset_password( + _admin: AdminRole, + session: SessionInfo, + State(appstate): State, + Path(username): Path, +) -> ApiResult { + debug!( + "Admin {} changing password for user {username}", + session.user.username, + ); + + if session.user.username == username { + debug!("Cannot change own password with this endpoint."); + return Ok(ApiResponse { + json: json!({}), + status: StatusCode::BAD_REQUEST, + }); + } + + let user = User::find_by_username(&appstate.pool, &username).await?; + + if let Some(user) = user { + let mut transaction = appstate.pool.begin().await?; + + Token::delete_unused_user_password_reset_tokens( + &mut transaction, + user.id.expect("Missing user ID"), + ) + .await?; + + let enrollment = Token::new( + user.id.expect("Missing user ID"), + Some(session.user.id.expect("Missing admin ID")), + Some(user.email.clone()), + appstate.config.password_reset_token_timeout.as_secs(), + Some(PASSWORD_RESET_TOKEN_TYPE.to_string()), + ); + enrollment.save(&mut *transaction).await?; + + let mail = Mail { + to: user.email.clone(), + subject: EMAIL_PASSOWRD_RESET_START_SUBJECT.into(), + content: templates::email_password_reset_mail( + appstate.config.enrollment_url.clone(), + &enrollment.id.clone().as_str(), + None, + None, + )?, + attachments: Vec::new(), + result_tx: None, + }; + + let to = mail.to.clone(); + + match &appstate.mail_tx.send(mail) { + Ok(()) => { + info!("Password reset email sent to {to}"); + Ok(()) + } + Err(err) => { + error!("Failed to send password reset email to {to} with error:\n{err}"); + Err(WebError::Serialization(format!( + "Could not send password reset email to user {username}" + ))) + } + }?; + + transaction.commit().await?; + + info!( + "Admin {} changed password for user {username}", + session.user.username + ); + Ok(ApiResponse::default()) + } else { + debug!("User not found"); + Ok(ApiResponse { + json: json!({}), + status: StatusCode::NOT_FOUND, + }) + } +} + /// Similar to [`models::WalletInfo`] but without `use_for_mfa`. #[derive(Deserialize)] pub struct WalletInfoShort { diff --git a/src/lib.rs b/src/lib.rs index 8b2057e9a..a10fd4fa5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,10 @@ use axum::{ routing::{delete, get, patch, post, put}, serve, Extension, Router, }; -use handlers::settings::{get_settings_essentials, patch_settings, test_ldap_settings}; +use handlers::{ + settings::{get_settings_essentials, patch_settings, test_ldap_settings}, + user::reset_password, +}; use secrecy::ExposeSecret; use tokio::{ net::TcpListener, @@ -183,6 +186,7 @@ pub fn build_webapp( // FIXME: username `change_password` is invalid .route("/user/change_password", put(change_self_password)) .route("/user/:username/password", put(change_password)) + .route("/user/:username/reset_password", post(reset_password)) .route("/user/:username/challenge", get(wallet_challenge)) .route("/user/:username/wallet", put(set_wallet)) .route("/user/:username/wallet/:address", put(update_wallet)) diff --git a/src/templates.rs b/src/templates.rs index 837db0aa9..d2d0a5deb 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -27,6 +27,8 @@ static MAIL_NEW_DEVICE_OCID_LOGIN: &str = static MAIL_EMAIL_MFA_ACTIVATION: &str = include_str!("../templates/mail_email_mfa_activation.tera"); static MAIL_EMAIL_MFA_CODE: &str = include_str!("../templates/mail_email_mfa_code.tera"); +static MAIL_PASSWORD_RESET_START: &str = + include_str!("../templates/mail_password_reset_start.tera"); #[allow(dead_code)] static MAIL_DATE_FORMAT: &str = "%Y-%m-%dT%H:%M:00Z"; @@ -266,6 +268,33 @@ pub fn email_mfa_code_mail(code: u32, session: &Session) -> Result, + device_info: Option, +) -> Result { + let (mut tera, mut context) = get_base_tera(None, None, ip_address, device_info)?; + + context.insert("enrollment_url", &service_url.to_string()); + context.insert( + "defguard_url", + &SERVER_CONFIG.get().expect("Server config not found").url, + ); + context.insert("token", password_reset_token); + + service_url.set_path("/password-reset"); + service_url + .query_pairs_mut() + .append_pair("token", password_reset_token); + + context.insert("link_url", &service_url.to_string()); + + tera.add_raw_template("mail_passowrd_reset_start", MAIL_PASSWORD_RESET_START)?; + + Ok(tera.render("mail_passowrd_reset_start", &context)?) +} + #[cfg(test)] mod test { use crate::config::DefGuardConfig; diff --git a/templates/mail_password_reset_start.tera b/templates/mail_password_reset_start.tera new file mode 100644 index 000000000..f3a878f3c --- /dev/null +++ b/templates/mail_password_reset_start.tera @@ -0,0 +1,38 @@ +{# Requires context +enrollment_url -> URL of the enrollment service +link_url -> URL of the enrollment service with the token query param included +defguard_url -> URL of defguard core Web UI +token -> enrollment token +#} +{% extends "base.tera" %} +{% import "macros.tera" as macros %} +{% block mail_content %} +{% set client_docs_url="https://defguard.gitbook.io/defguard/features/desktop-client" %} +{% set client_docs_link=macros::link(content=client_docs_url, href=client_docs_url) %} +{% set release_url="https://github.com/DefGuard/client/releases/latest" %} +{% set release_link=macros::link(content=release_url, href=release_url) %} +{% set section_content = [ +macros::paragraph(content="Password reset"), +macros::paragraph(content= "If you wish to reset your password, please copy & paste the following URL in your browser: "), +macros::link(content=link_url, href=link_url), +macros::paragraph(content="Or click the button below:"), +] %} +{{ macros::text_section(content_array=section_content)}} +

Reset password

+{% endblock %} diff --git a/tests/enrollment.rs b/tests/enrollment.rs index f7767ec0b..5bb40fb66 100644 --- a/tests/enrollment.rs +++ b/tests/enrollment.rs @@ -1,7 +1,7 @@ mod common; use defguard::{ - db::{models::enrollment::Enrollment, DbPool}, + db::{models::enrollment::Token, DbPool}, handlers::{AddUserData, Auth}, }; use reqwest::StatusCode; @@ -36,7 +36,7 @@ async fn test_initialize_enrollment() { assert_eq!(response.status(), StatusCode::CREATED); // verify enrollment token was not created - let enrollments = Enrollment::fetch_all(&pool).await.unwrap(); + let enrollments = Token::fetch_all(&pool).await.unwrap(); assert_eq!(enrollments.len(), 0); // try to start enrollment @@ -64,7 +64,7 @@ async fn test_initialize_enrollment() { assert_eq!(response.status(), StatusCode::CREATED); // verify enrollment token was not created - let enrollments = Enrollment::fetch_all(&pool).await.unwrap(); + let enrollments = Token::fetch_all(&pool).await.unwrap(); assert_eq!(enrollments.len(), 0); // try to start enrollment @@ -77,10 +77,10 @@ async fn test_initialize_enrollment() { let response: StartEnrollmentResponse = response.json().await; // verify enrollment token was created - let enrollment = Enrollment::find_by_id(&pool, &response.enrollment_token) + let enrollment = Token::find_by_id(&pool, &response.enrollment_token) .await .unwrap(); assert_eq!(enrollment.user_id, 4); - assert_eq!(enrollment.admin_id, 1); + assert_eq!(enrollment.admin_id, Some(1)); assert_eq!(enrollment.used_at, None); } diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index b55752654..49ec386ef 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -484,6 +484,7 @@ const en: BaseTranslation = { messages: { editSuccess: 'User updated.', failedToFetchUserData: 'Could not get user information.', + passwordResetEmailSent: 'Password reset email has been sent.', }, userDetails: { header: 'Profile Details', @@ -661,6 +662,7 @@ const en: BaseTranslation = { delete: 'Delete account', startEnrollment: 'Start enrollment', activateDesktop: 'Remote desktop activation', + resetPassword: 'Reset password', }, }, }, diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index 2e6a9ba32..ed43021f8 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -1059,6 +1059,10 @@ type RootTranslation = { * C​o​u​l​d​ ​n​o​t​ ​g​e​t​ ​u​s​e​r​ ​i​n​f​o​r​m​a​t​i​o​n​. */ failedToFetchUserData: string + /** + * P​a​s​s​w​o​r​d​ ​r​e​s​e​t​ ​e​m​a​i​l​ ​h​a​s​ ​b​e​e​n​ ​s​e​n​t​. + */ + passwordResetEmailSent: string } userDetails: { /** @@ -1512,6 +1516,10 @@ type RootTranslation = { * R​e​m​o​t​e​ ​d​e​s​k​t​o​p​ ​a​c​t​i​v​a​t​i​o​n */ activateDesktop: string + /** + * R​e​s​e​t​ ​p​a​s​s​w​o​r​d + */ + resetPassword: string } } } @@ -4571,6 +4579,10 @@ export type TranslationFunctions = { * Could not get user information. */ failedToFetchUserData: () => LocalizedString + /** + * Password reset email has been sent. + */ + passwordResetEmailSent: () => LocalizedString } userDetails: { /** @@ -5024,6 +5036,10 @@ export type TranslationFunctions = { * Remote desktop activation */ activateDesktop: () => LocalizedString + /** + * Reset password + */ + resetPassword: () => LocalizedString } } } diff --git a/web/src/pages/users/UsersOverview/components/UserEditButton/ResetPasswordButton.tsx b/web/src/pages/users/UsersOverview/components/UserEditButton/ResetPasswordButton.tsx new file mode 100644 index 000000000..67330458c --- /dev/null +++ b/web/src/pages/users/UsersOverview/components/UserEditButton/ResetPasswordButton.tsx @@ -0,0 +1,42 @@ +import { useMutation } from '@tanstack/react-query'; + +import { useI18nContext } from '../../../../../i18n/i18n-react'; +import { EditButtonOption } from '../../../../../shared/defguard-ui/components/Layout/EditButton/EditButtonOption'; +import useApi from '../../../../../shared/hooks/useApi'; +import { useToaster } from '../../../../../shared/hooks/useToaster'; +import { MutationKeys } from '../../../../../shared/mutations'; +import { User } from '../../../../../shared/types'; + +type Props = { + user: User; +}; + +export const ResetPasswordButton = ({ user }: Props) => { + const { LL } = useI18nContext(); + const toaster = useToaster(); + + const { + user: { resetPassword }, + } = useApi(); + + const changePasswordMutation = useMutation(resetPassword, { + mutationKey: [MutationKeys.RESET_PASSWORD], + onSuccess: () => { + toaster.success(LL.userPage.messages.passwordResetEmailSent()); + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onError: (_err) => { + toaster.error(LL.messages.error()); + }, + }); + + return ( + { + changePasswordMutation.mutate({ username: user.username }); + }} + /> + ); +}; diff --git a/web/src/pages/users/UsersOverview/components/UserEditButton/UserEditButton.tsx b/web/src/pages/users/UsersOverview/components/UserEditButton/UserEditButton.tsx index 43d7d506f..3525c9516 100644 --- a/web/src/pages/users/UsersOverview/components/UserEditButton/UserEditButton.tsx +++ b/web/src/pages/users/UsersOverview/components/UserEditButton/UserEditButton.tsx @@ -10,6 +10,7 @@ import { useModalStore } from '../../../../../shared/hooks/store/useModalStore'; import { useUserProfileStore } from '../../../../../shared/hooks/store/useUserProfileStore'; import { User } from '../../../../../shared/types'; import { useAddUserModal } from '../../modals/AddUserModal/hooks/useAddUserModal'; +import { ResetPasswordButton } from './ResetPasswordButton'; type Props = { user: User; @@ -34,6 +35,7 @@ export const UserEditButton = ({ user }: Props) => { onClick={() => setChangePasswordModal({ visible: true, user })} /> )} + { const changePassword = ({ username, ...rest }: ChangePasswordRequest) => client.put(`/user/${username}/password`, rest); + const resetPassword = ({ username }: ResetPasswordRequest) => + client.post(`/user/${username}/reset_password`); + const startEnrollment = ({ username, ...rest }: StartEnrollmentRequest) => client .post(`/user/${username}/start_enrollment`, rest) @@ -427,6 +431,7 @@ const useApi = (props?: HookProps): ApiHook => { deleteUser, usernameAvailable, changePassword, + resetPassword, walletChallenge, setWallet, deleteWallet, diff --git a/web/src/shared/mutations.ts b/web/src/shared/mutations.ts index 4eb32dbb8..1cb4a0d17 100644 --- a/web/src/shared/mutations.ts +++ b/web/src/shared/mutations.ts @@ -7,6 +7,7 @@ export const MutationKeys = { REGISTER_SECURITY_KEY_FINISH: 'REGISTER_SECURITY_KEY_FINISH', CREATE_WORKER_JOB: 'CREATE_WORKER_JOB', CHANGE_PASSWORD: 'CHANGE_PASSWORD', + RESET_PASSWORD: 'RESET_PASSWORD', SET_WALLET: 'SET_WALLET', DELETE_WALLET: 'DELETE_WALLET', WALLET_CHALLENGE: 'WALLET_CHALLENGE', diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index d526a87b1..ecacd44ea 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -191,6 +191,10 @@ export interface ChangePasswordRequest { username: string; } +export interface ResetPasswordRequest { + username: string; +} + export interface WalletChallengeRequest { name?: string; username: string; @@ -364,6 +368,7 @@ export interface ApiHook { deleteUser: (user: User) => EmptyApiResponse; usernameAvailable: (username: string) => EmptyApiResponse; changePassword: (data: ChangePasswordRequest) => EmptyApiResponse; + resetPassword: (data: ResetPasswordRequest) => EmptyApiResponse; walletChallenge: (data: WalletChallengeRequest) => Promise; setWallet: (data: AddWalletRequest) => EmptyApiResponse; deleteWallet: (data: WalletChallengeRequest) => EmptyApiResponse; From b7f770c52e6eea8074a6f90c6627d3106976b0d3 Mon Sep 17 00:00:00 2001 From: blazej-teonite <104985522+blazej-teonite@users.noreply.github.com> Date: Tue, 12 Dec 2023 21:39:51 +0100 Subject: [PATCH 03/26] fix: added missing polish password reset translations (#471) --- web/src/i18n/pl/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index 8c661ee30..75affc6dc 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -469,6 +469,7 @@ Uwaga, konfiguracje tutaj podane, nie posiadają twojego klucza prywatnego. Musi messages: { editSuccess: 'Użytkownik zaktualizowany.', failedToFetchUserData: 'Błąd pobierania informacji o użtkowniku.', + passwordResetEmailSent: 'Email z resetem hasła został wysłany.', }, userDetails: { header: 'Szczegóły profilu', @@ -646,6 +647,7 @@ Uwaga, konfiguracje tutaj podane, nie posiadają twojego klucza prywatnego. Musi provision: 'Stwórz klucze na YubiKey', delete: 'Usuń konto', startEnrollment: 'Rozpocznij rejestrację', + resetPassword: 'Resetuj hasło', }, }, }, From f6a7ffbdb1de04057a53b227f4a65d000928b59d Mon Sep 17 00:00:00 2001 From: blazej-teonite <104985522+blazej-teonite@users.noreply.github.com> Date: Tue, 12 Dec 2023 23:49:32 +0100 Subject: [PATCH 04/26] fix: enrollment e2e fixes (#472) * Fixed enrollment e2e tests --- e2e/tests/enrollment.spec.ts | 2 ++ e2e/utils/controllers/enrollment.ts | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/e2e/tests/enrollment.spec.ts b/e2e/tests/enrollment.spec.ts index 5d544491b..2e6725c7c 100644 --- a/e2e/tests/enrollment.spec.ts +++ b/e2e/tests/enrollment.spec.ts @@ -7,6 +7,7 @@ import { createDevice, createUserEnrollment, password, + selectEnrollment, setPassword, setToken, validateData, @@ -44,6 +45,7 @@ test.describe('Create user with enrollment enabled', () => { await waitForBase(page); await page.goto(testsConfig.ENROLLMENT_URL); await waitForPromise(2000); + await selectEnrollment(page); await setToken(token, page); // Welcome page await page.getByTestId('enrollment-next').click(); diff --git a/e2e/utils/controllers/enrollment.ts b/e2e/utils/controllers/enrollment.ts index 504a4b8f9..8a26c2632 100644 --- a/e2e/utils/controllers/enrollment.ts +++ b/e2e/utils/controllers/enrollment.ts @@ -55,10 +55,15 @@ export const createUserEnrollment = async ( return { user, token }; }; +export const selectEnrollment = async (page: Page) => { + const selectButton = page.getByTestId('select-enrollment'); + selectButton.click(); +}; + export const setToken = async (token: string, page: Page) => { const formElement = page.getByTestId('enrollment-token-form'); await formElement.getByTestId('field-token').type(token); - await formElement.locator('button[type="submit"]').click(); + await page.getByTestId('enrollment-token-submit-button').click(); }; export const validateData = async (user: User, page: Page) => { From 892c3253841212ff23e2334340589d88cf59dcb9 Mon Sep 17 00:00:00 2001 From: blazej-teonite <104985522+blazej-teonite@users.noreply.github.com> Date: Wed, 13 Dec 2023 13:12:29 +0100 Subject: [PATCH 05/26] Dfg 425 password reset e2e test (#474) * Added password reset e2e test --- e2e/tests/passwordReset.spec.ts | 49 ++++++++++++++++++++++++++ e2e/utils/controllers/passwordReset.ts | 18 ++++++++++ e2e/utils/db/getPasswordResetToken.ts | 18 ++++++++++ 3 files changed, 85 insertions(+) create mode 100644 e2e/tests/passwordReset.spec.ts create mode 100644 e2e/utils/controllers/passwordReset.ts create mode 100644 e2e/utils/db/getPasswordResetToken.ts diff --git a/e2e/tests/passwordReset.spec.ts b/e2e/tests/passwordReset.spec.ts new file mode 100644 index 000000000..d84a6ec36 --- /dev/null +++ b/e2e/tests/passwordReset.spec.ts @@ -0,0 +1,49 @@ +import { test } from '@playwright/test'; + +import { testsConfig, testUserTemplate } from '../config'; +import { loginBasic } from '../utils/controllers/login'; +import { dockerDown, dockerRestart } from '../utils/docker'; +import { waitForBase } from '../utils/waitForBase'; +import { waitForPromise } from '../utils/waitForPromise'; +import { selectPasswordReset, setEmail, setPassword } from '../utils/controllers/passwordReset'; +import { getPasswordResetToken } from '../utils/db/getPasswordResetToken'; +import { createUser } from '../utils/controllers/createUser'; +import { logout } from '../utils/controllers/logout'; +import { User } from '../types'; + +const newPassword = '!7(8o3aN8RoF'; + +test.describe('Reset password', () => { + const user: User = { ...testUserTemplate, username: 'test' }; + + test.beforeEach(async ({ browser, page }) => { + dockerRestart(); + await createUser(browser, user); + }); + + test.afterAll(() => { + dockerDown(); + }); + + test('Reset user password', async ({ page }) => { + await waitForBase(page); + await page.goto(testsConfig.ENROLLMENT_URL); + await waitForPromise(2000); + await selectPasswordReset(page); + await setEmail(user.mail, page); + + await page.getByTestId('email-sent-message').waitFor({ state: 'visible' }); + + const token = await getPasswordResetToken(user.mail); + + await page.goto(`${testsConfig.ENROLLMENT_URL}/password-reset/?token=${token}`); + await waitForPromise(2000); + + await setPassword(newPassword, page); + await page.getByTestId('password-reset-success').waitFor({ state: 'visible' }); + + await waitForBase(page); + await loginBasic(page, { ...user, password: newPassword }); + await logout(page); + }); +}); diff --git a/e2e/utils/controllers/passwordReset.ts b/e2e/utils/controllers/passwordReset.ts new file mode 100644 index 000000000..c8b0b7761 --- /dev/null +++ b/e2e/utils/controllers/passwordReset.ts @@ -0,0 +1,18 @@ +import { Page } from "playwright"; + + +export const selectPasswordReset = async (page: Page) => { + const selectButton = page.getByTestId('select-password-reset'); + selectButton.click(); +}; + +export const setEmail = async (token: string, page: Page) => { + await page.getByTestId('field-email').type(token); + await page.getByTestId('password-reset-email-submit-button').click(); +}; + +export const setPassword = async (password: string, page: Page) => { + await page.getByTestId('field-password').type(password); + await page.getByTestId('field-repeat').type(password); + await page.getByTestId('password-reset-submit').click(); +}; diff --git a/e2e/utils/db/getPasswordResetToken.ts b/e2e/utils/db/getPasswordResetToken.ts new file mode 100644 index 000000000..0594c421a --- /dev/null +++ b/e2e/utils/db/getPasswordResetToken.ts @@ -0,0 +1,18 @@ +import { expect } from '@playwright/test'; + +import { makeConnection } from './makeConnection'; + +export const getPasswordResetToken = async (email: string): Promise => { + const client = await makeConnection(); + const sql = `select id from "token" where email='${email}';`; + try { + const result = await client.query(sql); + expect(result.rows.length).toBeGreaterThan(0); + const token = result.rows[0]['id']; + expect(token).toBeDefined(); + expect(token?.length).toBeGreaterThan(0); + return token; + } finally { + await client.end(); + } +}; From 39cab7f995ce2f2e7222e077c1a3901eae4e5a67 Mon Sep 17 00:00:00 2001 From: blazej-teonite <104985522+blazej-teonite@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:15:35 +0100 Subject: [PATCH 06/26] Dfg 425 password reset success mail (#475) * Send password reset success mail --- src/grpc/password_reset.rs | 24 ++++++++++++++++- src/handlers/mail.rs | 31 ++++++++++++++++++++++ src/templates.rs | 13 +++++++++ templates/base.tera | 2 +- templates/mail_password_reset_start.tera | 1 + templates/mail_password_reset_success.tera | 15 +++++++++++ 6 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 templates/mail_password_reset_success.tera diff --git a/src/grpc/password_reset.rs b/src/grpc/password_reset.rs index 056ccbd53..759be5bf3 100644 --- a/src/grpc/password_reset.rs +++ b/src/grpc/password_reset.rs @@ -10,7 +10,10 @@ use crate::{ models::enrollment::{Token, PASSWORD_RESET_TOKEN_TYPE}, DbPool, User, }, - handlers::{mail::send_password_reset_email, user::check_password_strength}, + handlers::{ + mail::{send_password_reset_email, send_password_reset_success_email}, + user::check_password_strength, + }, ldap::utils::ldap_change_password, mail::Mail, }; @@ -207,6 +210,18 @@ impl password_reset_service_server::PasswordResetService for PasswordResetServer debug!("Starting password reset: {request:?}"); let enrollment = self.validate_session(&request).await?; + let ip_address = request + .metadata() + .get("ip_address") + .and_then(|value| value.to_str().map(ToString::to_string).ok()) + .unwrap_or_default(); + + let user_agent = request + .metadata() + .get("user_agent") + .and_then(|value| value.to_str().map(ToString::to_string).ok()) + .unwrap_or_default(); + let request = request.into_inner(); if let Err(err) = check_password_strength(&request.password) { error!("Password not strong enough: {err}"); @@ -236,6 +251,13 @@ impl password_reset_service_server::PasswordResetService for PasswordResetServer Status::internal("unexpected error") })?; + send_password_reset_success_email( + &user, + &self.mail_tx, + Some(ip_address), + Some(user_agent), + )?; + Ok(Response::new(())) } } diff --git a/src/handlers/mail.rs b/src/handlers/mail.rs index 237ec1b73..8a3f2b971 100644 --- a/src/handlers/mail.rs +++ b/src/handlers/mail.rs @@ -39,6 +39,7 @@ static EMAIL_MFA_CODE_EMAIL_SUBJECT: &str = "Your Multi-Factor Authentication Co static GATEWAY_DISCONNECTED: &str = "Defguard: Gateway disconnected"; pub static EMAIL_PASSOWRD_RESET_START_SUBJECT: &str = "Defguard: Password reset"; +pub static EMAIL_PASSOWRD_RESET_SUCCESS_SUBJECT: &str = "Defguard: Password reset success"; #[derive(Clone, Deserialize)] pub struct TestMail { @@ -452,3 +453,33 @@ pub fn send_password_reset_email( } } } + +pub fn send_password_reset_success_email( + user: &User, + mail_tx: &UnboundedSender, + ip_address: Option, + device_info: Option, +) -> Result<(), TokenError> { + debug!("Sending password reset success email to {}", user.email); + + let mail = Mail { + to: user.email.clone(), + subject: EMAIL_PASSOWRD_RESET_SUCCESS_SUBJECT.into(), + content: templates::email_password_reset_success_mail(ip_address, device_info)?, + attachments: Vec::new(), + result_tx: None, + }; + + let to = mail.to.clone(); + + match mail_tx.send(mail) { + Ok(()) => { + info!("Password reset email success sent to {to}"); + Ok(()) + } + Err(err) => { + error!("Failed to send password reset success email to {to} with error:\n{err}"); + Ok(()) + } + } +} diff --git a/src/templates.rs b/src/templates.rs index d2d0a5deb..bab83f880 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -29,6 +29,8 @@ static MAIL_EMAIL_MFA_ACTIVATION: &str = static MAIL_EMAIL_MFA_CODE: &str = include_str!("../templates/mail_email_mfa_code.tera"); static MAIL_PASSWORD_RESET_START: &str = include_str!("../templates/mail_password_reset_start.tera"); +static MAIL_PASSWORD_RESET_SUCCESS: &str = + include_str!("../templates/mail_password_reset_success.tera"); #[allow(dead_code)] static MAIL_DATE_FORMAT: &str = "%Y-%m-%dT%H:%M:00Z"; @@ -295,6 +297,17 @@ pub fn email_password_reset_mail( Ok(tera.render("mail_passowrd_reset_start", &context)?) } +pub fn email_password_reset_success_mail( + ip_address: Option, + device_info: Option, +) -> Result { + let (mut tera, context) = get_base_tera(None, None, ip_address, device_info)?; + + tera.add_raw_template("mail_passowrd_reset_success", MAIL_PASSWORD_RESET_SUCCESS)?; + + Ok(tera.render("mail_passowrd_reset_success", &context)?) +} + #[cfg(test)] mod test { use crate::config::DefGuardConfig; diff --git a/templates/base.tera b/templates/base.tera index 24db61235..b74d4fdd6 100644 --- a/templates/base.tera +++ b/templates/base.tera @@ -215,7 +215,7 @@ {% endblock %} -
+
diff --git a/templates/mail_password_reset_start.tera b/templates/mail_password_reset_start.tera index f3a878f3c..7bd2fd595 100644 --- a/templates/mail_password_reset_start.tera +++ b/templates/mail_password_reset_start.tera @@ -33,6 +33,7 @@ macros::paragraph(content="Or click the button below:"), text-align: center; display: inline-block; margin: 0px auto; + margin-bottom: 10px; cursor: pointer; ">Reset password

{% endblock %} diff --git a/templates/mail_password_reset_success.tera b/templates/mail_password_reset_success.tera new file mode 100644 index 000000000..4f087bd5d --- /dev/null +++ b/templates/mail_password_reset_success.tera @@ -0,0 +1,15 @@ +{# Requires context +enrollment_url -> URL of the enrollment service +link_url -> URL of the enrollment service with the token query param included +defguard_url -> URL of defguard core Web UI +token -> enrollment token +#} +{% extends "base.tera" %} +{% import "macros.tera" as macros %} +{% block mail_content %} +{% set section_content = [ +macros::paragraph(content="Password reset"), +macros::paragraph(content= "Your password has been successfully changed."), +] %} +{{ macros::text_section(content_array=section_content)}} +{% endblock %} From 94dd54d9927c049997c368bd3062c151026464b2 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 14 Dec 2023 13:00:48 +0100 Subject: [PATCH 07/26] feat: groups as roles (#467) * Introduce VpnRole * Introduce UserAdminRole --- ...bdc9289ef5ce5169c5b8c853e7c4e7dfb1877.json | 22 ++ ...ab52c14293fa64d260db812c89ed43a5a7cc8.json | 15 + ...c7421cb5324f70e427bd7e6f7312be4a20946.json | 15 - ...83d1694b8053c8522e2e6a311e1e1b4b4de15.json | 28 ++ ...118ceb4a68dad324366ed65813b05cc5ba63c.json | 15 - ...c4217fc4f3b662028eb0c8283b85d5a279eca.json | 15 + ...2677301edffa66042632d02c9b77dd11a1e83.json | 22 -- ...c2be9ba6f3cda184e0ee772cf900f8f110bd8.json | 15 + Cargo.lock | 178 +++++----- Cargo.toml | 9 +- model-derive/src/lib.rs | 8 +- sqlx-data.json | 3 - src/appstate.rs | 29 +- src/auth/failed_login.rs | 15 +- src/auth/mod.rs | 78 +++-- src/config.rs | 10 + src/db/models/device.rs | 47 +-- src/db/models/enrollment.rs | 27 +- src/db/models/group.rs | 82 +++-- src/db/models/mod.rs | 18 +- src/db/models/session.rs | 88 +++-- src/db/models/settings.rs | 16 +- src/db/models/user.rs | 157 +++++---- src/db/models/wallet.rs | 23 +- src/db/models/webauthn.rs | 12 +- src/db/models/wireguard.rs | 67 ++-- src/grpc/auth.rs | 8 +- src/grpc/enrollment.rs | 59 ++-- src/grpc/gateway.rs | 24 +- src/grpc/mod.rs | 11 +- src/grpc/password_reset.rs | 25 +- src/handlers/auth.rs | 306 ++++++++++-------- src/handlers/forward_auth.rs | 4 +- src/handlers/group.rs | 6 +- src/handlers/mail.rs | 33 +- src/handlers/mod.rs | 2 +- src/handlers/openid_flow.rs | 90 +++--- src/handlers/user.rs | 35 +- src/handlers/wireguard.rs | 49 ++- src/headers.rs | 2 +- src/lib.rs | 9 +- src/random.rs | 7 + src/templates.rs | 40 +-- src/wireguard_stats_purge.rs | 6 +- tests/auth.rs | 43 ++- 45 files changed, 984 insertions(+), 789 deletions(-) create mode 100644 .sqlx/query-0cf5e325412549a381f4a2309ccbdc9289ef5ce5169c5b8c853e7c4e7dfb1877.json create mode 100644 .sqlx/query-1bb3c8ecbd6500717d639678e08ab52c14293fa64d260db812c89ed43a5a7cc8.json delete mode 100644 .sqlx/query-4f318cd5100bf4d155a555b927cc7421cb5324f70e427bd7e6f7312be4a20946.json create mode 100644 .sqlx/query-8c69910bf01556b4e35a26a4bd583d1694b8053c8522e2e6a311e1e1b4b4de15.json delete mode 100644 .sqlx/query-9838df2efdce6632793cb64bf4a118ceb4a68dad324366ed65813b05cc5ba63c.json create mode 100644 .sqlx/query-a6074af430326dc14dd2b50a6d2c4217fc4f3b662028eb0c8283b85d5a279eca.json delete mode 100644 .sqlx/query-ad0934b3563f5ab5ca351d0db6a2677301edffa66042632d02c9b77dd11a1e83.json create mode 100644 .sqlx/query-f2c353b073b98ba636f9fbde139c2be9ba6f3cda184e0ee772cf900f8f110bd8.json delete mode 100644 sqlx-data.json diff --git a/.sqlx/query-0cf5e325412549a381f4a2309ccbdc9289ef5ce5169c5b8c853e7c4e7dfb1877.json b/.sqlx/query-0cf5e325412549a381f4a2309ccbdc9289ef5ce5169c5b8c853e7c4e7dfb1877.json new file mode 100644 index 000000000..f3444d949 --- /dev/null +++ b/.sqlx/query-0cf5e325412549a381f4a2309ccbdc9289ef5ce5169c5b8c853e7c4e7dfb1877.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT name FROM wireguard_network_allowed_group wag JOIN \"group\" g ON wag.group_id = g.id WHERE wag.network_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "0cf5e325412549a381f4a2309ccbdc9289ef5ce5169c5b8c853e7c4e7dfb1877" +} diff --git a/.sqlx/query-1bb3c8ecbd6500717d639678e08ab52c14293fa64d260db812c89ed43a5a7cc8.json b/.sqlx/query-1bb3c8ecbd6500717d639678e08ab52c14293fa64d260db812c89ed43a5a7cc8.json new file mode 100644 index 000000000..8d314a5fa --- /dev/null +++ b/.sqlx/query-1bb3c8ecbd6500717d639678e08ab52c14293fa64d260db812c89ed43a5a7cc8.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE session SET expires = $1 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamp", + "Text" + ] + }, + "nullable": [] + }, + "hash": "1bb3c8ecbd6500717d639678e08ab52c14293fa64d260db812c89ed43a5a7cc8" +} diff --git a/.sqlx/query-4f318cd5100bf4d155a555b927cc7421cb5324f70e427bd7e6f7312be4a20946.json b/.sqlx/query-4f318cd5100bf4d155a555b927cc7421cb5324f70e427bd7e6f7312be4a20946.json deleted file mode 100644 index c3f53658b..000000000 --- a/.sqlx/query-4f318cd5100bf4d155a555b927cc7421cb5324f70e427bd7e6f7312be4a20946.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n DELETE FROM wireguard_network_allowed_group\n WHERE network_id = $1 AND group_id IN (\n SELECT id\n FROM \"group\"\n WHERE name IN (SELECT * FROM UNNEST($2::text[]))\n )\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "TextArray" - ] - }, - "nullable": [] - }, - "hash": "4f318cd5100bf4d155a555b927cc7421cb5324f70e427bd7e6f7312be4a20946" -} diff --git a/.sqlx/query-8c69910bf01556b4e35a26a4bd583d1694b8053c8522e2e6a311e1e1b4b4de15.json b/.sqlx/query-8c69910bf01556b4e35a26a4bd583d1694b8053c8522e2e6a311e1e1b4b4de15.json new file mode 100644 index 000000000..4aef0c1ba --- /dev/null +++ b/.sqlx/query-8c69910bf01556b4e35a26a4bd583d1694b8053c8522e2e6a311e1e1b4b4de15.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id \"id?\", name FROM \"group\" JOIN group_user ON \"group\".id = group_user.group_id WHERE group_user.user_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id?", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "8c69910bf01556b4e35a26a4bd583d1694b8053c8522e2e6a311e1e1b4b4de15" +} diff --git a/.sqlx/query-9838df2efdce6632793cb64bf4a118ceb4a68dad324366ed65813b05cc5ba63c.json b/.sqlx/query-9838df2efdce6632793cb64bf4a118ceb4a68dad324366ed65813b05cc5ba63c.json deleted file mode 100644 index 919eec6c3..000000000 --- a/.sqlx/query-9838df2efdce6632793cb64bf4a118ceb4a68dad324366ed65813b05cc5ba63c.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO wireguard_network_allowed_group (network_id, group_id)\n SELECT $1, g.id\n FROM \"group\" g\n WHERE g.name = $2\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Text" - ] - }, - "nullable": [] - }, - "hash": "9838df2efdce6632793cb64bf4a118ceb4a68dad324366ed65813b05cc5ba63c" -} diff --git a/.sqlx/query-a6074af430326dc14dd2b50a6d2c4217fc4f3b662028eb0c8283b85d5a279eca.json b/.sqlx/query-a6074af430326dc14dd2b50a6d2c4217fc4f3b662028eb0c8283b85d5a279eca.json new file mode 100644 index 000000000..497c65cca --- /dev/null +++ b/.sqlx/query-a6074af430326dc14dd2b50a6d2c4217fc4f3b662028eb0c8283b85d5a279eca.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO wireguard_network_allowed_group (network_id, group_id) SELECT $1, g.id FROM \"group\" g WHERE g.name = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [] + }, + "hash": "a6074af430326dc14dd2b50a6d2c4217fc4f3b662028eb0c8283b85d5a279eca" +} diff --git a/.sqlx/query-ad0934b3563f5ab5ca351d0db6a2677301edffa66042632d02c9b77dd11a1e83.json b/.sqlx/query-ad0934b3563f5ab5ca351d0db6a2677301edffa66042632d02c9b77dd11a1e83.json deleted file mode 100644 index b5c9df1d1..000000000 --- a/.sqlx/query-ad0934b3563f5ab5ca351d0db6a2677301edffa66042632d02c9b77dd11a1e83.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT name\n FROM wireguard_network_allowed_group wag\n JOIN \"group\" g ON wag.group_id = g.id\n WHERE wag.network_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "name", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - }, - "hash": "ad0934b3563f5ab5ca351d0db6a2677301edffa66042632d02c9b77dd11a1e83" -} diff --git a/.sqlx/query-f2c353b073b98ba636f9fbde139c2be9ba6f3cda184e0ee772cf900f8f110bd8.json b/.sqlx/query-f2c353b073b98ba636f9fbde139c2be9ba6f3cda184e0ee772cf900f8f110bd8.json new file mode 100644 index 000000000..f0fca8cb4 --- /dev/null +++ b/.sqlx/query-f2c353b073b98ba636f9fbde139c2be9ba6f3cda184e0ee772cf900f8f110bd8.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM wireguard_network_allowed_group WHERE network_id = $1 AND group_id IN ( SELECT id FROM \"group\" WHERE name IN (SELECT * FROM UNNEST($2::text[])) )", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "TextArray" + ] + }, + "nullable": [] + }, + "hash": "f2c353b073b98ba636f9fbde139c2be9ba6f3cda184e0ee772cf900f8f110bd8" +} diff --git a/Cargo.lock b/Cargo.lock index de5d7e60b..e52b4edee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,9 +97,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" dependencies = [ "anstyle", "anstyle-parse", @@ -126,9 +126,9 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3a318f1f38d2418400f8209655bfd825785afd25aa30bb7ba6cc792e4596748" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ "windows-sys 0.52.0", ] @@ -225,7 +225,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -236,7 +236,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -288,7 +288,7 @@ dependencies = [ "bytes", "futures-util", "http 0.2.11", - "http-body 0.4.5", + "http-body 0.4.6", "hyper 0.14.27", "itoa", "matchit", @@ -358,7 +358,7 @@ dependencies = [ "bytes", "futures-util", "http 0.2.11", - "http-body 0.4.5", + "http-body 0.4.6", "mime", "rustversion", "tower-layer", @@ -394,6 +394,7 @@ dependencies = [ "axum 0.7.2", "axum-core 0.4.1", "bytes", + "cookie 0.18.0", "futures-util", "headers", "http 1.0.0", @@ -666,7 +667,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -762,11 +763,8 @@ checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8" dependencies = [ "aes-gcm", "base64 0.21.5", - "hkdf", - "hmac", "percent-encoding", "rand", - "sha2", "subtle", "time", "version_check", @@ -840,9 +838,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -851,22 +849,21 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.15" +version = "0.9.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset", - "scopeguard", ] [[package]] name = "crossbeam-queue" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +checksum = "b9bcf5bdbfdd6030fb4a1c497b5d5fc5921aa2f60d359a17e249c0e6df3de153" dependencies = [ "cfg-if", "crossbeam-utils", @@ -874,9 +871,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" dependencies = [ "cfg-if", ] @@ -944,7 +941,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -968,7 +965,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -979,7 +976,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -1036,12 +1033,12 @@ dependencies = [ "struct-patch", "tera", "thiserror", + "time", "tiny-keccak", "tokio", "tokio-stream", "tonic", "tonic-build", - "tower-cookies", "tower-http", "tracing", "tracing-subscriber", @@ -1103,9 +1100,9 @@ dependencies = [ [[package]] name = "deunicode" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a1abaf4d861455be59f64fd2b55606cb151fce304ede7165f410243ce96bde6" +checksum = "3ae2a35373c5c74340b79ae6780b498b2b183915ec5dacf263aac5a099bf485a" [[package]] name = "digest" @@ -1127,7 +1124,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -1532,7 +1529,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -1765,9 +1762,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hkdf" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ "hmac", ] @@ -1825,9 +1822,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http 0.2.11", @@ -1902,7 +1899,7 @@ dependencies = [ "futures-util", "h2 0.3.22", "http 0.2.11", - "http-body 0.4.5", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -2181,9 +2178,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" @@ -2304,9 +2301,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.150" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libm" @@ -2461,7 +2458,7 @@ name = "model_derive" version = "0.1.2" dependencies = [ "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -2612,7 +2609,7 @@ dependencies = [ "proc-macro-crate 2.0.1", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -2654,9 +2651,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -2744,7 +2741,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -2902,9 +2899,9 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pem" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3163d2912b7c3b52d651a055f2c7eec9ba5cd22d26ef75b8dd3a59980b185923" +checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" dependencies = [ "base64 0.21.5", "serde", @@ -2956,7 +2953,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -3035,7 +3032,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -3114,7 +3111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -3236,7 +3233,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.39", + "syn 2.0.41", "tempfile", "which", ] @@ -3251,7 +3248,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -3428,7 +3425,7 @@ dependencies = [ "futures-util", "h2 0.3.22", "http 0.2.11", - "http-body 0.4.5", + "http-body 0.4.6", "hyper 0.14.27", "hyper-rustls", "hyper-tls", @@ -3594,9 +3591,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.26" +version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ "bitflags 2.4.1", "errno", @@ -3607,9 +3604,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.9" +version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", "ring 0.17.7", @@ -3656,9 +3653,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "same-file" @@ -3827,7 +3824,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -3909,7 +3906,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -4346,7 +4343,7 @@ checksum = "f14a349c27ebe59faba22f933c9c734d428da7231e88a247e9d8c61eea964ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -4368,7 +4365,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -4390,9 +4387,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" dependencies = [ "proc-macro2", "quote", @@ -4496,7 +4493,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -4564,9 +4561,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.34.0" +version = "1.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" dependencies = [ "backtrace", "bytes", @@ -4598,7 +4595,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -4688,7 +4685,7 @@ dependencies = [ "flate2", "h2 0.3.22", "http 0.2.11", - "http-body 0.4.5", + "http-body 0.4.6", "hyper 0.14.27", "hyper-timeout", "percent-encoding", @@ -4716,7 +4713,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -4739,23 +4736,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tower-cookies" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fd0118512cf0b3768f7fcccf0bef1ae41d68f2b45edc1e77432b36c97c56c6d" -dependencies = [ - "async-trait", - "axum-core 0.4.1", - "cookie 0.18.0", - "futures-util", - "http 1.0.0", - "parking_lot", - "pin-project-lite", - "tower-layer", - "tower-service", -] - [[package]] name = "tower-http" version = "0.5.0" @@ -4813,7 +4793,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -4857,9 +4837,9 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" @@ -5129,7 +5109,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", "wasm-bindgen-shared", ] @@ -5163,7 +5143,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5464,9 +5444,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.25" +version = "0.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e87b8dfbe3baffbe687eef2e164e32286eff31a5ee16463ce03d991643ec94" +checksum = "6c830786f7720c2fd27a1a0e27a709dbd3c4d009b56d098fc742d4f4eab91fe2" dependencies = [ "memchr", ] @@ -5531,22 +5511,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.29" +version = "0.7.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d075cf85bbb114e933343e087b92f2146bac0d55b534cbb8188becf0039948e" +checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.29" +version = "0.7.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86cd5ca076997b97ef09d3ad65efe811fa68c9e874cb636ccb211223a813b0c2" +checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -5566,5 +5546,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] diff --git a/Cargo.toml b/Cargo.toml index 3be2b52cd..60ef1d613 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,11 @@ anyhow = "1.0" argon2 = { version = "0.5", features = ["std"] } axum = { version = "0.7" } axum-client-ip = "0.5" -axum-extra = { version = "0.9", features = ["typed-header"] } +axum-extra = { version = "0.9", features = [ + "cookie", + "cookie-private", + "typed-header", +] } base64 = "0.21" bincode = "1.3" chrono = { version = "0.4", default-features = false, features = [ @@ -61,6 +65,8 @@ sqlx = { version = "0.7", features = [ ] } tera = "1.19" thiserror = "1.0" +# match axum-extra -> cookies +time = { version = "0.3", default-features = false } tiny-keccak = { version = "2.0", features = ["keccak"] } tokio = { version = "1", features = [ "macros", @@ -72,7 +78,6 @@ tokio = { version = "1", features = [ ] } tokio-stream = "0.1" tonic = { version = "0.10", features = ["gzip", "tls", "tls-roots"] } -tower-cookies = { version = "0.10", features = ["private"] } tower-http = { version = "0.5", features = ["fs", "trace"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/model-derive/src/lib.rs b/model-derive/src/lib.rs index 238120dac..acff2d332 100644 --- a/model-derive/src/lib.rs +++ b/model-derive/src/lib.rs @@ -177,7 +177,7 @@ pub fn derive(input: TokenStream) -> TokenStream { impl #name { pub async fn find_by_id<'e, E>(executor: E, id: i64) -> Result, sqlx::Error> where - E: sqlx::Executor<'e, Database = sqlx::Postgres> + E: sqlx::PgExecutor<'e> { sqlx::query_as!(Self, #find_by_id_query, id).fetch_optional(executor).await } @@ -185,14 +185,14 @@ pub fn derive(input: TokenStream) -> TokenStream { // TODO: add limit and offset pub async fn all<'e, E>(executor: E) -> Result, sqlx::Error> where - E: sqlx::Executor<'e, Database = sqlx::Postgres> + E: sqlx::PgExecutor<'e> { sqlx::query_as!(Self, #all_query).fetch_all(executor).await } pub async fn delete<'e, E>(self, executor: E) -> Result<(), sqlx::Error> where - E: sqlx::Executor<'e, Database = sqlx::Postgres> + E: sqlx::PgExecutor<'e> { if let Some(id) = self.id { sqlx::query!(#delete_query, id).execute(executor).await?; @@ -202,7 +202,7 @@ pub fn derive(input: TokenStream) -> TokenStream { pub async fn save<'e, E>(&mut self, executor: E) -> Result<(), sqlx::Error> where - E: sqlx::Executor<'e, Database = sqlx::Postgres> + E: sqlx::PgExecutor<'e> { match self.id { None => { diff --git a/sqlx-data.json b/sqlx-data.json deleted file mode 100644 index 95c8c858b..000000000 --- a/sqlx-data.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "db": "PostgreSQL" -} \ No newline at end of file diff --git a/src/appstate.rs b/src/appstate.rs index e44b0785d..7a662b612 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -1,12 +1,10 @@ -use crate::{ - auth::failed_login::FailedLoginMap, - config::DefGuardConfig, - db::{AppEvent, DbPool, GatewayEvent, WebHook}, - mail::Mail, -}; +use std::sync::{Arc, Mutex}; + +use axum::extract::FromRef; +use axum_extra::extract::cookie::Key; use reqwest::Client; +use secrecy::ExposeSecret; use serde_json::json; -use std::sync::{Arc, Mutex}; use tokio::{ sync::{ broadcast::Sender, @@ -17,6 +15,13 @@ use tokio::{ use uaparser::UserAgentParser; use webauthn_rs::prelude::*; +use crate::{ + auth::failed_login::FailedLoginMap, + config::DefGuardConfig, + db::{AppEvent, DbPool, GatewayEvent, WebHook}, + mail::Mail, +}; + #[derive(Clone)] pub struct AppState { pub config: DefGuardConfig, @@ -27,6 +32,7 @@ pub struct AppState { pub webauthn: Arc, pub user_agent_parser: Arc, pub failed_logins: Arc>, + key: Key, } impl AppState { @@ -116,6 +122,8 @@ impl AppState { .expect("Invalid WebAuthn configuration"), ); + let key = Key::from(config.secret_key.expose_secret().as_bytes()); + Self { config, pool, @@ -125,6 +133,13 @@ impl AppState { webauthn, user_agent_parser, failed_logins, + key, } } } + +impl FromRef for Key { + fn from_ref(state: &AppState) -> Self { + state.key.clone() + } +} diff --git a/src/auth/failed_login.rs b/src/auth/failed_login.rs index b0036f395..ee11ed4ae 100644 --- a/src/auth/failed_login.rs +++ b/src/auth/failed_login.rs @@ -1,9 +1,9 @@ -use chrono::{DateTime, Duration, Local}; use std::{ collections::HashMap, - ops::DerefMut, sync::{Arc, Mutex}, }; + +use chrono::{DateTime, Duration, Local}; use thiserror::Error; // Time window in seconds @@ -67,14 +67,9 @@ impl FailedLogin { // Counter can be reset after enough time has passed since the initial attempt. // If user was blocked we also check if enough time (timeout) has passed since last attempt. fn should_reset_counter(&self) -> bool { - if self.time_since_first_attempt() > Duration::seconds(FAILED_LOGIN_WINDOW) + self.time_since_first_attempt() > Duration::seconds(FAILED_LOGIN_WINDOW) && self.attempt_count < FAILED_LOGIN_COUNT || self.time_since_last_attempt() > Duration::seconds(FAILED_LOGIN_TIMEOUT) - { - return true; - } - - false } } @@ -130,7 +125,7 @@ pub fn check_username( let mut failed_logins = failed_logins .lock() .expect("Failed to get a lock on failed login map."); - failed_logins.deref_mut().verify_username(username) + failed_logins.verify_username(username) } // Helper to log failed login attempt @@ -138,5 +133,5 @@ pub fn log_failed_login_attempt(failed_logins: &Arc>, user let mut failed_logins = failed_logins .lock() .expect("Failed to get a lock on failed login map."); - failed_logins.deref_mut().log_failed_attempt(username); + failed_logins.log_failed_attempt(username); } diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 7cb02a292..107a26799 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -10,16 +10,17 @@ use axum::{ extract::{FromRef, FromRequestParts}, http::request::Parts, }; +use axum_extra::extract::cookie::CookieJar; use jsonwebtoken::{ decode, encode, errors::Error as JWTError, DecodingKey, EncodingKey, Header, Validation, }; use serde::{Deserialize, Serialize}; -use tower_cookies::{Cookie, Cookies}; use crate::{ appstate::AppState, - db::{OAuth2AuthorizedApp, OAuth2Token, Session, SessionState, User}, + db::{Group, OAuth2AuthorizedApp, OAuth2Token, Session, SessionState, User}, error::WebError, + handlers::SESSION_COOKIE_NAME, }; pub static JWT_ISSUER: &str = "DefGuard"; @@ -123,14 +124,13 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let appstate = AppState::from_ref(state); - if let Ok(cookies) = Cookies::from_request_parts(parts, state).await { - if let Some(session_cookie) = cookies.get("defguard_session") { + if let Ok(cookies) = CookieJar::from_request_parts(parts, state).await { + if let Some(session_cookie) = cookies.get(SESSION_COOKIE_NAME) { return { match Session::find_by_id(&appstate.pool, session_cookie.value()).await { Ok(Some(session)) => { if session.expired() { let _result = session.delete(&appstate.pool).await; - cookies.remove(Cookie::from("defguard_session")); Err(WebError::Authorization("Session expired".into())) } else { Ok(session) @@ -152,6 +152,7 @@ pub struct SessionInfo { pub session: Session, pub user: User, pub is_admin: bool, + groups: Vec, } impl SessionInfo { @@ -161,8 +162,13 @@ impl SessionInfo { session, user, is_admin, + groups: Vec::new(), } } + + fn contains_group(&self, group_name: &str) -> bool { + self.groups.iter().any(|group| group.name == group_name) + } } #[async_trait] @@ -176,43 +182,63 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let appstate = AppState::from_ref(state); let session = Session::from_request_parts(parts, state).await?; - let user = User::find_by_id(&appstate.pool, session.user_id).await; + let user = User::find_by_id(&appstate.pool, session.user_id).await?; - if let Ok(Some(user)) = user { + if let Some(user) = user { if user.mfa_enabled && session.state != SessionState::MultiFactorVerified { return Err(WebError::Authorization("MFA not verified".into())); } - let is_admin = match user.member_of(&appstate.pool).await { - Ok(groups) => groups.contains(&appstate.config.admin_groupname), - _ => false, + let Ok(groups) = user.member_of(&appstate.pool).await else { + return Err(WebError::DbError("cannot fetch groups".into())); }; - Ok(SessionInfo::new(session, user, is_admin)) + Ok(SessionInfo { + session, + user, + is_admin: groups + .iter() + .any(|group| group.name == appstate.config.admin_groupname), + groups, + }) } else { Err(WebError::Authorization("User not found".into())) } } } -pub struct AdminRole; +#[macro_export] +macro_rules! role { + ($name:ident, $($config_field:ident)*) => { + pub struct $name; -#[async_trait] -impl FromRequestParts for AdminRole -where - S: Send + Sync, - AppState: FromRef, -{ - type Rejection = WebError; + #[async_trait] + impl FromRequestParts for $name + where + S: Send + Sync, + AppState: FromRef, + { + type Rejection = WebError; - async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - let session_info = SessionInfo::from_request_parts(parts, state).await?; - if session_info.is_admin { - Ok(AdminRole {}) - } else { - Err(WebError::Forbidden("access denied".into())) + async fn from_request_parts( + parts: &mut Parts, + state: &S, + ) -> Result { + let appstate = AppState::from_ref(state); + let session_info = SessionInfo::from_request_parts(parts, state).await?; + $( + if session_info.contains_group(&appstate.config.$config_field) { + return Ok(Self {}); + } + )* + Err(WebError::Forbidden("access denied".into())) + } } - } + }; } +role!(AdminRole, admin_groupname); +role!(UserAdminRole, admin_groupname useradmin_groupname); +role!(VpnRole, admin_groupname vpn_groupname); + // User authenticated by a valid access token pub struct AccessUserInfo(pub(crate) User); diff --git a/src/config.rs b/src/config.rs index 892b5bb8a..118564fde 100644 --- a/src/config.rs +++ b/src/config.rs @@ -58,6 +58,16 @@ pub struct DefGuardConfig { #[arg(long, env = "DEFGUARD_ADMIN_GROUPNAME", default_value = "admin")] pub admin_groupname: String, + #[arg( + long, + env = "DEFGUARD_USERADMIN_GROUPNAME", + default_value = "useradmin" + )] + pub useradmin_groupname: String, + + #[arg(long, env = "DEFGUARD_VPN_GROUPNAME", default_value = "vpn")] + pub vpn_groupname: String, + #[arg( long, env = "DEFGUARD_DEFAULT_ADMIN_PASSWORD", diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 0439a1164..98692d614 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -1,3 +1,15 @@ +use std::{ + fmt::{Display, Formatter}, + net::IpAddr, +}; + +use base64::{prelude::BASE64_STANDARD, Engine}; +use chrono::{NaiveDateTime, Utc}; +use ipnetwork::IpNetwork; +use model_derive::Model; +use sqlx::{query, query_as, Error as SqlxError, FromRow, PgConnection, PgExecutor}; +use thiserror::Error; + use super::{ error::ModelError, wireguard::{WireguardNetwork, WIREGUARD_MAX_HANDSHAKE_MINUTES}, @@ -16,17 +28,6 @@ pub struct DeviceConfig { pub(crate) dns: Option, } -use base64::{prelude::BASE64_STANDARD, Engine}; -use chrono::{NaiveDateTime, Utc}; -use ipnetwork::IpNetwork; -use model_derive::Model; -use sqlx::{query, query_as, Error as SqlxError, FromRow, PgConnection}; -use std::{ - fmt::{Display, Formatter}, - net::IpAddr, -}; -use thiserror::Error; - #[derive(Clone, Deserialize, Model, Serialize, Debug)] pub struct Device { pub id: Option, @@ -39,7 +40,7 @@ pub struct Device { impl Display for Device { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self.id { - Some(device_id) => write!(f, "[ID {}] {}", device_id, self.name), + Some(device_id) => write!(f, "[ID {device_id}] {}", self.name), None => write!(f, "{}", self.name), } } @@ -62,9 +63,9 @@ pub struct DeviceNetworkInfo { impl DeviceInfo { pub async fn from_device<'e, E>(executor: E, device: Device) -> Result where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { - debug!("Generating device info for {}", device); + debug!("Generating device info for {device}"); let device_id = device.get_id()?; let network_info = query_as!( DeviceNetworkInfo, @@ -193,7 +194,7 @@ impl WireguardNetworkDevice { pub async fn insert<'e, E>(&self, executor: E) -> Result<(), SqlxError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { query!( "INSERT INTO wireguard_network_device @@ -212,7 +213,7 @@ impl WireguardNetworkDevice { pub async fn update<'e, E>(&self, executor: E) -> Result<(), SqlxError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { query!( r#" @@ -231,7 +232,7 @@ impl WireguardNetworkDevice { pub async fn delete<'e, E>(&self, executor: E) -> Result<(), SqlxError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { query!( r#" @@ -252,7 +253,7 @@ impl WireguardNetworkDevice { network_id: i64, ) -> Result, SqlxError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { let res = query_as!( Self, @@ -290,7 +291,7 @@ impl WireguardNetworkDevice { network_id: i64, ) -> Result, SqlxError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { let res = query_as!( Self, @@ -387,7 +388,7 @@ impl Device { network_id: i64, ) -> Result, SqlxError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { query_as!( Self, @@ -405,7 +406,7 @@ impl Device { pub async fn find_by_pubkey<'e, E>(executor: E, pubkey: &str) -> Result, SqlxError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { query_as!( Self, @@ -490,7 +491,7 @@ impl Device { pub async fn add_to_all_networks( &self, transaction: &mut PgConnection, - admin_group_name: &String, + admin_group_name: &str, ) -> Result<(Vec, Vec), DeviceError> { info!("Adding device {} to all existing networks", self.name); let networks = WireguardNetwork::all(&mut *transaction).await?; @@ -561,7 +562,7 @@ impl Device { &self, transaction: &mut PgConnection, network: &WireguardNetwork, - reserved_ips: Option<&Vec>, + reserved_ips: Option<&[IpAddr]>, ) -> Result { let Some(network_id) = network.id else { return Err(ModelError::CannotCreate); diff --git a/src/db/models/enrollment.rs b/src/db/models/enrollment.rs index 2ea912aeb..e7f20161e 100644 --- a/src/db/models/enrollment.rs +++ b/src/db/models/enrollment.rs @@ -1,3 +1,11 @@ +use chrono::{Duration, NaiveDateTime, Utc}; +use reqwest::Url; +use sqlx::{query, query_as, Error as SqlxError, PgConnection, PgExecutor}; +use tera::{Context, Tera}; +use thiserror::Error; +use tokio::sync::mpsc::UnboundedSender; +use tonic::{Code, Status}; + use super::{settings::Settings, DbPool, User}; use crate::{ mail::Mail, @@ -5,19 +13,12 @@ use crate::{ templates::{self, TemplateError}, SERVER_CONFIG, VERSION, }; -use chrono::{Duration, NaiveDateTime, Utc}; -use reqwest::Url; -use sqlx::{query, query_as, Error as SqlxError, PgConnection}; -use tera::{Context, Tera}; -use thiserror::Error; -use tokio::sync::mpsc::UnboundedSender; -use tonic::{Code, Status}; pub static ENROLLMENT_TOKEN_TYPE: &str = "ENROLLMENT"; pub static PASSWORD_RESET_TOKEN_TYPE: &str = "PASSWORD_RESET"; -const ENROLLMENT_START_MAIL_SUBJECT: &str = "Defguard user enrollment"; -const DESKTOP_START_MAIL_SUBJECT: &str = "Defguard desktop client configuration"; +static ENROLLMENT_START_MAIL_SUBJECT: &str = "Defguard user enrollment"; +static DESKTOP_START_MAIL_SUBJECT: &str = "Defguard desktop client configuration"; #[derive(Error, Debug)] pub enum TokenError { @@ -200,7 +201,7 @@ impl Token { pub async fn fetch_user<'e, E>(&self, executor: E) -> Result where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { debug!("Fetching user for enrollment"); let Some(user) = User::find_by_id(executor, self.user_id).await? else { @@ -212,7 +213,7 @@ impl Token { pub async fn fetch_admin<'e, E>(&self, executor: E) -> Result, TokenError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { debug!("Fetching admin for enrollment"); if self.admin_id.is_none() { @@ -328,8 +329,8 @@ impl Token { pub async fn get_welcome_email_content( &self, transaction: &mut PgConnection, - ip_address: String, - device_info: Option, + ip_address: &str, + device_info: Option<&str>, ) -> Result { let settings = Settings::get_settings(&mut *transaction).await?; diff --git a/src/db/models/group.rs b/src/db/models/group.rs index bc42d00f3..b8a49e914 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -1,10 +1,7 @@ use model_derive::Model; -use sqlx::{query, query_as, query_scalar, Error as SqlxError, PgConnection}; +use sqlx::{query, query_as, query_scalar, Error as SqlxError, PgConnection, PgExecutor}; -use crate::{ - db::{models::error::ModelError, User, WireguardNetwork}, - DbPool, -}; +use crate::db::{models::error::ModelError, User, WireguardNetwork}; #[derive(Model)] pub struct Group { @@ -23,7 +20,7 @@ impl Group { pub async fn find_by_name<'e, E>(executor: E, name: &str) -> Result, SqlxError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { query_as!( Self, @@ -34,21 +31,27 @@ impl Group { .await } - pub async fn member_usernames(&self, pool: &DbPool) -> Result, SqlxError> { + pub async fn member_usernames<'e, E>(&self, executor: E) -> Result, SqlxError> + where + E: PgExecutor<'e>, + { if let Some(id) = self.id { query_scalar!( "SELECT \"user\".username FROM \"user\" JOIN group_user ON \"user\".id = group_user.user_id \ WHERE group_user.group_id = $1", id ) - .fetch_all(pool) + .fetch_all(executor) .await } else { Ok(Vec::new()) } } - pub async fn fetch_all_members(&self, pool: &DbPool) -> Result, SqlxError> { + pub async fn fetch_all_members<'e, E>(&self, executor: E) -> Result, SqlxError> + where + E: PgExecutor<'e>, + { if let Some(id) = self.id { query_as!( User, @@ -60,7 +63,7 @@ impl Group { WHERE group_user.group_id = $1", id ) - .fetch_all(pool) + .fetch_all(executor) .await } else { Ok(Vec::new()) @@ -72,16 +75,12 @@ impl WireguardNetwork { /// Fetch a list of all allowed groups for a given network from DB pub async fn fetch_allowed_groups<'e, E>(&self, executor: E) -> Result, ModelError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { - debug!("Fetching all allowed groups for network {}", self); + debug!("Fetching all allowed groups for network {self}"); let groups = query_scalar!( - r#" - SELECT name - FROM wireguard_network_allowed_group wag - JOIN "group" g ON wag.group_id = g.id - WHERE wag.network_id = $1 - "#, + "SELECT name FROM wireguard_network_allowed_group wag \ + JOIN \"group\" g ON wag.group_id = g.id WHERE wag.network_id = $1", self.id ) .fetch_all(executor) @@ -98,20 +97,20 @@ impl WireguardNetwork { pub async fn get_allowed_groups( &self, transaction: &mut PgConnection, - admin_group_name: &String, + admin_group_name: &str, ) -> Result>, ModelError> { - debug!("Returning a list of allowed groups for network {}", self); + debug!("Returning a list of allowed groups for network {self}"); // get allowed groups from DB let mut groups = self.fetch_allowed_groups(&mut *transaction).await?; - // if no allowed groups are set then all group are allowed + // if no allowed groups are set then all groups are allowed if groups.is_empty() { return Ok(None); } // make sure admin group is included - if !groups.contains(admin_group_name) { - groups.push(admin_group_name.clone()); + if !groups.iter().any(|name| name == admin_group_name) { + groups.push(admin_group_name.to_string()); } Ok(Some(groups)) @@ -123,7 +122,7 @@ impl WireguardNetwork { transaction: &mut PgConnection, allowed_groups: Vec, ) -> Result<(), ModelError> { - info!("Setting allowed groups for network {}", self); + info!("Setting allowed groups for network {self}"); if allowed_groups.is_empty() { return self.clear_allowed_groups(transaction).await; } @@ -152,14 +151,10 @@ impl WireguardNetwork { transaction: &mut PgConnection, group: &str, ) -> Result<(), ModelError> { - info!("Adding allowed group {} for network {}", group, self); + info!("Adding allowed group {group} for network {self}"); query!( - r#" - INSERT INTO wireguard_network_allowed_group (network_id, group_id) - SELECT $1, g.id - FROM "group" g - WHERE g.name = $2 - "#, + "INSERT INTO wireguard_network_allowed_group (network_id, group_id) \ + SELECT $1, g.id FROM \"group\" g WHERE g.name = $2", self.id, group ) @@ -173,32 +168,28 @@ impl WireguardNetwork { transaction: &mut PgConnection, groups: Vec, ) -> Result<(), ModelError> { - info!("Removing allowed groups {:?} for network {}", groups, self); + info!("Removing allowed groups {groups:?} for network {self}"); let result = query!( - r#" - DELETE FROM wireguard_network_allowed_group - WHERE network_id = $1 AND group_id IN ( - SELECT id - FROM "group" - WHERE name IN (SELECT * FROM UNNEST($2::text[])) - ) - "#, + "DELETE FROM wireguard_network_allowed_group \ + WHERE network_id = $1 AND group_id IN ( \ + SELECT id FROM \"group\" \ + WHERE name IN (SELECT * FROM UNNEST($2::text[])) \ + )", self.id, &groups ) .execute(transaction) .await?; info!( - "Removed {} allowed groups for network {}", + "Removed {} allowed groups for network {self}", result.rows_affected(), - self ); Ok(()) } /// Remove all allowed groups for a given network async fn clear_allowed_groups(&self, transaction: &mut PgConnection) -> Result<(), ModelError> { - info!("Removing all allowed groups for network {}", self); + info!("Removing all allowed groups for network {self}"); let result = query!( "DELETE FROM wireguard_network_allowed_group WHERE network_id=$1", self.id @@ -206,9 +197,8 @@ impl WireguardNetwork { .execute(transaction) .await?; info!( - "Removed {} allowed groups for network {}", + "Removed {} allowed groups for network {self}", result.rows_affected(), - self ); Ok(()) } @@ -217,7 +207,7 @@ impl WireguardNetwork { #[cfg(test)] mod test { use super::*; - use crate::db::User; + use crate::db::{DbPool, User}; #[sqlx::test] async fn test_group(pool: DbPool) { diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 22e5592a5..fc29365d1 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -19,12 +19,13 @@ pub mod webauthn; pub mod webhook; pub mod wireguard; +use sqlx::{query_as, Error as SqlxError, PgConnection}; + use self::{ device::UserDevice, user::{MFAMethod, User}, }; use super::{DbPool, Group}; -use sqlx::{query_as, Error as SqlxError, PgConnection}; #[cfg(feature = "openid")] #[derive(Deserialize, Serialize)] @@ -80,7 +81,7 @@ pub struct UserInfo { impl UserInfo { pub async fn from_user(pool: &DbPool, user: &User) -> Result { - let groups = user.member_of(pool).await?; + let groups = user.member_of_names(pool).await?; let authorized_apps = user.oauth2authorizedapps(pool).await?; Ok(Self { @@ -119,7 +120,10 @@ impl UserInfo { // add to groups if not already a member for groupname in &self.groups { - match present_groups.iter().position(|name| name == groupname) { + match present_groups + .iter() + .position(|group| &group.name == groupname) + { Some(index) => { present_groups.swap_remove(index); } @@ -133,11 +137,9 @@ impl UserInfo { } // remove from remaining groups - for groupname in present_groups { - if let Some(group) = Group::find_by_name(&mut *transaction, &groupname).await? { - user.remove_from_group(&mut *transaction, &group).await?; - groups_changed = true; - } + for group in present_groups { + user.remove_from_group(&mut *transaction, &group).await?; + groups_changed = true; } Ok(groups_changed) diff --git a/src/db/models/session.rs b/src/db/models/session.rs index 3fcb32063..d5db28a57 100644 --- a/src/db/models/session.rs +++ b/src/db/models/session.rs @@ -1,9 +1,10 @@ -use super::DbPool; -use crate::{auth::SESSION_TIMEOUT, random::gen_alphanumeric}; use chrono::{Duration, NaiveDateTime, Utc}; -use sqlx::{query, query_as, Error as SqlxError, Type}; +use sqlx::{query, query_as, Error as SqlxError, PgExecutor, Type}; use webauthn_rs::prelude::{PasskeyAuthentication, PasskeyRegistration}; +use super::DbPool; +use crate::{auth::SESSION_TIMEOUT, random::gen_alphanumeric}; + #[derive(Clone, PartialEq, Type)] #[repr(i16)] pub enum SessionState { @@ -110,66 +111,83 @@ impl Session { .and_then(|challenge| serde_cbor::from_slice(challenge).ok()) } - pub async fn set_passkey_authentication( + pub async fn set_passkey_authentication<'e, E>( &mut self, - pool: &DbPool, + executor: E, passkey_auth: &PasskeyAuthentication, - ) -> Result<(), SqlxError> { - let webauthn_challenge = serde_cbor::to_vec(passkey_auth).unwrap(); - query!( - "UPDATE session SET webauthn_challenge = $1 WHERE id = $2", - webauthn_challenge, - self.id - ) - .execute(pool) - .await?; - self.webauthn_challenge = Some(webauthn_challenge); + ) -> Result<(), SqlxError> + where + E: PgExecutor<'e>, + { + if let Ok(webauthn_challenge) = serde_cbor::to_vec(passkey_auth) { + query!( + "UPDATE session SET webauthn_challenge = $1 WHERE id = $2", + webauthn_challenge, + self.id + ) + .execute(executor) + .await?; + self.webauthn_challenge = Some(webauthn_challenge); + } Ok(()) } - pub async fn set_passkey_registration( + pub async fn set_passkey_registration<'e, E>( &mut self, - pool: &DbPool, + executor: E, passkey_reg: &PasskeyRegistration, - ) -> Result<(), SqlxError> { - let webauthn_challenge = serde_cbor::to_vec(passkey_reg).unwrap(); - query!( - "UPDATE session SET webauthn_challenge = $1 WHERE id = $2", - webauthn_challenge, - self.id - ) - .execute(pool) - .await?; - self.webauthn_challenge = Some(webauthn_challenge); + ) -> Result<(), SqlxError> + where + E: PgExecutor<'e>, + { + if let Ok(webauthn_challenge) = serde_cbor::to_vec(passkey_reg) { + query!( + "UPDATE session SET webauthn_challenge = $1 WHERE id = $2", + webauthn_challenge, + self.id + ) + .execute(executor) + .await?; + self.webauthn_challenge = Some(webauthn_challenge); + } Ok(()) } - pub async fn set_web3_challenge( + pub async fn set_web3_challenge<'e, E>( &mut self, - pool: &DbPool, + executor: E, web3_challenge: String, - ) -> Result<(), SqlxError> { + ) -> Result<(), SqlxError> + where + E: PgExecutor<'e>, + { query!( "UPDATE session SET web3_challenge = $1 WHERE id = $2", web3_challenge, self.id ) - .execute(pool) + .execute(executor) .await?; self.web3_challenge = Some(web3_challenge); Ok(()) } - pub async fn delete(self, pool: &DbPool) -> Result<(), SqlxError> { + pub async fn delete<'e, E>(self, executor: E) -> Result<(), SqlxError> + where + E: PgExecutor<'e>, + { query!("DELETE FROM session WHERE id = $1", self.id) - .execute(pool) + .execute(executor) .await?; Ok(()) } - pub async fn delete_expired(pool: &DbPool) -> Result<(), SqlxError> { + pub async fn delete_expired<'e, E>(executor: E) -> Result<(), SqlxError> + where + E: PgExecutor<'e>, + { query!("DELETE FROM session WHERE expires < now()",) - .execute(pool) + .execute(executor) .await?; Ok(()) } diff --git a/src/db/models/settings.rs b/src/db/models/settings.rs index cf2097b5f..b9eb7f489 100644 --- a/src/db/models/settings.rs +++ b/src/db/models/settings.rs @@ -1,10 +1,12 @@ -use super::DbPool; -use crate::secret::SecretString; -use model_derive::Model; -use sqlx::{query, Error as SqlxError, Type}; use std::collections::HashMap; + +use model_derive::Model; +use sqlx::{query, Error as SqlxError, PgExecutor, Type}; use struct_patch::Patch; +use super::DbPool; +use crate::secret::SecretString; + #[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Type, Debug)] #[sqlx(type_name = "smtp_encryption", rename_all = "lowercase")] pub enum SmtpEncryption { @@ -63,9 +65,9 @@ pub struct Settings { } impl Settings { - pub async fn get_settings<'e, E>(executor: E) -> Result + pub async fn get_settings<'e, E>(executor: E) -> Result where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { let settings = Settings::find_by_id(executor, 1).await?; @@ -75,7 +77,7 @@ impl Settings { // Set default values for settings if not set yet. // This is only relevant to a subset of settings which are nullable // and we want to initialize their values. - pub async fn init_defaults(pool: &DbPool) -> Result<(), sqlx::Error> { + pub async fn init_defaults(pool: &DbPool) -> Result<(), SqlxError> { info!("Initializing default settings"); let default_settings = HashMap::from([ diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 4677ba82c..40d294989 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -1,12 +1,5 @@ -use super::{ - device::{Device, UserDevice}, - group::Group, - wallet::Wallet, - webauthn::WebAuthn, - DbPool, MFAInfo, OAuth2AuthorizedAppInfo, SecurityKey, WalletInfo, -}; -use crate::auth::EMAIL_CODE_VALIDITY_PERIOD; -use crate::{auth::TOTP_CODE_VALIDITY_PERIOD, error::WebError, random::gen_alphanumeric}; +use std::{string::ToString, time::SystemTime}; + use argon2::{ password_hash::{ errors::Error as HashError, rand_core::OsRng, PasswordHash, PasswordHasher, @@ -17,9 +10,20 @@ use argon2::{ use axum::http::StatusCode; use model_derive::Model; use otpauth::TOTP; -use rand::{thread_rng, Rng}; -use sqlx::{query, query_as, query_scalar, Error as SqlxError, Type}; -use std::time::SystemTime; +use sqlx::{query, query_as, query_scalar, Error as SqlxError, PgExecutor, Type}; + +use super::{ + device::{Device, UserDevice}, + group::Group, + wallet::Wallet, + webauthn::WebAuthn, + DbPool, MFAInfo, OAuth2AuthorizedAppInfo, SecurityKey, WalletInfo, +}; +use crate::{ + auth::{EMAIL_CODE_VALIDITY_PERIOD, TOTP_CODE_VALIDITY_PERIOD}, + error::WebError, + random::{gen_alphanumeric, gen_totp_secret}, +}; const RECOVERY_CODES_COUNT: usize = 8; @@ -33,7 +37,7 @@ pub enum MFAMethod { Email, } -impl std::string::ToString for MFAMethod { +impl ToString for MFAMethod { fn to_string(&self) -> String { match self { MFAMethod::None => "None".into(), @@ -46,7 +50,7 @@ impl std::string::ToString for MFAMethod { } // User information ready to be sent as part of diagnostic data. -#[derive(Debug, Serialize)] +#[derive(Serialize)] pub struct UserDiagnostic { pub id: i64, pub mfa_enabled: bool, @@ -97,9 +101,8 @@ impl User { email: String, phone: Option, ) -> Self { - let password_hash = password.map(|password_hash| { - Self::hash_password(password_hash).expect("Failed to hash password") - }); + let password_hash = + password.and_then(|password_hash| Self::hash_password(password_hash).ok()); Self { id: None, username, @@ -122,7 +125,7 @@ impl User { } pub fn set_password(&mut self, password: &str) { - self.password_hash = Some(Self::hash_password(password).unwrap()); + self.password_hash = Self::hash_password(password).ok(); } pub fn verify_password(&self, password: &str) -> Result<(), HashError> { @@ -148,9 +151,9 @@ impl User { format!("{} {}", self.first_name, self.last_name) } - /// Generate new `secret`, save it, then return it as RFC 4648 base32-encoded string. + /// Generate new TOTP secret, save it, then return it as RFC 4648 base32-encoded string. pub async fn new_totp_secret(&mut self, pool: &DbPool) -> Result { - let secret = thread_rng().gen::<[u8; 20]>().to_vec(); + let secret = gen_totp_secret(); if let Some(id) = self.id { query!( "UPDATE \"user\" SET totp_secret = $1 WHERE id = $2", @@ -165,9 +168,9 @@ impl User { Ok(secret_base32) } - /// Generate new `secret` similar to TOTP secret above, but don't return generated value. + /// Generate new email secret, similar to TOTP secret above, but don't return generated value. pub async fn new_email_secret(&mut self, pool: &DbPool) -> Result<(), SqlxError> { - let email_secret = thread_rng().gen::<[u8; 20]>().to_vec(); + let email_secret = gen_totp_secret(); if let Some(id) = self.id { query!( "UPDATE \"user\" SET email_mfa_secret = $1 WHERE id = $2", @@ -181,14 +184,17 @@ impl User { Ok(()) } - pub async fn set_mfa_method( + pub async fn set_mfa_method<'e, E>( &mut self, - pool: &DbPool, + executor: E, mfa_method: MFAMethod, - ) -> Result<(), SqlxError> { + ) -> Result<(), SqlxError> + where + E: PgExecutor<'e>, + { info!( - "Setting MFA method for user {} to {:?}", - self.username, mfa_method + "Setting MFA method for user {} to {mfa_method:?}", + self.username ); if let Some(id) = self.id { query!( @@ -196,7 +202,7 @@ impl User { id, &mfa_method as &MFAMethod ) - .execute(pool) + .execute(executor) .await?; } self.mfa_method = mfa_method; @@ -208,7 +214,10 @@ impl User { /// - TOTP is enabled /// - a [`Wallet`] flagged `use_for_mfa` /// - a security key for Webauthn - async fn check_mfa_enabled(&self, pool: &DbPool) -> Result { + async fn check_mfa_enabled<'e, E>(&self, executor: E) -> Result + where + E: PgExecutor<'e>, + { // short-cut if self.totp_enabled || self.email_mfa_enabled { return Ok(true); @@ -223,7 +232,7 @@ impl User { WHERE \"user\".id = $1 GROUP BY totp_enabled, email_mfa_enabled;", id ) - .fetch_one(pool) + .fetch_one(executor) .await } else { Ok(false) @@ -274,12 +283,12 @@ impl User { } Some(methods) => { info!( - "Checking if {:?} in in available methods {:?}, {}", + "Checking if {:?} in in available methods {methods:?}, {}", info.mfa_method, - methods, methods.contains(&info.mfa_method) ); if !methods.contains(&info.mfa_method) { + // FIXME: do not panic self.set_mfa_method(pool, methods.into_iter().next().unwrap()) .await?; } @@ -300,10 +309,13 @@ impl User { /// Get recovery codes. If recovery codes exist, this function returns `None`. /// That way recovery codes are returned only once - when MFA is turned on. - pub async fn get_recovery_codes( + pub async fn get_recovery_codes<'e, E>( &mut self, - pool: &DbPool, - ) -> Result>, SqlxError> { + executor: E, + ) -> Result>, SqlxError> + where + E: PgExecutor<'e>, + { if !self.recovery_codes.is_empty() { return Ok(None); } @@ -318,7 +330,7 @@ impl User { id, &self.recovery_codes ) - .execute(pool) + .execute(executor) .await?; } @@ -348,11 +360,14 @@ impl User { } /// Enable TOTP - pub async fn enable_totp(&mut self, pool: &DbPool) -> Result<(), SqlxError> { + pub async fn enable_totp<'e, E>(&mut self, executor: E) -> Result<(), SqlxError> + where + E: PgExecutor<'e>, + { if !self.totp_enabled { if let Some(id) = self.id { query!("UPDATE \"user\" SET totp_enabled = TRUE WHERE id = $1", id) - .execute(pool) + .execute(executor) .await?; } self.totp_enabled = true; @@ -384,14 +399,17 @@ impl User { } /// Enable email MFA - pub async fn enable_email_mfa(&mut self, pool: &DbPool) -> Result<(), SqlxError> { + pub async fn enable_email_mfa<'e, E>(&mut self, executor: E) -> Result<(), SqlxError> + where + E: PgExecutor<'e>, + { if !self.email_mfa_enabled { if let Some(id) = self.id { query!( "UPDATE \"user\" SET email_mfa_enabled = TRUE WHERE id = $1", id ) - .execute(pool) + .execute(executor) .await?; } self.email_mfa_enabled = true; @@ -535,10 +553,13 @@ impl User { } } - pub async fn find_by_username( - pool: &DbPool, + pub async fn find_by_username<'e, E>( + executor: E, username: &str, - ) -> Result, SqlxError> { + ) -> Result, SqlxError> + where + E: PgExecutor<'e>, + { query_as!( Self, "SELECT id \"id?\", username, password_hash, last_name, first_name, email, \ @@ -547,11 +568,14 @@ impl User { FROM \"user\" WHERE username = $1", username ) - .fetch_optional(pool) + .fetch_optional(executor) .await } - pub async fn find_by_email(pool: &DbPool, email: &str) -> Result, SqlxError> { + pub async fn find_by_email<'e, E>(executor: E, email: &str) -> Result, SqlxError> + where + E: PgExecutor<'e>, + { query_as!( Self, "SELECT id \"id?\", username, password_hash, last_name, first_name, email, \ @@ -560,13 +584,13 @@ impl User { FROM \"user\" WHERE email = $1", email ) - .fetch_optional(pool) + .fetch_optional(executor) .await } - pub async fn member_of<'e, E>(&self, executor: E) -> Result, SqlxError> + pub async fn member_of_names<'e, E>(&self, executor: E) -> Result, SqlxError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { if let Some(id) = self.id { query_scalar!( @@ -581,6 +605,24 @@ impl User { } } + pub async fn member_of<'e, E>(&self, executor: E) -> Result, SqlxError> + where + E: PgExecutor<'e>, + { + if let Some(id) = self.id { + query_as!( + Group, + "SELECT id \"id?\", name FROM \"group\" JOIN group_user ON \"group\".id = group_user.group_id \ + WHERE group_user.user_id = $1", + id + ) + .fetch_all(executor) + .await + } else { + Ok(Vec::new()) + } + } + pub async fn devices(&self, pool: &DbPool) -> Result, SqlxError> { if let Some(id) = self.id { let devices = query_as!( @@ -606,7 +648,10 @@ impl User { } } - pub async fn wallets(&self, pool: &DbPool) -> Result, SqlxError> { + pub async fn wallets<'e, E>(&self, executor: E) -> Result, SqlxError> + where + E: PgExecutor<'e>, + { if let Some(id) = self.id { query_as!( WalletInfo, @@ -614,7 +659,7 @@ impl User { FROM wallet WHERE user_id = $1 AND validation_timestamp IS NOT NULL", id ) - .fetch_all(pool) + .fetch_all(executor) .await } else { Ok(Vec::new()) @@ -626,7 +671,7 @@ impl User { executor: E, ) -> Result, SqlxError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { if let Some(id) = self.id { query_as!( @@ -661,7 +706,7 @@ impl User { pub async fn add_to_group<'e, E>(&self, executor: E, group: &Group) -> Result<(), SqlxError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { if let (Some(id), Some(group_id)) = (self.id, group.id) { query!( @@ -682,7 +727,7 @@ impl User { group: &Group, ) -> Result<(), SqlxError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { if let (Some(id), Some(group_id)) = (self.id, group.id) { query!( @@ -696,14 +741,14 @@ impl User { Ok(()) } - // Remove authoirzed apps by their client id's from user + /// Remove authorized apps by their client id's from user pub async fn remove_oauth2_authorized_apps<'e, E>( &self, executor: E, - app_client_ids: &Vec, + app_client_ids: &[i64], ) -> Result<(), SqlxError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { if let Some(id) = self.id { query!( diff --git a/src/db/models/wallet.rs b/src/db/models/wallet.rs index e64912bf6..6983ef24b 100644 --- a/src/db/models/wallet.rs +++ b/src/db/models/wallet.rs @@ -1,5 +1,8 @@ -use super::DbPool; -use crate::hex::hex_decode; +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; + use chrono::{NaiveDateTime, Utc}; use ethers_core::types::transaction::eip712::{Eip712, TypedData}; use model_derive::Model; @@ -8,13 +11,12 @@ use secp256k1::{ ecdsa::{RecoverableSignature, RecoveryId}, Message, Secp256k1, }; -use sqlx::{query, query_as, Error as SqlxError}; -use std::{ - error::Error, - fmt::{Display, Formatter, Result as FmtResult}, -}; +use sqlx::{query, query_as, Error as SqlxError, PgExecutor}; use tiny_keccak::{Hasher, Keccak}; +use super::DbPool; +use crate::hex::hex_decode; + #[derive(Debug)] pub enum Web3Error { Decode, @@ -199,12 +201,15 @@ impl Wallet { .await } - pub async fn disable_mfa_for_user(pool: &DbPool, user_id: i64) -> Result<(), SqlxError> { + pub async fn disable_mfa_for_user<'e, E>(executor: E, user_id: i64) -> Result<(), SqlxError> + where + E: PgExecutor<'e>, + { query!( "UPDATE wallet SET use_for_mfa = FALSE WHERE user_id = $1", user_id ) - .execute(pool) + .execute(executor) .await?; Ok(()) } diff --git a/src/db/models/webauthn.rs b/src/db/models/webauthn.rs index 96da7d2b3..2a7bbfe62 100644 --- a/src/db/models/webauthn.rs +++ b/src/db/models/webauthn.rs @@ -1,8 +1,9 @@ -use super::{error::ModelError, DbPool}; use model_derive::Model; -use sqlx::{query, query_as, query_scalar, Error as SqlxError}; +use sqlx::{query, query_as, query_scalar, Error as SqlxError, PgExecutor}; use webauthn_rs::prelude::Passkey; +use super::{error::ModelError, DbPool}; + #[derive(Model)] pub struct WebAuthn { id: Option, @@ -55,9 +56,12 @@ impl WebAuthn { } /// Delete all for a given user. - pub async fn delete_all_for_user(pool: &DbPool, user_id: i64) -> Result<(), SqlxError> { + pub async fn delete_all_for_user<'e, E>(executor: E, user_id: i64) -> Result<(), SqlxError> + where + E: PgExecutor<'e>, + { query!("DELETE FROM webauthn WHERE user_id = $1", user_id) - .execute(pool) + .execute(executor) .await?; Ok(()) } diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 6d17dd9ff..084654583 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -1,3 +1,19 @@ +use std::{ + collections::HashMap, + fmt::{Debug, Display, Formatter}, + net::{IpAddr, Ipv4Addr}, + str::FromStr, +}; + +use base64::prelude::{Engine, BASE64_STANDARD}; +use chrono::{Duration, NaiveDateTime, Utc}; +use ipnetwork::{IpNetwork, IpNetworkError, NetworkSize}; +use model_derive::Model; +use rand_core::OsRng; +use sqlx::{query_as, query_scalar, Error as SqlxError, FromRow, PgConnection, PgExecutor}; +use thiserror::Error; +use x25519_dalek::{PublicKey, StaticSecret}; + use super::{ device::{Device, DeviceError, DeviceInfo, DeviceNetworkInfo, WireguardNetworkDevice}, error::ModelError, @@ -17,21 +33,6 @@ pub struct MappedDevice { pub wireguard_ip: IpAddr, } -use base64::prelude::{Engine, BASE64_STANDARD}; -use chrono::{Duration, NaiveDateTime, Utc}; -use ipnetwork::{IpNetwork, IpNetworkError, NetworkSize}; -use model_derive::Model; -use rand_core::OsRng; -use sqlx::{query_as, query_scalar, Error as SqlxError, FromRow, PgConnection}; -use std::{ - collections::HashMap, - fmt::{Debug, Display, Formatter}, - net::{IpAddr, Ipv4Addr}, - str::FromStr, -}; -use thiserror::Error; -use x25519_dalek::{PublicKey, StaticSecret}; - pub static WIREGUARD_MAX_HANDSHAKE_MINUTES: u32 = 5; pub static PEER_STATS_LIMIT: i64 = 6 * 60; @@ -149,7 +150,7 @@ impl WireguardNetwork { name: &str, ) -> Result>, WireguardNetworkError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { let networks = query_as!( WireguardNetwork, @@ -286,7 +287,7 @@ impl WireguardNetwork { async fn get_allowed_devices( &self, transaction: &mut PgConnection, - admin_group_name: &String, + admin_group_name: &str, ) -> Result, ModelError> { debug!("Fetching all allowed devices for network {}", self); let devices = match self @@ -324,7 +325,7 @@ impl WireguardNetwork { pub async fn add_all_allowed_devices( &self, transaction: &mut PgConnection, - admin_group_name: &String, + admin_group_name: &str, ) -> Result<(), ModelError> { info!( "Assigning IPs in network {} for all existing devices ", @@ -346,8 +347,8 @@ impl WireguardNetwork { &self, transaction: &mut PgConnection, device: &Device, - admin_group_name: &String, - reserved_ips: Option<&Vec>, + admin_group_name: &str, + reserved_ips: Option<&[IpAddr]>, ) -> Result { info!("Assigning IP in network {self} for {device}"); let allowed_devices = self @@ -375,13 +376,10 @@ impl WireguardNetwork { pub async fn sync_allowed_devices( &self, transaction: &mut PgConnection, - admin_group_name: &String, - reserved_ips: Option<&Vec>, + admin_group_name: &str, + reserved_ips: Option<&[IpAddr]>, ) -> Result, WireguardNetworkError> { - info!( - "Synchronizing IPs in network {} for all allowed devices ", - self - ); + info!("Synchronizing IPs in network {self} for all allowed devices "); // list all allowed devices let allowed_devices = self .get_allowed_devices(&mut *transaction, admin_group_name) @@ -471,7 +469,7 @@ impl WireguardNetwork { &self, transaction: &mut PgConnection, imported_devices: Vec, - admin_group_name: &String, + admin_group_name: &str, ) -> Result<(Vec, Vec), WireguardNetworkError> { let network_id = self.get_id()?; let allowed_devices = self @@ -536,7 +534,7 @@ impl WireguardNetwork { &self, transaction: &mut PgConnection, mapped_devices: Vec, - admin_group_name: &String, + admin_group_name: &str, ) -> Result, WireguardNetworkError> { info!("Mapping user devices for network {}", self); let network_id = self.get_id()?; @@ -561,7 +559,7 @@ impl WireguardNetwork { mapped_device.user_id, ); device.save(&mut *transaction).await?; - debug!("Saved new device {}", device); + debug!("Saved new device {device}"); // get a list of groups user is assigned to let groups = match user_groups.get(&device.user_id) { @@ -570,9 +568,9 @@ impl WireguardNetwork { // fetch user info None => match User::find_by_id(&mut *transaction, device.user_id).await? { Some(user) => { - let groups = user.member_of(&mut *transaction).await?; + let groups = user.member_of_names(&mut *transaction).await?; user_groups.insert(device.user_id, groups); - // ugly workaround to get around `groups` being dropped + // FIXME: ugly workaround to get around `groups` being dropped user_groups.get(&device.user_id).unwrap() } None => return Err(WireguardNetworkError::from(ModelError::NotFound)), @@ -738,14 +736,15 @@ impl WireguardNetwork { .await?; let mut result = Vec::new(); for device in devices { - let latest_stats = self.fetch_latest_stats(conn, device.id.unwrap()).await?; + let Some(device_id) = device.id else { continue }; + let latest_stats = self.fetch_latest_stats(conn, device_id).await?; result.push(WireguardDeviceStatsRow { - id: device.id.unwrap(), + id: device_id, user_id: device.user_id, name: device.name.clone(), wireguard_ip: latest_stats.as_ref().and_then(Self::parse_wireguard_ip), public_ip: latest_stats.as_ref().and_then(Self::parse_public_ip), - connected_at: self.connected_at(conn, device.id.unwrap()).await?, + connected_at: self.connected_at(conn, device_id).await?, // Filter stats for this device stats: stats .iter() diff --git a/src/grpc/auth.rs b/src/grpc/auth.rs index 950eb0ac7..977da2a59 100644 --- a/src/grpc/auth.rs +++ b/src/grpc/auth.rs @@ -40,14 +40,14 @@ impl auth_service_server::AuthService for AuthServer { request: Request, ) -> Result, Status> { let request = request.into_inner(); - debug!("Authenticating user {}", &request.username); + debug!("Authenticating user {}", request.username); // check if user can proceed with login check_username(&self.failed_logins, &request.username) .map_err(|_| Status::resource_exhausted("too many login requests"))?; if let Ok(Some(user)) = User::find_by_username(&self.pool, &request.username).await { if user.verify_password(&request.password).is_ok() { - info!("Authentication successful for user {}", &request.username); + info!("Authentication successful for user {}", request.username); Ok(Response::new(AuthenticateResponse { token: Self::create_jwt(&request.username).map_err(|_| { log_failed_login_attempt(&self.failed_logins, &request.username); @@ -55,12 +55,12 @@ impl auth_service_server::AuthService for AuthServer { })?, })) } else { - warn!("Invalid login credentials for user {}", &request.username); + warn!("Invalid login credentials for user {}", request.username); log_failed_login_attempt(&self.failed_logins, &request.username); Err(Status::unauthenticated("invalid credentials")) } } else { - warn!("User {} not found", &request.username); + warn!("User {} not found", request.username); log_failed_login_attempt(&self.failed_logins, &request.username); Err(Status::unauthenticated("invalid credentials")) } diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index c21309a83..5b055e2d5 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -21,17 +21,17 @@ use reqwest::Url; use sqlx::Transaction; use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender}; use tonic::{Request, Response, Status}; +use uaparser::UserAgentParser; -#[allow(non_snake_case)] pub mod proto { tonic::include_proto!("enrollment"); } -use proto::{ + +use self::proto::{ enrollment_service_server, ActivateUserRequest, AdminInfo, Device as ProtoDevice, DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse, EnrollmentStartRequest, EnrollmentStartResponse, ExistingDevice, InitialUserInfo, NewDevice, }; -use uaparser::UserAgentParser; pub struct EnrollmentServer { pool: DbPool, @@ -117,7 +117,7 @@ impl EnrollmentServer { /// Sends given `GatewayEvent` to be handled by gateway GRPC server pub fn send_wireguard_event(&self, event: GatewayEvent) { if let Err(err) = self.wireguard_tx.send(event) { - error!("Error sending wireguard event {err}"); + error!("Error sending WireGuard event {err}"); } } } @@ -170,7 +170,7 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { Status::internal("unexpected error") })?; - let admin_info = admin.and_then(|v| Some(AdminInfo::from(v))); + let admin_info = admin.map(AdminInfo::from); let response = EnrollmentStartResponse { admin: admin_info, @@ -262,8 +262,8 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { &self.mail_tx, &user, &settings, - ip_address.clone(), - device_info.clone(), + &ip_address, + device_info.as_deref(), ) .await?; @@ -271,7 +271,13 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { let admin = enrollment.fetch_admin(&mut *transaction).await?; if let Some(admin) = admin { - Token::send_admin_notification(&self.mail_tx, &admin, &user, ip_address, device_info)?; + Token::send_admin_notification( + &self.mail_tx, + &admin, + &user, + &ip_address, + device_info.as_deref(), + )?; } transaction.commit().await.map_err(|_| { @@ -367,10 +373,9 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { &template_locations, &user.email, &self.mail_tx, - Some(ip_address), - device_info, + Some(&ip_address), + device_info.as_deref(), ) - .await .map_err(|_| Status::internal("Failed to render new device added tempalte"))?; let response = DeviceConfigResponse { device: Some(device.into()), @@ -412,19 +417,19 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { Status::internal(format!("unexpected error: {err}")) })?; - let mut configs: Vec = vec![]; + let mut configs: Vec = Vec::new(); if let Some(device) = device { for network in networks { - let wireguard_network_device = WireguardNetworkDevice::find( - &self.pool, - device.id.unwrap(), - network.id.unwrap(), - ) - .await - .map_err(|err| { - error!("Invalid failed to get networks {err}"); - Status::internal(format!("unexpected error: {err}")) - })?; + let (Some(device_id), Some(network_id)) = (device.id, network.id) else { + continue; + }; + let wireguard_network_device = + WireguardNetworkDevice::find(&self.pool, device_id, network_id) + .await + .map_err(|err| { + error!("Invalid failed to get networks {err}"); + Status::internal(format!("unexpected error: {err}")) + })?; if let Some(wireguard_network_device) = wireguard_network_device { let allowed_ips = network .allowed_ips @@ -434,7 +439,7 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { .join(","); let config = ProtoDeviceConfig { config: device.create_config(&network, &wireguard_network_device), - network_id: network.id.unwrap(), + network_id, network_name: network.name, assigned_ip: wireguard_network_device.wireguard_ip.to_string(), endpoint: network.endpoint, @@ -527,8 +532,8 @@ impl Token { mail_tx: &UnboundedSender, user: &User, settings: &Settings, - ip_address: String, - device_info: Option, + ip_address: &str, + device_info: Option<&str>, ) -> Result<(), TokenError> { debug!("Sending welcome mail to {}", user.username); let mail = Mail { @@ -557,8 +562,8 @@ impl Token { mail_tx: &UnboundedSender, admin: &User, user: &User, - ip_address: String, - device_info: Option, + ip_address: &str, + device_info: Option<&str>, ) -> Result<(), TokenError> { debug!( "Sending enrollment success notification for user {} to {}", diff --git a/src/grpc/gateway.rs b/src/grpc/gateway.rs index 9927161a8..6c9370939 100644 --- a/src/grpc/gateway.rs +++ b/src/grpc/gateway.rs @@ -1,18 +1,11 @@ -use super::GatewayMap; -use crate::{ - db::{ - models::wireguard::{WireguardNetwork, WireguardPeerStats}, - DbPool, Device, GatewayEvent, - }, - mail::Mail, -}; -use chrono::{NaiveDateTime, Utc}; -use sqlx::{query_as, Error as SqlxError}; use std::{ pin::Pin, sync::{Arc, Mutex}, task::{Context, Poll}, }; + +use chrono::{NaiveDateTime, Utc}; +use sqlx::{query_as, Error as SqlxError, PgExecutor}; use tokio::{ sync::{ broadcast::{Receiver as BroadcastReceiver, Sender}, @@ -23,6 +16,15 @@ use tokio::{ use tokio_stream::Stream; use tonic::{metadata::MetadataMap, Code, Request, Response, Status}; +use super::GatewayMap; +use crate::{ + db::{ + models::wireguard::{WireguardNetwork, WireguardPeerStats}, + DbPool, Device, GatewayEvent, + }, + mail::Mail, +}; + tonic::include_proto!("gateway"); pub struct GatewayServer { @@ -36,7 +38,7 @@ impl WireguardNetwork { /// Get a list of all peers pub async fn get_peers<'e, E>(&self, executor: E) -> Result, SqlxError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { debug!("Fetching all peers for network {}", self.id.unwrap()); let result = query_as!( diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index 983e3f976..e40801bbc 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -302,16 +302,16 @@ impl GatewayState { // To return result instead of logging tokio::spawn(async move { if let Err(e) = - send_gateway_disconnected_email(name, network_name, hostname, &mail_tx, &pool) + send_gateway_disconnected_email(name, network_name, &hostname, &mail_tx, &pool) .await { - error!("Sending gateway disconnected notification failed: {}", e); + error!("Sending gateway disconnected notification failed: {e}"); } }); } else { debug!( - "Gateway {} disconnected not sending email. Last notification time was at {:?}", - hostname, self.last_email_notification + "Gateway {hostname} disconnected not sending email. Last notification time was at {:?}", + self.last_email_notification ); }; @@ -338,13 +338,12 @@ pub async fn run_grpc_server( pool.clone(), wireguard_tx.clone(), mail_tx.clone(), - user_agent_parser.clone(), + user_agent_parser, config.clone(), )); let password_reset_service = PasswordResetServiceServer::new(PasswordResetServer::new( pool.clone(), mail_tx.clone(), - user_agent_parser, config.clone(), )); #[cfg(feature = "worker")] diff --git a/src/grpc/password_reset.rs b/src/grpc/password_reset.rs index 759be5bf3..fcf2e9573 100644 --- a/src/grpc/password_reset.rs +++ b/src/grpc/password_reset.rs @@ -1,8 +1,5 @@ -use std::sync::Arc; - use tokio::sync::mpsc::UnboundedSender; use tonic::{Request, Response, Status}; -use uaparser::UserAgentParser; use crate::{ config::DefGuardConfig, @@ -23,7 +20,6 @@ use self::proto::{ PasswordResetStartRequest, PasswordResetStartResponse, }; -#[allow(non_snake_case)] pub mod proto { tonic::include_proto!("password_reset"); } @@ -31,25 +27,18 @@ pub mod proto { pub struct PasswordResetServer { pool: DbPool, mail_tx: UnboundedSender, - user_agent_parser: Arc, config: DefGuardConfig, ldap_feature_active: bool, } impl PasswordResetServer { #[must_use] - pub fn new( - pool: DbPool, - mail_tx: UnboundedSender, - user_agent_parser: Arc, - config: DefGuardConfig, - ) -> Self { + pub fn new(pool: DbPool, mail_tx: UnboundedSender, config: DefGuardConfig) -> Self { // FIXME: check if LDAP feature is enabled let ldap_feature_active = true; Self { pool, mail_tx, - user_agent_parser, config, ldap_feature_active, } @@ -141,7 +130,7 @@ impl password_reset_service_server::PasswordResetService for PasswordResetServer self.config.password_reset_token_timeout.as_secs(), Some(PASSWORD_RESET_TOKEN_TYPE.to_string()), ); - enrollment.save(&mut *transaction).await?; + enrollment.save(&mut transaction).await?; transaction.commit().await.map_err(|_| { error!("Failed to commit transaction"); @@ -152,9 +141,9 @@ impl password_reset_service_server::PasswordResetService for PasswordResetServer &user, &self.mail_tx, self.config.enrollment_url.clone(), - enrollment.id.clone(), - Some(ip_address), - Some(user_agent), + &enrollment.id, + Some(&ip_address), + Some(&user_agent), )?; Ok(Response::new(())) @@ -254,8 +243,8 @@ impl password_reset_service_server::PasswordResetService for PasswordResetServer send_password_reset_success_email( &user, &self.mail_tx, - Some(ip_address), - Some(user_agent), + Some(&ip_address), + Some(&user_agent), )?; Ok(Response::new(())) diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index 2bde95a19..fa01c4b99 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -3,14 +3,17 @@ use axum::{ http::StatusCode, }; use axum_client_ip::{InsecureClientIp, LeftmostXForwardedFor}; -use axum_extra::{headers::UserAgent, TypedHeader}; -use secrecy::ExposeSecret; +use axum_extra::{ + extract::{ + cookie::{Cookie, CookieJar, SameSite}, + PrivateCookieJar, + }, + headers::UserAgent, + TypedHeader, +}; use serde_json::json; use sqlx::types::Uuid; -use tower_cookies::{ - cookie::{time::Duration, SameSite}, - Cookie, Cookies, Key, -}; +use time::Duration; use webauthn_rs::prelude::PublicKeyCredential; use webauthn_rs_proto::options::CollectedClientData; @@ -39,13 +42,14 @@ use crate::{ /// * 200 with MFA disabled /// * 201 with MFA enabled when additional authentication factor is required pub async fn authenticate( - cookies: Cookies, + cookies: CookieJar, + private_cookies: PrivateCookieJar, user_agent: Option>, forwarded_for_ip: Option, InsecureClientIp(insecure_ip): InsecureClientIp, State(appstate): State, Json(data): Json, -) -> ApiResult { +) -> Result<(CookieJar, PrivateCookieJar, ApiResponse), WebError> { let lowercase_username = data.username.to_lowercase(); debug!("Authenticating user {lowercase_username}"); // check if user can proceed with login @@ -114,7 +118,7 @@ pub async fn authenticate( .secure(!server_config.cookie_insecure) .same_site(SameSite::Lax) .max_age(max_age); - cookies.add(auth_cookie.into()); + let cookies = cookies.add(auth_cookie); let login_event_type = "AUTHENTICATION".to_string(); @@ -131,17 +135,19 @@ pub async fn authenticate( agent, ) .await?; - Ok(ApiResponse { - json: json!(mfa_info), - status: StatusCode::CREATED, - }) + Ok(( + cookies, + private_cookies, + ApiResponse { + json: json!(mfa_info), + status: StatusCode::CREATED, + }, + )) } else { Err(WebError::DbError("MFA info read error".into())) } } else { let user_info = UserInfo::from_user(&appstate.pool, &user).await?; - let key = Key::from(server_config.secret_key.expose_secret().as_bytes()); - let private_cookies = cookies.private(&key); check_new_device_login( &appstate.pool, @@ -157,59 +163,66 @@ pub async fn authenticate( if let Some(openid_cookie) = private_cookies.get(SIGN_IN_COOKIE_NAME) { debug!("Found openid session cookie."); let redirect_url = openid_cookie.value().to_string(); - private_cookies.remove(openid_cookie); - Ok(ApiResponse { - json: json!(AuthResponse { - user: user_info, - url: Some(redirect_url) - }), - status: StatusCode::OK, - }) + Ok(( + cookies, + private_cookies.remove(openid_cookie), + ApiResponse { + json: json!(AuthResponse { + user: user_info, + url: Some(redirect_url) + }), + status: StatusCode::OK, + }, + )) } else { - debug!("No openid session found"); - Ok(ApiResponse { - json: json!(AuthResponse { - user: user_info, - url: None, - }), - status: StatusCode::OK, - }) + debug!("No OpenID session found"); + Ok(( + cookies, + private_cookies, + ApiResponse { + json: json!(AuthResponse { + user: user_info, + url: None, + }), + status: StatusCode::OK, + }, + )) } } } /// Logout - forget the session cookie. pub async fn logout( - cookies: Cookies, + cookies: CookieJar, session: Session, State(appstate): State, -) -> ApiResult { +) -> Result<(CookieJar, ApiResponse), WebError> { // remove auth cookie - cookies.remove(Cookie::from(SESSION_COOKIE_NAME)); + let cookies = cookies.remove(Cookie::from(SESSION_COOKIE_NAME)); // remove stored session session.delete(&appstate.pool).await?; - Ok(ApiResponse::default()) + Ok((cookies, ApiResponse::default())) } /// Enable MFA pub async fn mfa_enable( - cookies: Cookies, + cookies: CookieJar, session: Session, session_info: SessionInfo, State(appstate): State, -) -> ApiResult { +) -> Result<(CookieJar, ApiResponse), WebError> { let mut user = session_info.user; debug!("Enabling MFA for user {}", user.username); user.enable_mfa(&appstate.pool).await?; if user.mfa_enabled { info!("Enabled MFA for user {}", user.username); - cookies.remove(Cookie::from("defguard_sesssion")); + let cookies = cookies.remove(Cookie::from("defguard_sesssion")); session.delete(&appstate.pool).await?; debug!( "Removed auth session for user {} after enabling MFA", user.username ); - Ok(ApiResponse::default()) + Ok((cookies, ApiResponse::default())) } else { error!("Error enabling MFA for user {}", user.username); Err(WebError::Http(StatusCode::NOT_MODIFIED)) @@ -349,11 +362,11 @@ pub async fn webauthn_start(mut session: Session, State(appstate): State, Json(pubkey): Json, -) -> ApiResult { +) -> Result<(PrivateCookieJar, ApiResponse), WebError> { if let Some(passkey_auth) = session.get_passkey_authentication() { if let Ok(auth_result) = appstate .webauthn @@ -372,30 +385,34 @@ pub async fn webauthn_end( .await?; return if let Some(user) = User::find_by_id(&appstate.pool, session.user_id).await? { let user_info = UserInfo::from_user(&appstate.pool, &user).await?; - let key = Key::from(appstate.config.secret_key.expose_secret().as_bytes()); - let private_cookies = cookies.private(&key); if let Some(openid_cookie) = private_cookies.get(SIGN_IN_COOKIE_NAME) { debug!("Found OpenID session cookie."); let redirect_url = openid_cookie.value().to_string(); - private_cookies.remove(openid_cookie); - Ok(ApiResponse { - json: json!(AuthResponse { - user: user_info, - url: Some(redirect_url), - }), - status: StatusCode::OK, - }) + let private_cookies = private_cookies.remove(openid_cookie); + Ok(( + private_cookies, + ApiResponse { + json: json!(AuthResponse { + user: user_info, + url: Some(redirect_url), + }), + status: StatusCode::OK, + }, + )) } else { - Ok(ApiResponse { - json: json!(AuthResponse { - user: user_info, - url: None, - }), - status: StatusCode::OK, - }) + Ok(( + private_cookies, + ApiResponse { + json: json!(AuthResponse { + user: user_info, + url: None, + }), + status: StatusCode::OK, + }, + )) } } else { - Ok(ApiResponse::default()) + Ok((private_cookies, ApiResponse::default())) }; } } @@ -459,11 +476,11 @@ pub async fn totp_disable(session: SessionInfo, State(appstate): State /// Validate one-time passcode pub async fn totp_code( - cookies: Cookies, + private_cookies: PrivateCookieJar, mut session: Session, State(appstate): State, Json(data): Json, -) -> ApiResult { +) -> Result<(PrivateCookieJar, ApiResponse), WebError> { if let Some(user) = User::find_by_id(&appstate.pool, session.user_id).await? { let username = user.username.clone(); debug!("Verifying TOTP for user {}", username); @@ -473,27 +490,31 @@ pub async fn totp_code( .await?; let user_info = UserInfo::from_user(&appstate.pool, &user).await?; info!("Verified TOTP for user {username}"); - let key = Key::from(appstate.config.secret_key.expose_secret().as_bytes()); - let private_cookies = cookies.private(&key); if let Some(openid_cookie) = private_cookies.get(SIGN_IN_COOKIE_NAME) { debug!("Found openid session cookie."); let redirect_url = openid_cookie.value().to_string(); - private_cookies.remove(openid_cookie); - Ok(ApiResponse { - json: json!(AuthResponse { - user: user_info, - url: Some(redirect_url), - }), - status: StatusCode::OK, - }) + let private_cookies = private_cookies.remove(openid_cookie); + Ok(( + private_cookies, + ApiResponse { + json: json!(AuthResponse { + user: user_info, + url: Some(redirect_url), + }), + status: StatusCode::OK, + }, + )) } else { - Ok(ApiResponse { - json: json!(AuthResponse { - user: user_info, - url: None, - }), - status: StatusCode::OK, - }) + Ok(( + private_cookies, + ApiResponse { + json: json!(AuthResponse { + user: user_info, + url: None, + }), + status: StatusCode::OK, + }, + )) } } else { Err(WebError::Authorization("Invalid TOTP code".into())) @@ -590,11 +611,11 @@ pub async fn request_email_mfa_code( /// Validate email MFA code pub async fn email_mfa_code( - cookies: Cookies, + private_cookies: PrivateCookieJar, mut session: Session, State(appstate): State, Json(data): Json, -) -> ApiResult { +) -> Result<(PrivateCookieJar, ApiResponse), WebError> { if let Some(user) = User::find_by_id(&appstate.pool, session.user_id).await? { let username = user.username.clone(); debug!("Verifying email MFA code for user {}", username); @@ -604,27 +625,31 @@ pub async fn email_mfa_code( .await?; let user_info = UserInfo::from_user(&appstate.pool, &user).await?; info!("Verified email MFA code for user {username}"); - let key = Key::from(appstate.config.secret_key.expose_secret().as_bytes()); - let private_cookies = cookies.private(&key); if let Some(openid_cookie) = private_cookies.get(SIGN_IN_COOKIE_NAME) { debug!("Found openid session cookie."); let redirect_url = openid_cookie.value().to_string(); - private_cookies.remove(openid_cookie); - Ok(ApiResponse { - json: json!(AuthResponse { - user: user_info, - url: Some(redirect_url), - }), - status: StatusCode::OK, - }) + let private_cookies = private_cookies.remove(openid_cookie); + Ok(( + private_cookies, + ApiResponse { + json: json!(AuthResponse { + user: user_info, + url: Some(redirect_url), + }), + status: StatusCode::OK, + }, + )) } else { - Ok(ApiResponse { - json: json!(AuthResponse { - user: user_info, - url: None, - }), - status: StatusCode::OK, - }) + Ok(( + private_cookies, + ApiResponse { + json: json!(AuthResponse { + user: user_info, + url: None, + }), + status: StatusCode::OK, + }, + )) } } else { Err(WebError::Authorization("Invalid email MFA code".into())) @@ -659,11 +684,11 @@ pub async fn web3auth_start( /// Finish Web3 authentication pub async fn web3auth_end( - cookies: Cookies, + private_cookies: PrivateCookieJar, mut session: Session, State(appstate): State, Json(signature): Json, -) -> ApiResult { +) -> Result<(PrivateCookieJar, ApiResponse), WebError> { debug!( "Finishing web3 authentication for wallet {}", signature.address @@ -685,34 +710,37 @@ pub async fn web3auth_end( let username = user.username.clone(); let user_info = UserInfo::from_user(&appstate.pool, &user).await?; info!( - "User {} authenticated with wallet {}", - username, signature.address + "User {username} authenticated with wallet {}", + signature.address ); - let key = - Key::from(appstate.config.secret_key.expose_secret().as_bytes()); - let private_cookies = cookies.private(&key); if let Some(openid_cookie) = private_cookies.get(SIGN_IN_COOKIE_NAME) { debug!("Found openid session cookie."); let redirect_url = openid_cookie.value().to_string(); - private_cookies.remove(openid_cookie); - Ok(ApiResponse { - json: json!(AuthResponse { - user: user_info, - url: Some(redirect_url), - }), - status: StatusCode::OK, - }) + let private_cookies = private_cookies.remove(openid_cookie); + Ok(( + private_cookies, + ApiResponse { + json: json!(AuthResponse { + user: user_info, + url: Some(redirect_url), + }), + status: StatusCode::OK, + }, + )) } else { - Ok(ApiResponse { - json: json!(AuthResponse { - user: user_info, - url: None, - }), - status: StatusCode::OK, - }) + Ok(( + private_cookies, + ApiResponse { + json: json!(AuthResponse { + user: user_info, + url: None, + }), + status: StatusCode::OK, + }, + )) } } else { - Ok(ApiResponse::default()) + Ok((private_cookies, ApiResponse::default())) } } _ => Err(WebError::Authorization("Signature not verified".into())), @@ -725,11 +753,11 @@ pub async fn web3auth_end( /// Authenticate with a recovery code. pub async fn recovery_code( - cookies: Cookies, + private_cookies: PrivateCookieJar, mut session: Session, State(appstate): State, Json(recovery_code): Json, -) -> ApiResult { +) -> Result<(PrivateCookieJar, ApiResponse), WebError> { if let Some(mut user) = User::find_by_id(&appstate.pool, session.user_id).await? { let username = user.username.clone(); debug!("Authenticating user {} with recovery code", username); @@ -742,28 +770,32 @@ pub async fn recovery_code( .await?; let user_info = UserInfo::from_user(&appstate.pool, &user).await?; info!("Authenticated user {username} with recovery code"); - let key = Key::from(appstate.config.secret_key.expose_secret().as_bytes()); - let private_cookies = cookies.private(&key); if let Some(openid_cookie) = private_cookies.get(SIGN_IN_COOKIE_NAME) { debug!("Found OpenID session cookie."); let redirect_url = openid_cookie.value().to_string(); - private_cookies.remove(openid_cookie); - return Ok(ApiResponse { + let private_cookies = private_cookies.remove(openid_cookie); + return Ok(( + private_cookies, + ApiResponse { + json: json!(AuthResponse { + user: user_info, + url: Some(redirect_url), + }), + status: StatusCode::OK, + }, + )); + } + + return Ok(( + private_cookies, + ApiResponse { json: json!(AuthResponse { user: user_info, - url: Some(redirect_url), + url: None, }), status: StatusCode::OK, - }); - } - - return Ok(ApiResponse { - json: json!(AuthResponse { - user: user_info, - url: None, - }), - status: StatusCode::OK, - }); + }, + )); } } Err(WebError::Http(StatusCode::UNAUTHORIZED)) diff --git a/src/handlers/forward_auth.rs b/src/handlers/forward_auth.rs index 2f6c4a4a0..d5ceb74e4 100644 --- a/src/handlers/forward_auth.rs +++ b/src/handlers/forward_auth.rs @@ -4,8 +4,8 @@ use axum::{ http::{header::HeaderValue, request::Parts, StatusCode}, response::{IntoResponse, Redirect, Response}, }; +use axum_extra::extract::cookie::CookieJar; use reqwest::Url; -use tower_cookies::Cookies; use super::SESSION_COOKIE_NAME; use crate::{appstate::AppState, db::Session, error::WebError, SERVER_CONFIG}; @@ -61,7 +61,7 @@ impl FromRequestParts for ForwardAuthHeaders { pub async fn forward_auth( State(appstate): State, - cookies: Cookies, + cookies: CookieJar, headers: ForwardAuthHeaders, ) -> Result { // check if session cookie is present diff --git a/src/handlers/group.rs b/src/handlers/group.rs index 179d6d328..ec899c971 100644 --- a/src/handlers/group.rs +++ b/src/handlers/group.rs @@ -7,7 +7,7 @@ use serde_json::json; use super::{ApiResponse, ApiResult, Username}; use crate::{ appstate::AppState, - auth::{AdminRole, SessionInfo}, + auth::{SessionInfo, UserAdminRole}, db::{Group, User}, error::WebError, ldap::utils::{ldap_add_user_to_group, ldap_remove_user_from_group}, @@ -72,7 +72,7 @@ pub async fn get_group( } pub async fn add_group_member( - _admin: AdminRole, + _role: UserAdminRole, State(appstate): State, Path(name): Path, Json(data): Json, @@ -98,7 +98,7 @@ pub async fn add_group_member( } pub async fn remove_group_member( - _admin: AdminRole, + _role: UserAdminRole, State(appstate): State, Path((name, username)): Path<(String, String)>, ) -> ApiResult { diff --git a/src/handlers/mail.rs b/src/handlers/mail.rs index 8a3f2b971..61307237d 100644 --- a/src/handlers/mail.rs +++ b/src/handlers/mail.rs @@ -169,14 +169,14 @@ pub async fn send_support_data( } } -pub async fn send_new_device_added_email( +pub fn send_new_device_added_email( device_name: &str, public_key: &str, - template_locations: &Vec, + template_locations: &[TemplateLocation], user_email: &str, mail_tx: &UnboundedSender, - ip_address: Option, - device_info: Option, + ip_address: Option<&str>, + device_info: Option<&str>, ) -> Result<(), TemplateError> { debug!("User {user_email} new device added mail to {SUPPORT_EMAIL_ADDRESS}"); @@ -207,10 +207,11 @@ pub async fn send_new_device_added_email( } } } + pub async fn send_gateway_disconnected_email( gateway_name: Option, network_name: String, - gateway_adress: String, + gateway_adress: &str, mail_tx: &UnboundedSender, pool: &DbPool, ) -> Result<(), WebError> { @@ -227,7 +228,7 @@ pub async fn send_gateway_disconnected_email( subject: GATEWAY_DISCONNECTED.to_string(), content: templates::gateway_disconnected_mail( &gateway_name, - &gateway_adress, + gateway_adress, &network_name, )?, attachments: Vec::new(), @@ -237,12 +238,11 @@ pub async fn send_gateway_disconnected_email( match mail_tx.send(mail) { Ok(()) => { - info!("Sent gateway disconnected notification to {}", &to); + info!("Sent gateway disconnected notification to {to}"); } Err(err) => { error!( - "Sending gateway disconnected notification to {} failed with error:\n{}", - &to, &err + "Sending gateway disconnected notification to {to} failed with error:\n{err}" ); } } @@ -421,9 +421,9 @@ pub fn send_password_reset_email( user: &User, mail_tx: &UnboundedSender, service_url: Url, - token: String, - ip_address: Option, - device_info: Option, + token: &str, + ip_address: Option<&str>, + device_info: Option<&str>, ) -> Result<(), TokenError> { debug!("Sending password reset email to {}", user.email); @@ -432,7 +432,7 @@ pub fn send_password_reset_email( subject: EMAIL_PASSOWRD_RESET_START_SUBJECT.into(), content: templates::email_password_reset_mail( service_url.clone(), - &token.as_str(), + token, ip_address, device_info, )?, @@ -457,8 +457,8 @@ pub fn send_password_reset_email( pub fn send_password_reset_success_email( user: &User, mail_tx: &UnboundedSender, - ip_address: Option, - device_info: Option, + ip_address: Option<&str>, + device_info: Option<&str>, ) -> Result<(), TokenError> { debug!("Sending password reset success email to {}", user.email); @@ -475,11 +475,10 @@ pub fn send_password_reset_success_email( match mail_tx.send(mail) { Ok(()) => { info!("Password reset email success sent to {to}"); - Ok(()) } Err(err) => { error!("Failed to send password reset success email to {to} with error:\n{err}"); - Ok(()) } } + Ok(()) } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index a3ce53569..63328dbf2 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -34,7 +34,7 @@ pub mod wireguard; #[cfg(feature = "worker")] pub mod worker; -static SESSION_COOKIE_NAME: &str = "defguard_session"; +pub(crate) static SESSION_COOKIE_NAME: &str = "defguard_session"; static SIGN_IN_COOKIE_NAME: &str = "defguard_sign_in"; #[derive(Default)] diff --git a/src/handlers/openid_flow.rs b/src/handlers/openid_flow.rs index 0b247ff5e..741fe31c3 100644 --- a/src/handlers/openid_flow.rs +++ b/src/handlers/openid_flow.rs @@ -9,6 +9,7 @@ use axum::{ http::{header::LOCATION, request::Parts, HeaderMap, HeaderValue, StatusCode}, Form, }; +use axum_extra::extract::cookie::{Cookie, CookieJar, PrivateCookieJar, SameSite}; use base64::{prelude::BASE64_STANDARD, Engine}; use chrono::Utc; use openidconnect::{ @@ -26,18 +27,14 @@ use openidconnect::{ PrivateSigningKey, RefreshToken, ResponseTypes, Scope, StandardClaims, StandardErrorResponse, StandardTokenResponse, SubjectIdentifier, TokenUrl, UserInfoUrl, }; -use secrecy::ExposeSecret; use serde::{ de::{Deserialize, Deserializer, Error as DeError, Unexpected, Visitor}, ser::{Serialize, Serializer}, }; use serde_json::json; -use tower_cookies::{ - cookie::{time::Duration, SameSite}, - Cookie, Cookies, Key, -}; +use time::Duration; -use super::{ApiResponse, ApiResult}; +use super::{ApiResponse, ApiResult, SESSION_COOKIE_NAME}; use crate::{ appstate::AppState, auth::{AccessUserInfo, SessionInfo, SESSION_TIMEOUT}, @@ -224,7 +221,7 @@ impl AuthenticationRequest { if self .scope .split(' ') - .all(|scope| !oauth2client.scope.contains(&scope.to_owned())) + .all(|scope| !oauth2client.scope.iter().any(|s| s == scope)) { error!( "Invalid scope for client {}: {}", @@ -247,15 +244,14 @@ impl AuthenticationRequest { // check `redirect_uri` matches client config (ignoring trailing slashes) let parsed_redirect_uris: Vec = oauth2client .redirect_uri - .clone() - .into_iter() + .iter() .map(|uri| uri.trim_end_matches('/').into()) .collect(); if self .redirect_uri .split(' ') .map(|uri| uri.trim_end_matches('/')) - .all(|uri| !parsed_redirect_uris.contains(&uri.to_owned())) + .all(|uri| !parsed_redirect_uris.iter().any(|u| u == uri)) { error!( "Invalid redirect_uri for client {}: {} not in [{}]", @@ -313,22 +309,25 @@ async fn generate_auth_code_redirect( } /// Helper function to return redirection with status code 302. -fn redirect_to>(uri: T) -> (StatusCode, HeaderMap) { +fn redirect_to>( + uri: T, + private_cookies: PrivateCookieJar, +) -> (StatusCode, HeaderMap, PrivateCookieJar) { let mut headers = HeaderMap::new(); headers.insert( LOCATION, HeaderValue::try_from(uri.as_ref()).expect("URI isn't a valid header value"), ); - (StatusCode::FOUND, headers) + (StatusCode::FOUND, headers, private_cookies) } /// Helper function to redirect unauthorized user to login page /// and store information about OpenID authorize url in cookie to redirect later async fn login_redirect( data: &AuthenticationRequest, - cookies: Cookies, -) -> Result<(StatusCode, HeaderMap), WebError> { + private_cookies: PrivateCookieJar, +) -> Result<(StatusCode, HeaderMap, PrivateCookieJar), WebError> { let server_config = SERVER_CONFIG.get().ok_or(WebError::ServerConfigMissing)?; let base_url = server_config.url.join("api/v1/oauth/authorize").unwrap(); let cookie = Cookie::build(( @@ -349,10 +348,7 @@ async fn login_redirect( .same_site(SameSite::Lax) .http_only(true) .max_age(Duration::minutes(10)); - let key = Key::from(server_config.secret_key.expose_secret().as_bytes()); - let private_cookies = cookies.private(&key); - private_cookies.add(cookie.into()); - Ok(redirect_to("/login")) + Ok(redirect_to("/login", private_cookies.add(cookie))) } /// Authorization Endpoint @@ -360,8 +356,9 @@ async fn login_redirect( pub async fn authorization( State(appstate): State, Query(data): Query, - cookies: Cookies, -) -> Result<(StatusCode, HeaderMap), WebError> { + cookies: CookieJar, + private_cookies: PrivateCookieJar, +) -> Result<(StatusCode, HeaderMap, PrivateCookieJar), WebError> { let error; if let Some(oauth2client) = OAuth2Client::find_by_client_id(&appstate.pool, &data.client_id).await? @@ -374,17 +371,18 @@ pub async fn authorization( "Redirecting user to consent form - client id {}", data.client_id ); - return Ok(redirect_to(format!( - "/consent?{}", - serde_urlencoded::to_string(data).unwrap(), - ))); + // FIXME: do not panic + return Ok(redirect_to( + format!("/consent?{}", serde_urlencoded::to_string(data).unwrap(),), + private_cookies, + )); } Some(s) if s == "none" => { error!("'none' prompt in client id {} request", data.client_id); error = CoreAuthErrorResponseType::LoginRequired; } _ => { - return if let Some(session_cookie) = cookies.get("defguard_session") { + return if let Some(session_cookie) = cookies.get(SESSION_COOKIE_NAME) { if let Ok(Some(session)) = Session::find_by_id(&appstate.pool, session_cookie.value()).await { @@ -392,7 +390,7 @@ pub async fn authorization( if session.expired() { info!("Session {} for user id {} has expired, redirecting to login", session.id, session.user_id); let _result = session.delete(&appstate.pool).await; - login_redirect(&data, cookies).await + login_redirect(&data, private_cookies).await } else { // If session is present check if app is in user authorized apps. // If yes return auth code and state else redirect to consent form. @@ -408,28 +406,28 @@ pub async fn authorization( "OAuth client id {} authorized by user id {}, returning auth code", app.oauth2client_id, session.user_id ); - let key = Key::from( - appstate.config.secret_key.expose_secret().as_bytes(), - ); - let private_cookies = cookies.private(&key); - private_cookies.remove(Cookie::from(SIGN_IN_COOKIE_NAME)); + let private_cookies = private_cookies + .remove(Cookie::from(SIGN_IN_COOKIE_NAME)); let location = generate_auth_code_redirect( appstate, data, Some(session.user_id), ) .await?; - Ok(redirect_to(location)) + Ok(redirect_to(location, private_cookies)) } else { // If authorized app not found redirect to consent form info!( "OAuth client id {} not yet authorized by user id {}, redirecting to consent form", oauth2client.id.unwrap(), session.user_id ); - Ok(redirect_to(format!( - "/consent?{}", - serde_urlencoded::to_string(data).unwrap() - ))) + Ok(redirect_to( + format!( + "/consent?{}", + serde_urlencoded::to_string(data).unwrap() + ), + private_cookies, + )) } } } else { @@ -438,13 +436,13 @@ pub async fn authorization( "Session {} not found, redirecting to login page", session_cookie.value() ); - login_redirect(&data, cookies).await + login_redirect(&data, private_cookies).await } // If no session cookie provided redirect to login } else { info!("Session cookie not provided, redirecting to login page"); - login_redirect(&data, cookies).await + login_redirect(&data, private_cookies).await }; } } @@ -473,7 +471,7 @@ pub async fn authorization( }; }; - Ok(redirect_to(url)) + Ok(redirect_to(url, private_cookies)) } /// Login Authorization Endpoint redirect with authorization code @@ -481,8 +479,8 @@ pub async fn secure_authorization( session_info: SessionInfo, State(appstate): State, Query(data): Query, - cookies: Cookies, -) -> Result<(StatusCode, HeaderMap), WebError> { + private_cookies: PrivateCookieJar, +) -> Result<(StatusCode, HeaderMap, PrivateCookieJar), WebError> { let mut url = Url::parse(&data.redirect_uri).map_err(|_| WebError::Http(StatusCode::BAD_REQUEST))?; let error; @@ -518,18 +516,14 @@ pub async fn secure_authorization( "User {} allowed login with client {}", session_info.user.username, oauth2client.name ); - let key = Key::from(appstate.config.secret_key.expose_secret().as_bytes()); - let private_cookies = cookies.private(&key); - if let Some(cookie) = private_cookies.get(SIGN_IN_COOKIE_NAME) { - cookies.remove(cookie.clone()); - }; + let private_cookies = private_cookies.remove(SIGN_IN_COOKIE_NAME); let location = generate_auth_code_redirect(appstate, data, session_info.user.id).await?; info!( "Redirecting user {} to {location}", session_info.user.username ); - return Ok(redirect_to(location)); + return Ok(redirect_to(location, private_cookies)); } Err(err) => { info!( @@ -562,7 +556,7 @@ pub async fn secure_authorization( }; }; - Ok(redirect_to(url)) + Ok(redirect_to(url, private_cookies)) } /// https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest diff --git a/src/handlers/user.rs b/src/handlers/user.rs index 05c699c4b..1f6a9ae1a 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -5,20 +5,20 @@ use axum::{ use serde_json::json; use super::{ + mail::{send_mfa_configured_email, EMAIL_PASSOWRD_RESET_START_SUBJECT}, user_for_admin_or_self, AddUserData, ApiResponse, ApiResult, PasswordChange, PasswordChangeSelf, RecoveryCodes, StartEnrollmentRequest, Username, WalletChallenge, WalletChange, WalletSignature, }; use crate::{ appstate::AppState, - auth::{AdminRole, SessionInfo}, + auth::{SessionInfo, UserAdminRole}, db::{ models::enrollment::{Token, PASSWORD_RESET_TOKEN_TYPE}, AppEvent, MFAMethod, OAuth2AuthorizedApp, Settings, User, UserDetails, UserInfo, Wallet, WebAuthn, WireguardNetwork, }, error::WebError, - handlers::mail::{send_mfa_configured_email, EMAIL_PASSOWRD_RESET_START_SUBJECT}, ldap::utils::{ldap_add_user, ldap_change_password, ldap_delete_user, ldap_modify_user}, mail::Mail, templates, @@ -77,7 +77,7 @@ pub(crate) fn check_password_strength(password: &str) -> Result<(), WebError> { Ok(()) } -pub async fn list_users(_admin: AdminRole, State(appstate): State) -> ApiResult { +pub async fn list_users(_role: UserAdminRole, State(appstate): State) -> ApiResult { let all_users = User::all(&appstate.pool).await?; let mut users: Vec = Vec::with_capacity(all_users.len()); for user in all_users { @@ -103,7 +103,7 @@ pub async fn get_user( } pub async fn add_user( - _admin: AdminRole, + _role: UserAdminRole, session: SessionInfo, State(appstate): State, Json(user_data): Json, @@ -163,7 +163,7 @@ pub async fn add_user( // Trigger enrollment process manually pub async fn start_enrollment( - _admin: AdminRole, + _role: UserAdminRole, session: SessionInfo, State(appstate): State, Path(username): Path, @@ -181,12 +181,11 @@ pub async fn start_enrollment( )); } - let user = match User::find_by_username(&appstate.pool, &username).await? { - Some(user) => Ok(user), - None => Err(WebError::ObjectNotFound(format!( + let Some(user) = User::find_by_username(&appstate.pool, &username).await? else { + return Err(WebError::ObjectNotFound(format!( "user {username} not found" - ))), - }?; + ))); + }; let mut transaction = appstate.pool.begin().await?; @@ -194,7 +193,7 @@ pub async fn start_enrollment( .start_enrollment( &mut transaction, &session.user, - data.email.clone(), + data.email, appstate.config.enrollment_token_timeout.as_secs(), appstate.config.enrollment_url.clone(), data.send_enrollment_notification, @@ -252,12 +251,12 @@ pub async fn start_remote_desktop_configuration( } pub async fn username_available( - _admin: AdminRole, + _role: UserAdminRole, State(appstate): State, Json(data): Json, ) -> ApiResult { if let Err(err) = check_username(&data.username) { - debug!("{}", err); + debug!("{err}"); return Ok(ApiResponse { json: json!({}), status: StatusCode::BAD_REQUEST, @@ -338,7 +337,7 @@ pub async fn modify_user( } pub async fn delete_user( - _admin: AdminRole, + _role: UserAdminRole, State(appstate): State, Path(username): Path, session: SessionInfo, @@ -399,7 +398,7 @@ pub async fn change_self_password( } pub async fn change_password( - _admin: AdminRole, + _role: UserAdminRole, session: SessionInfo, State(appstate): State, Path(username): Path, @@ -454,7 +453,7 @@ pub async fn change_password( } pub async fn reset_password( - _admin: AdminRole, + _role: UserAdminRole, session: SessionInfo, State(appstate): State, Path(username): Path, @@ -490,14 +489,14 @@ pub async fn reset_password( appstate.config.password_reset_token_timeout.as_secs(), Some(PASSWORD_RESET_TOKEN_TYPE.to_string()), ); - enrollment.save(&mut *transaction).await?; + enrollment.save(&mut transaction).await?; let mail = Mail { to: user.email.clone(), subject: EMAIL_PASSOWRD_RESET_START_SUBJECT.into(), content: templates::email_password_reset_mail( appstate.config.enrollment_url.clone(), - &enrollment.id.clone().as_str(), + enrollment.id.clone().as_str(), None, None, )?, diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index 73e554535..9387dd38b 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -17,7 +17,7 @@ use uuid::Uuid; use super::{device_for_admin_or_self, user_for_admin_or_self, ApiResponse, ApiResult, WebError}; use crate::{ appstate::AppState, - auth::{AdminRole, Claims, ClaimsType, SessionInfo}, + auth::{Claims, ClaimsType, SessionInfo, VpnRole}, db::{ models::{ device::{ @@ -54,7 +54,7 @@ impl WireguardNetworkData { } } -// Used in process of importing network from wireguard config +// Used in process of importing network from WireGuard config #[derive(Serialize, Deserialize, Debug, Clone)] pub struct MappedDevices { pub devices: Vec, @@ -80,15 +80,15 @@ pub struct ImportedNetworkData { } pub async fn create_network( - _admin: AdminRole, + _role: VpnRole, State(appstate): State, session: SessionInfo, Json(data): Json, ) -> ApiResult { let network_name = data.name.clone(); debug!( - "User {} creating WireGuard network {}", - session.user.username, network_name + "User {} creating WireGuard network {network_name}", + session.user.username ); let allowed_ips = data.parse_allowed_ips(); let mut network = WireguardNetwork::new( @@ -119,7 +119,7 @@ pub async fn create_network( .send_wireguard_event(GatewayEvent::NetworkCreated(*network_id, network.clone())); } None => { - error!("Network {} ID was not created during network creation, gateway event was not send!", &network.name); + error!("Network {} ID was not created during network creation, gateway event was not send!", network.name); return Ok(ApiResponse { json: json!({}), status: StatusCode::INTERNAL_SERVER_ERROR, @@ -130,8 +130,8 @@ pub async fn create_network( transaction.commit().await?; info!( - "User {} created WireGuard network {}", - session.user.username, network_name + "User {} created WireGuard network {network_name}", + session.user.username ); Ok(ApiResponse { json: json!(network), @@ -146,7 +146,7 @@ async fn find_network(id: i64, pool: &DbPool) -> Result, State(appstate): State, session: SessionInfo, @@ -206,7 +206,7 @@ pub async fn modify_network( } pub async fn delete_network( - _admin: AdminRole, + _role: VpnRole, Path(network_id): Path, State(appstate): State, session: SessionInfo, @@ -227,7 +227,7 @@ pub async fn delete_network( } pub async fn list_networks( - _admin: AdminRole, + _role: VpnRole, State(appstate): State, Extension(gateway_state): Extension>>, ) -> ApiResult { @@ -260,7 +260,7 @@ pub async fn list_networks( pub async fn network_details( Path(network_id): Path, - _admin: AdminRole, + _role: VpnRole, State(appstate): State, Extension(gateway_state): Extension>>, ) -> ApiResult { @@ -295,7 +295,7 @@ pub async fn network_details( pub async fn gateway_status( Path(network_id): Path, - _admin: AdminRole, + _role: VpnRole, Extension(gateway_state): Extension>>, ) -> ApiResult { debug!("Displaying gateway status for network {network_id}"); @@ -311,7 +311,7 @@ pub async fn gateway_status( pub async fn remove_gateway( Path((network_id, gateway_id)): Path<(i64, String)>, - _admin: AdminRole, + _role: VpnRole, Extension(gateway_state): Extension>>, ) -> ApiResult { info!("Removing gateway {gateway_id} in network {network_id}"); @@ -332,7 +332,7 @@ pub async fn remove_gateway( } pub async fn import_network( - _admin: AdminRole, + _role: VpnRole, State(appstate): State, Json(data): Json, ) -> ApiResult { @@ -396,7 +396,7 @@ pub async fn import_network( // This is used exclusively for the wizard to map imported devices to users. pub async fn add_user_devices( - _admin: AdminRole, + _role: VpnRole, session: SessionInfo, State(appstate): State, Path(network_id): Path, @@ -519,7 +519,7 @@ pub async fn add_device( (None, None) } else { ( - Some(session.session.ip_address.clone()), + Some(session.session.ip_address.as_str()), session.session.device_info.clone(), ) }; @@ -530,9 +530,8 @@ pub async fn add_device( &user.email, &appstate.mail_tx, session_ip, - session_device_info, - ) - .await?; + session_device_info.as_deref(), + )?; info!( "User {} added device {device_name} for user {username}", @@ -636,7 +635,7 @@ pub async fn delete_device( Ok(ApiResponse::default()) } -pub async fn list_devices(_admin: AdminRole, State(appstate): State) -> ApiResult { +pub async fn list_devices(_role: VpnRole, State(appstate): State) -> ApiResult { debug!("Listing devices"); let devices = Device::all(&appstate.pool).await?; info!("Listed devices"); @@ -683,14 +682,14 @@ pub async fn download_config( String::new() }; Err(WebError::ObjectNotFound(format!( - "No ip found for device: {}({device_id})", + "No IP address found for device: {}({device_id})", device.name ))) } } pub async fn create_network_token( - _admin: AdminRole, + _role: VpnRole, State(appstate): State, Path(network_id): Path, ) -> ApiResult { @@ -744,7 +743,7 @@ impl QueryFrom { } pub async fn user_stats( - _admin: AdminRole, + _role: VpnRole, State(appstate): State, Path(network_id): Path, Query(query_from): Query, @@ -769,7 +768,7 @@ pub async fn user_stats( } pub async fn network_stats( - _admin: AdminRole, + _role: VpnRole, State(appstate): State, Path(network_id): Path, Query(query_from): Query, diff --git a/src/headers.rs b/src/headers.rs index 810c36fe2..eb154bab9 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -38,7 +38,7 @@ pub fn get_device_info( ) -> Option { let agent = parse_user_agent(user_agent_parser, user_agent); - agent.clone().map(|v| get_user_agent_device(&v)) + agent.map(|v| get_user_agent_device(&v)) } #[must_use] diff --git a/src/lib.rs b/src/lib.rs index a10fd4fa5..82fe3bef8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ use handlers::{ settings::{get_settings_essentials, patch_settings, test_ldap_settings}, user::reset_password, }; +use ipnetwork::IpNetwork; use secrecy::ExposeSecret; use tokio::{ net::TcpListener, @@ -25,7 +26,6 @@ use tokio::{ OnceCell, }, }; -use tower_cookies::CookieManagerLayer; use tower_http::{ services::{ServeDir, ServeFile}, trace::{DefaultOnResponse, TraceLayer}, @@ -310,7 +310,6 @@ pub fn build_webapp( user_agent_parser, failed_logins, )) - .layer(CookieManagerLayer::new()) .layer( TraceLayer::new_for_http() .make_span_with(|request: &Request<_>| { @@ -396,14 +395,14 @@ pub async fn init_dev_env(config: &DefGuardConfig) { info!("Test network exists already, skipping creation..."); networks.into_iter().next().unwrap() } else { - info!("Creating test network "); + info!("Creating test network"); let mut network = WireguardNetwork::new( "TestNet".to_string(), - "10.1.1.1/24".parse().unwrap(), + IpNetwork::new(IpAddr::V4(Ipv4Addr::new(10, 1, 1, 1)), 24).unwrap(), 50051, "0.0.0.0".to_string(), None, - vec!["10.1.1.0/24".parse().unwrap()], + vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::new(10, 1, 1, 0)), 24).unwrap()], ) .expect("Could not create network"); network.pubkey = "zGMeVGm9HV9I4wSKF9AXmYnnAIhDySyqLMuKpcfIaQo=".to_string(); diff --git a/src/random.rs b/src/random.rs index 624562d3e..3732db766 100644 --- a/src/random.rs +++ b/src/random.rs @@ -1,5 +1,6 @@ use rand::{distributions::Alphanumeric, thread_rng, Rng}; +/// Generate random alphanumeric string. #[must_use] pub(crate) fn gen_alphanumeric(n: usize) -> String { thread_rng() @@ -8,3 +9,9 @@ pub(crate) fn gen_alphanumeric(n: usize) -> String { .map(char::from) .collect() } + +/// Generate random 20-byte secret for TOTP. +#[must_use] +pub(crate) fn gen_totp_secret() -> Vec { + thread_rng().gen::<[u8; 20]>().to_vec() +} diff --git a/src/templates.rs b/src/templates.rs index bab83f880..5c19636aa 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -1,4 +1,4 @@ -use chrono::{Datelike, NaiveDateTime}; +use chrono::{Datelike, NaiveDateTime, Utc}; use reqwest::Url; use tera::{Context, Tera}; use thiserror::Error; @@ -46,8 +46,8 @@ pub enum TemplateError { pub fn get_base_tera( external_context: Option, session: Option<&Session>, - ip_address: Option, - device_info: Option, + ip_address: Option<&str>, + device_info: Option<&str>, ) -> Result<(Tera, Context), TemplateError> { let mut tera = Tera::default(); let mut context = external_context.unwrap_or_default(); @@ -55,23 +55,23 @@ pub fn get_base_tera( tera.add_raw_template("macros.tera", MAIL_MACROS)?; // supply context required by base context.insert("application_version", &VERSION); - let now = chrono::Utc::now(); + let now = Utc::now(); let current_year = format!("{:04}", &now.year()); context.insert("current_year", ¤t_year); context.insert("date_now", &now.format("%A, %B %d, %Y at %r").to_string()); if let Some(current_session) = session { - let device_info = current_session.device_info.clone(); + let device_info = ¤t_session.device_info; context.insert("device_type", &device_info); context.insert("ip_address", ¤t_session.ip_address); } if let Some(ip) = ip_address { - context.insert("ip_address", &ip); + context.insert("ip_address", ip); } if let Some(device_info) = device_info { - context.insert("device_type", &device_info); + context.insert("device_type", device_info); } Ok((tera, context)) @@ -131,8 +131,8 @@ pub fn desktop_start_mail( // content is stored in markdown, so it's parsed into HTML pub fn enrollment_welcome_mail( content: &str, - ip_address: Option, - device_info: Option, + ip_address: Option<&str>, + device_info: Option<&str>, ) -> Result { let (mut tera, mut context) = get_base_tera(None, None, ip_address, device_info)?; tera.add_raw_template("mail_enrollment_welcome", MAIL_ENROLLMENT_WELCOME)?; @@ -151,8 +151,8 @@ pub fn enrollment_welcome_mail( pub fn enrollment_admin_notification( user: &User, admin: &User, - ip_address: String, - device_info: Option, + ip_address: &str, + device_info: Option<&str>, ) -> Result { let (mut tera, mut context) = get_base_tera(None, None, Some(ip_address), device_info)?; @@ -183,9 +183,9 @@ pub struct TemplateLocation { pub fn new_device_added_mail( device_name: &str, public_key: &str, - template_locations: &Vec, - ip_address: Option, - device_info: Option, + template_locations: &[TemplateLocation], + ip_address: Option<&str>, + device_info: Option<&str>, ) -> Result { let (mut tera, mut context) = get_base_tera(None, None, ip_address, device_info)?; context.insert("device_name", device_name); @@ -273,8 +273,8 @@ pub fn email_mfa_code_mail(code: u32, session: &Session) -> Result, - device_info: Option, + ip_address: Option<&str>, + device_info: Option<&str>, ) -> Result { let (mut tera, mut context) = get_base_tera(None, None, ip_address, device_info)?; @@ -298,8 +298,8 @@ pub fn email_password_reset_mail( } pub fn email_password_reset_success_mail( - ip_address: Option, - device_info: Option, + ip_address: Option<&str>, + device_info: Option<&str>, ) -> Result { let (mut tera, context) = get_base_tera(None, None, ip_address, device_info)?; @@ -394,7 +394,7 @@ mod test { "Test device", "TestKey", &template_locations, - Some("1.1.1.1".to_string()), + Some("1.1.1.1"), None, )); } @@ -420,7 +420,7 @@ mod test { assert_ok!(enrollment_admin_notification( &test_user, &test_user, - "11.11.11.11".to_string(), + "11.11.11.11", None )); } diff --git a/src/wireguard_stats_purge.rs b/src/wireguard_stats_purge.rs index 70e459607..b053420ed 100644 --- a/src/wireguard_stats_purge.rs +++ b/src/wireguard_stats_purge.rs @@ -1,7 +1,7 @@ use crate::db::{DbPool, WireguardPeerStats}; use chrono::{DateTime, Duration as ChronoDuration, NaiveDateTime, Utc}; use humantime::format_duration; -use sqlx::{query, query_scalar, Error as SqlxError}; +use sqlx::{query, query_scalar, Error as SqlxError, PgExecutor}; use std::time::Duration; use tokio::time::sleep; @@ -53,7 +53,7 @@ impl WireguardPeerStats { // Check how much time has elapsed since last recorded stats purge pub async fn time_since_last_purge<'e, E>(executor: E) -> Result, SqlxError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { debug!("Checking time since last stats purge"); @@ -83,7 +83,7 @@ impl WireguardPeerStats { records_removed: i64, ) -> Result<(), SqlxError> where - E: sqlx::Executor<'e, Database = sqlx::Postgres>, + E: PgExecutor<'e>, { debug!("Recording successful stats purge in DB"); query!("INSERT INTO wireguard_stats_purge (started_at, finished_at, removal_threshold, records_removed) VALUES ($1, $2, $3, $4)", diff --git a/tests/auth.rs b/tests/auth.rs index 2fbe0a79c..2162bf81a 100644 --- a/tests/auth.rs +++ b/tests/auth.rs @@ -2,6 +2,7 @@ mod common; use std::{str::FromStr, time::SystemTime}; +use chrono::NaiveDateTime; use claims::assert_err; use defguard::{ auth::TOTP_CODE_VALIDITY_PERIOD, @@ -22,6 +23,8 @@ use webauthn_rs::prelude::{CreationChallengeResponse, RequestChallengeResponse}; use self::common::{client::TestClient, make_test_client, ClientState, X_FORWARDED_FOR}; +static SESSION_COOKIE_NAME: &str = "defguard_session"; + #[derive(Deserialize)] pub struct RecoveryCodes { codes: Option>, @@ -98,7 +101,7 @@ async fn test_logout() { // store auth cookie for later use let auth_cookie = response .cookies() - .find(|c| c.name() == "defguard_session") + .find(|c| c.name() == SESSION_COOKIE_NAME) .unwrap(); let response = client.get("/api/v1/me").send().await; @@ -705,13 +708,11 @@ This request will not trigger a blockchain transaction or cost any gas fees."; }}, "primaryType": "ProofOfOwnership", "message": {{ - "wallet": "{}", - "content": "{}", + "wallet": "{wallet_address}", + "content": "{challenge_message}", "nonce": {} }}}} "#, - wallet_address, - challenge_message, parsed_message.get("nonce").unwrap(), ) .chars() @@ -1074,3 +1075,35 @@ async fn test_login_ip_headers() { ); assert!(mail.content.contains("IP Address: 10.0.0.20")); } + +#[tokio::test] +async fn test_session_cookie() { + let (client, pool) = make_client_with_db().await; + + let auth = Auth::new("hpotter".into(), "pass123".into()); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + let auth_cookie = response + .cookies() + .find(|c| c.name() == SESSION_COOKIE_NAME) + .unwrap(); + + let session_id = auth_cookie.value(); + + // Forcibly expire the session + query!( + "UPDATE session SET expires = $1 WHERE id = $2", + NaiveDateTime::UNIX_EPOCH, + session_id + ) + .execute(&pool) + .await + .unwrap(); + + let response = client.get("/api/v1/me").send().await; + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + + let auth_cookie = response.cookies().find(|c| c.name() == SESSION_COOKIE_NAME); + assert!(auth_cookie.is_none()); +} From 3a3003ed5f23f1962eb395fc7758ae7d9b580ad2 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 18 Dec 2023 10:02:18 +0100 Subject: [PATCH 08/26] feat: group management API (#479) --- ...fb648b288f49586113cea21d889dca9655b9.json} | 4 +- ...87781e7fa62a39998d54bf79d736ed7a8827.json} | 4 +- Cargo.lock | 18 +- Cargo.toml | 2 +- src/appstate.rs | 3 +- src/bin/defguard.rs | 16 +- src/db/models/device.rs | 8 +- src/db/models/group.rs | 12 +- src/db/models/mod.rs | 8 +- src/db/models/settings.rs | 34 +-- src/db/models/user.rs | 66 ++--- src/db/models/wallet.rs | 25 +- src/db/models/wireguard.rs | 26 +- src/error.rs | 12 +- src/grpc/gateway.rs | 97 ++++---- src/grpc/mod.rs | 50 ++-- src/handlers/auth.rs | 2 +- src/handlers/group.rs | 191 ++++++++++++--- src/handlers/mail.rs | 7 +- src/handlers/mod.rs | 35 ++- src/handlers/ssh_authorized_keys.rs | 2 +- src/handlers/user.rs | 4 +- src/hex.rs | 2 +- src/ldap/error.rs | 17 +- src/ldap/hash.rs | 6 +- src/ldap/mod.rs | 227 ++++++++++-------- src/ldap/model.rs | 44 ++-- src/ldap/utils.rs | 78 ++++-- src/lib.rs | 4 + src/support.rs | 9 +- src/templates.rs | 10 +- tests/auth.rs | 87 ++++--- tests/common/client.rs | 4 +- tests/common/mod.rs | 8 +- tests/enrollment.rs | 2 +- tests/forward_auth.rs | 8 +- tests/group.rs | 103 ++++++++ tests/oauth.rs | 6 +- tests/openid.rs | 12 +- tests/settings.rs | 2 +- tests/user.rs | 42 ++-- tests/webhook.rs | 2 +- tests/wireguard.rs | 12 +- tests/wireguard_network_allowed_groups.rs | 26 +- tests/wireguard_network_import.rs | 10 +- tests/wireguard_network_stats.rs | 2 +- tests/worker.rs | 10 +- 47 files changed, 819 insertions(+), 540 deletions(-) rename .sqlx/{query-eb982489a09c45fcaec74346f499c657d3018d01be7e095683a40160d533f410.json => query-00454ac37de808986d66b6abd808fb648b288f49586113cea21d889dca9655b9.json} (78%) rename .sqlx/{query-cdda0d8e9b34aef0728fc390bf77a3211b708f23ecdb3df5cada3d628280a025.json => query-d8b3cbc7317bfdee111b80accd5d87781e7fa62a39998d54bf79d736ed7a8827.json} (50%) create mode 100644 tests/group.rs diff --git a/.sqlx/query-eb982489a09c45fcaec74346f499c657d3018d01be7e095683a40160d533f410.json b/.sqlx/query-00454ac37de808986d66b6abd808fb648b288f49586113cea21d889dca9655b9.json similarity index 78% rename from .sqlx/query-eb982489a09c45fcaec74346f499c657d3018d01be7e095683a40160d533f410.json rename to .sqlx/query-00454ac37de808986d66b6abd808fb648b288f49586113cea21d889dca9655b9.json index 5e191c1e1..e91d531de 100644 --- a/.sqlx/query-eb982489a09c45fcaec74346f499c657d3018d01be7e095683a40160d533f410.json +++ b/.sqlx/query-00454ac37de808986d66b6abd808fb648b288f49586113cea21d889dca9655b9.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT instance_name, main_logo_url, nav_logo_url, wireguard_enabled, webhooks_enabled, worker_enabled, openid_enabled FROM settings WHERE id = 1;\n ", + "query": "SELECT instance_name, main_logo_url, nav_logo_url, wireguard_enabled, webhooks_enabled, worker_enabled, openid_enabled FROM settings WHERE id = 1", "describe": { "columns": [ { @@ -52,5 +52,5 @@ false ] }, - "hash": "eb982489a09c45fcaec74346f499c657d3018d01be7e095683a40160d533f410" + "hash": "00454ac37de808986d66b6abd808fb648b288f49586113cea21d889dca9655b9" } diff --git a/.sqlx/query-cdda0d8e9b34aef0728fc390bf77a3211b708f23ecdb3df5cada3d628280a025.json b/.sqlx/query-d8b3cbc7317bfdee111b80accd5d87781e7fa62a39998d54bf79d736ed7a8827.json similarity index 50% rename from .sqlx/query-cdda0d8e9b34aef0728fc390bf77a3211b708f23ecdb3df5cada3d628280a025.json rename to .sqlx/query-d8b3cbc7317bfdee111b80accd5d87781e7fa62a39998d54bf79d736ed7a8827.json index c17124309..2c1f0f69e 100644 --- a/.sqlx/query-cdda0d8e9b34aef0728fc390bf77a3211b708f23ecdb3df5cada3d628280a025.json +++ b/.sqlx/query-d8b3cbc7317bfdee111b80accd5d87781e7fa62a39998d54bf79d736ed7a8827.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT d.wireguard_pubkey as pubkey, array[host(wnd.wireguard_ip)] as \"allowed_ips!: Vec\" FROM wireguard_network_device wnd\n JOIN device d\n ON wnd.device_id = d.id\n WHERE wireguard_network_id = $1\n ORDER BY d.id ASC\n ", + "query": "SELECT d.wireguard_pubkey as pubkey, array[host(wnd.wireguard_ip)] as \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id WHERE wireguard_network_id = $1 ORDER BY d.id ASC", "describe": { "columns": [ { @@ -24,5 +24,5 @@ null ] }, - "hash": "cdda0d8e9b34aef0728fc390bf77a3211b708f23ecdb3df5cada3d628280a025" + "hash": "d8b3cbc7317bfdee111b80accd5d87781e7fa62a39998d54bf79d736ed7a8827" } diff --git a/Cargo.lock b/Cargo.lock index e52b4edee..9f38afd68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -714,9 +714,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const-random" @@ -1780,11 +1780,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.5" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -4478,18 +4478,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 60ef1d613..2678ab831 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ humantime = "2.1" # match ipnetwork version from sqlx ipnetwork = { version = "0.20", features = ["serde"] } jsonwebtoken = "9.2" -ldap3 = "0.11" +ldap3 = { version = "0.11", default-features = false, features = ["tls"] } lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] } md4 = "0.10" otpauth = "0.4" diff --git a/src/appstate.rs b/src/appstate.rs index 7a662b612..feba4d36d 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -36,13 +36,14 @@ pub struct AppState { } impl AppState { - pub fn trigger_action(&self, event: AppEvent) { + pub(crate) fn trigger_action(&self, event: AppEvent) { let event_name = event.name().to_owned(); match self.tx.send(event) { Ok(()) => info!("Sent trigger {event_name}"), Err(err) => error!("Error sending trigger {event_name}: {err}"), } } + /// Handle webhook events async fn handle_triggers(pool: DbPool, mut rx: UnboundedReceiver) { let reqwest_client = Client::builder().user_agent("reqwest").build().unwrap(); diff --git a/src/bin/defguard.rs b/src/bin/defguard.rs index 60f25c616..851d1831a 100644 --- a/src/bin/defguard.rs +++ b/src/bin/defguard.rs @@ -1,3 +1,12 @@ +use std::{ + fs::read_to_string, + sync::{Arc, Mutex}, +}; + +use secrecy::ExposeSecret; +use tokio::sync::{broadcast, mpsc::unbounded_channel}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + use defguard::{ auth::failed_login::FailedLoginMap, config::{Command, DefGuardConfig}, @@ -10,13 +19,6 @@ use defguard::{ wireguard_stats_purge::run_periodic_stats_purge, SERVER_CONFIG, }; -use secrecy::ExposeSecret; -use std::{ - fs::read_to_string, - sync::{Arc, Mutex}, -}; -use tokio::sync::{broadcast, mpsc::unbounded_channel}; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[macro_use] extern crate tracing; diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 98692d614..f1c9fc9a3 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -655,11 +655,11 @@ mod test { network.save(&pool).await.unwrap(); let mut user = User::new( - "testuser".to_string(), + "testuser", Some("hunter2"), - "Tester".to_string(), - "Test".to_string(), - "test@test.com".to_string(), + "Tester", + "Test", + "test@test.com", None, ); user.save(&pool).await.unwrap(); diff --git a/src/db/models/group.rs b/src/db/models/group.rs index b8a49e914..2ee5f91ff 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -11,7 +11,7 @@ pub struct Group { impl Group { #[must_use] - pub fn new(name: &str) -> Self { + pub fn new>(name: S) -> Self { Self { id: None, name: name.into(), @@ -48,7 +48,7 @@ impl Group { } } - pub async fn fetch_all_members<'e, E>(&self, executor: E) -> Result, SqlxError> + pub async fn members<'e, E>(&self, executor: E) -> Result, SqlxError> where E: PgExecutor<'e>, { @@ -233,11 +233,11 @@ mod test { group.save(&pool).await.unwrap(); let mut user = User::new( - "hpotter".into(), + "hpotter", Some("pass123"), - "Potter".into(), - "Harry".into(), - "h.potter@hogwart.edu.uk".into(), + "Potter", + "Harry", + "h.potter@hogwart.edu.uk", None, ); user.save(&pool).await.unwrap(); diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index fc29365d1..6e46502d9 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -268,11 +268,11 @@ mod test { #[sqlx::test] async fn test_user_info(pool: DbPool) { let mut user = User::new( - "hpotter".into(), + "hpotter", Some("pass123"), - "Potter".into(), - "Harry".into(), - "h.potter@hogwart.edu.uk".into(), + "Potter", + "Harry", + "h.potter@hogwart.edu.uk", None, ); user.save(&pool).await.unwrap(); diff --git a/src/db/models/settings.rs b/src/db/models/settings.rs index b9eb7f489..ed8257ba5 100644 --- a/src/db/models/settings.rs +++ b/src/db/models/settings.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use model_derive::Model; -use sqlx::{query, Error as SqlxError, PgExecutor, Type}; +use sqlx::{query, query_as, Error as SqlxError, PgExecutor, Type}; use struct_patch::Patch; use super::DbPool; @@ -46,7 +46,7 @@ pub struct Settings { pub enrollment_welcome_email: Option, pub enrollment_welcome_email_subject: Option, pub enrollment_use_welcome_message_as_email: bool, - // Instance uuid needed for desktop client + // Instance UUID needed for desktop client #[serde(skip)] pub uuid: uuid::Uuid, // LDAP @@ -110,14 +110,7 @@ impl Settings { } } -#[derive(Debug, Serialize, Clone)] -pub struct SettingsBranding { - pub instance_name: String, - pub main_logo_url: String, - pub nav_logo_url: String, -} - -#[derive(Debug, Serialize, Clone)] +#[derive(Serialize)] pub struct SettingsEssentials { pub instance_name: String, pub main_logo_url: String, @@ -129,11 +122,18 @@ pub struct SettingsEssentials { } impl SettingsEssentials { - pub async fn get_settings_essentials(pool: &DbPool) -> Result { - let res = sqlx::query_as!(SettingsEssentials, r#" - SELECT instance_name, main_logo_url, nav_logo_url, wireguard_enabled, webhooks_enabled, worker_enabled, openid_enabled FROM settings WHERE id = 1; - "#).fetch_one(pool).await?; - Ok(res) + pub(crate) async fn get_settings_essentials<'e, E>(executor: E) -> Result + where + E: PgExecutor<'e>, + { + query_as!( + SettingsEssentials, + "SELECT instance_name, main_logo_url, nav_logo_url, wireguard_enabled, \ + webhooks_enabled, worker_enabled, openid_enabled \ + FROM settings WHERE id = 1" + ) + .fetch_one(executor) + .await } } @@ -152,7 +152,7 @@ impl From for SettingsEssentials { } mod defaults { - pub const WELCOME_MESSAGE: &str = "Dear {{ first_name }} {{ last_name }}, + pub static WELCOME_MESSAGE: &str = "Dear {{ first_name }} {{ last_name }}, By completing the enrollment process, you now have now access to all company systems. @@ -187,5 +187,5 @@ Sent by defguard {{ defguard_version }} Star us on GitHub! https://github.com/defguard/defguard\ "; - pub const WELCOME_EMAIL_SUBJECT: &str = "[defguard] Welcome message after enrollment"; + pub static WELCOME_EMAIL_SUBJECT: &str = "[defguard] Welcome message after enrollment"; } diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 40d294989..aa3902886 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -93,23 +93,23 @@ impl User { } #[must_use] - pub fn new( - username: String, + pub fn new>( + username: S, password: Option<&str>, - last_name: String, - first_name: String, - email: String, + last_name: S, + first_name: S, + email: S, phone: Option, ) -> Self { let password_hash = password.and_then(|password_hash| Self::hash_password(password_hash).ok()); Self { id: None, - username, + username: username.into(), password_hash, - last_name, - first_name, - email, + last_name: last_name.into(), + first_name: first_name.into(), + email: email.into(), phone, ssh_key: None, pgp_key: None, @@ -152,7 +152,10 @@ impl User { } /// Generate new TOTP secret, save it, then return it as RFC 4648 base32-encoded string. - pub async fn new_totp_secret(&mut self, pool: &DbPool) -> Result { + pub async fn new_totp_secret<'e, E>(&mut self, executor: E) -> Result + where + E: PgExecutor<'e>, + { let secret = gen_totp_secret(); if let Some(id) = self.id { query!( @@ -160,7 +163,7 @@ impl User { secret, id ) - .execute(pool) + .execute(executor) .await?; } let secret_base32 = TOTP::from_bytes(&secret).base32_secret(); @@ -169,7 +172,10 @@ impl User { } /// Generate new email secret, similar to TOTP secret above, but don't return generated value. - pub async fn new_email_secret(&mut self, pool: &DbPool) -> Result<(), SqlxError> { + pub async fn new_email_secret<'e, E>(&mut self, executor: E) -> Result<(), SqlxError> + where + E: PgExecutor<'e>, + { let email_secret = gen_totp_secret(); if let Some(id) = self.id { query!( @@ -177,7 +183,7 @@ impl User { email_secret, id ) - .execute(pool) + .execute(executor) .await?; } self.email_mfa_secret = Some(email_secret); @@ -783,7 +789,7 @@ impl User { // if new user was created add them to admin group (ID 1) if let Some(new_user_id) = result { - info!("New admin user was created, adding to Admin group..."); + info!("New admin user has been created, adding to Admin group..."); query("INSERT INTO group_user (group_id, user_id) VALUES (1, $1)") .bind(new_user_id) .execute(pool) @@ -801,11 +807,11 @@ mod test { #[sqlx::test] async fn test_user(pool: DbPool) { let mut user = User::new( - "hpotter".into(), + "hpotter", Some("pass123"), - "Potter".into(), - "Harry".into(), - "h.potter@hogwart.edu.uk".into(), + "Potter", + "Harry", + "h.potter@hogwart.edu.uk", None, ); user.save(&pool).await.unwrap(); @@ -830,21 +836,21 @@ mod test { #[sqlx::test] async fn test_all_users(pool: DbPool) { let mut harry = User::new( - "hpotter".into(), + "hpotter", Some("pass123"), - "Potter".into(), - "Harry".into(), - "h.potter@hogwart.edu.uk".into(), + "Potter", + "Harry", + "h.potter@hogwart.edu.uk", None, ); harry.save(&pool).await.unwrap(); let mut albus = User::new( - "adumbledore".into(), + "adumbledore", Some("magic!"), - "Dumbledore".into(), - "Albus".into(), - "a.dumbledore@hogwart.edu.uk".into(), + "Dumbledore", + "Albus", + "a.dumbledore@hogwart.edu.uk", None, ); albus.save(&pool).await.unwrap(); @@ -861,11 +867,11 @@ mod test { #[sqlx::test] async fn test_recovery_codes(pool: DbPool) { let mut harry = User::new( - "hpotter".into(), + "hpotter", Some("pass123"), - "Potter".into(), - "Harry".into(), - "h.potter@hogwart.edu.uk".into(), + "Potter", + "Harry", + "h.potter@hogwart.edu.uk", None, ); harry.get_recovery_codes(&pool).await.unwrap(); diff --git a/src/db/models/wallet.rs b/src/db/models/wallet.rs index 6983ef24b..e5de96e6f 100644 --- a/src/db/models/wallet.rs +++ b/src/db/models/wallet.rs @@ -77,20 +77,20 @@ pub struct Wallet { impl Wallet { #[must_use] - pub fn new_for_user( + pub fn new_for_user>( user_id: i64, - address: String, - name: String, + address: S, + name: S, chain_id: i64, - challenge_message: String, + challenge_message: S, ) -> Self { Self { id: None, user_id, - address, - name, + address: address.into(), + name: name.into(), chain_id, - challenge_message, + challenge_message: challenge_message.into(), challenge_signature: None, creation_timestamp: Utc::now().naive_utc(), validation_timestamp: None, @@ -184,11 +184,14 @@ impl Wallet { .collect() } - pub async fn find_by_user_and_address( - pool: &DbPool, + pub async fn find_by_user_and_address<'e, E>( + executor: E, user_id: i64, address: &str, - ) -> Result, SqlxError> { + ) -> Result, SqlxError> + where + E: PgExecutor<'e>, + { query_as!( Self, "SELECT id \"id?\", user_id, address, name, chain_id, challenge_message, challenge_signature, \ @@ -197,7 +200,7 @@ impl Wallet { user_id, address ) - .fetch_optional(pool) + .fetch_optional(executor) .await } diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 084654583..2961b4b98 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -438,7 +438,7 @@ impl WireguardNetwork { })); } else { let msg = format!("Device {} does not exist", device_network_config.device_id); - error!("{msg}"); + error!(msg); return Err(WireguardNetworkError::Unexpected(msg)); } } @@ -1013,11 +1013,11 @@ mod test { async fn add_devices(pool: &DbPool, network: &WireguardNetwork, count: usize) { let mut user = User::new( - "testuser".to_string(), + "testuser", Some("hunter2"), - "Tester".to_string(), - "Test".to_string(), - "test@test.com".to_string(), + "Tester", + "Test", + "test@test.com", None, ); user.save(pool).await.unwrap(); @@ -1094,11 +1094,11 @@ mod test { network.save(&pool).await.unwrap(); let mut user = User::new( - "testuser".to_string(), + "testuser", Some("hunter2"), - "Tester".to_string(), - "Test".to_string(), - "test@test.com".to_string(), + "Tester", + "Test", + "test@test.com", None, ); user.save(&pool).await.unwrap(); @@ -1144,11 +1144,11 @@ mod test { network.save(&pool).await.unwrap(); let mut user = User::new( - "testuser".to_string(), + "testuser", Some("hunter2"), - "Tester".to_string(), - "Test".to_string(), - "test@test.com".to_string(), + "Tester", + "Test", + "test@test.com", None, ); user.save(&pool).await.unwrap(); diff --git a/src/error.rs b/src/error.rs index dfc3a9ac6..3c003b0f9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,7 +9,7 @@ use crate::{ wireguard::WireguardNetworkError, }, grpc::GatewayMapError, - ldap::error::OriLDAPError, + ldap::error::LdapError, templates::TemplateError, }; @@ -64,12 +64,12 @@ impl From for WebError { } } -impl From for WebError { - fn from(error: OriLDAPError) -> Self { +impl From for WebError { + fn from(error: LdapError) -> Self { match error { - OriLDAPError::ObjectNotFound(msg) => Self::ObjectNotFound(msg), - OriLDAPError::Ldap(msg) => Self::Ldap(msg), - OriLDAPError::MissingSettings => Self::Ldap("LDAP settings are missing".to_string()), + LdapError::ObjectNotFound(msg) => Self::ObjectNotFound(msg), + LdapError::Ldap(msg) => Self::Ldap(msg), + LdapError::MissingSettings => Self::Ldap("LDAP settings are missing".to_string()), } } } diff --git a/src/grpc/gateway.rs b/src/grpc/gateway.rs index 6c9370939..6b35d5387 100644 --- a/src/grpc/gateway.rs +++ b/src/grpc/gateway.rs @@ -43,13 +43,11 @@ impl WireguardNetwork { debug!("Fetching all peers for network {}", self.id.unwrap()); let result = query_as!( Peer, - r#" - SELECT d.wireguard_pubkey as pubkey, array[host(wnd.wireguard_ip)] as "allowed_ips!: Vec" FROM wireguard_network_device wnd - JOIN device d - ON wnd.device_id = d.id - WHERE wireguard_network_id = $1 - ORDER BY d.id ASC - "#, + "SELECT d.wireguard_pubkey as pubkey, array[host(wnd.wireguard_ip)] as \"allowed_ips!: Vec\" \ + FROM wireguard_network_device wnd \ + JOIN device d ON wnd.device_id = d.id \ + WHERE wireguard_network_id = $1 \ + ORDER BY d.id ASC", self.id ) .fetch_all(executor) @@ -185,7 +183,7 @@ impl GatewayUpdatesHandler { self.gateway_hostname, self.network ); while let Ok(update) = self.events_rx.recv().await { - debug!("Received wireguard update: {:?}", update); + debug!("Received wireguard update: {update:?}"); let result = match update { GatewayEvent::NetworkCreated(network_id, network) => { if network_id == self.network_id { @@ -277,7 +275,7 @@ impl GatewayUpdatesHandler { peers: Vec, update_type: i32, ) -> Result<(), Status> { - debug!("Sending network update for network {}", network); + debug!("Sending network update for network {network}"); if let Err(err) = self .tx .send(Ok(Update { @@ -295,8 +293,8 @@ impl GatewayUpdatesHandler { let msg = format!( "Failed to send network update, network {network}, update type: {update_type}, error: {err}", ); - error!("{msg}"); - return Err(Status::new(tonic::Code::Internal, msg)); + error!(msg); + return Err(Status::new(Code::Internal, msg)); } Ok(()) } @@ -322,11 +320,11 @@ impl GatewayUpdatesHandler { .await { let msg = format!( - "Failed to send network update, network {}, update type: {}, error: {}", - self.network, 2, err, + "Failed to send network update, network {}, update type: 2, error: {err}", + self.network, ); - error!("{}", msg); - return Err(Status::new(tonic::Code::Internal, msg)); + error!(msg); + return Err(Status::new(Code::Internal, msg)); } Ok(()) } @@ -343,11 +341,11 @@ impl GatewayUpdatesHandler { .await { let msg = format!( - "Failed to send peer update for network {}, update type: {}, error: {}", - self.network, update_type, err, + "Failed to send peer update for network {}, update type: {update_type}, error: {err}", + self.network ); - error!("{}", msg); - return Err(Status::new(tonic::Code::Internal, msg)); + error!(msg); + return Err(Status::new(Code::Internal, msg)); } Ok(()) } @@ -367,11 +365,11 @@ impl GatewayUpdatesHandler { .await { let msg = format!( - "Failed to send peer update for network {}, peer {}, update type: 2, error: {}", - self.network, peer_pubkey, err, + "Failed to send peer update for network {}, peer {peer_pubkey}, update type: 2, error: {err}", + self.network, ); - error!("{}", msg); - return Err(Status::new(tonic::Code::Internal, msg)); + error!(msg); + return Err(Status::new(Code::Internal, msg)); } Ok(()) } @@ -383,6 +381,7 @@ pub struct GatewayUpdatesStream { network_id: i64, gateway_hostname: String, gateway_state: Arc>, + pool: DbPool, } impl GatewayUpdatesStream { @@ -393,6 +392,7 @@ impl GatewayUpdatesStream { network_id: i64, gateway_hostname: String, gateway_state: Arc>, + pool: DbPool, ) -> Self { Self { task_handle, @@ -400,6 +400,7 @@ impl GatewayUpdatesStream { network_id, gateway_hostname, gateway_state, + pool, } } } @@ -421,7 +422,7 @@ impl Drop for GatewayUpdatesStream { self.gateway_state .lock() .unwrap() - .disconnect_gateway(self.network_id, self.gateway_hostname.clone()) + .disconnect_gateway(self.network_id, self.gateway_hostname.clone(), &self.pool) .expect("Unable to disconnect gateway."); } } @@ -445,25 +446,19 @@ impl gateway_service_server::GatewayService for GatewayServer { stats.device_id = match Device::find_by_pubkey(&self.pool, &public_key).await { Ok(Some(device)) => device .id - .ok_or_else(|| Status::new(tonic::Code::Internal, "Device has no id"))?, + .ok_or_else(|| Status::new(Code::Internal, "Device has no ID"))?, Ok(None) => { - error!("Device with public key {} not found", &public_key); + error!("Device with public key {public_key} not found"); return Err(Status::new( - tonic::Code::Internal, - format!("Device with public key {} not found", &public_key), + Code::Internal, + format!("Device with public key {public_key} not found"), )); } Err(err) => { - error!( - "Failed to retrieve device with public key {}: {err}", - &public_key - ); + error!("Failed to retrieve device with public key {public_key}: {err}",); return Err(Status::new( - tonic::Code::Internal, - format!( - "Failed to retrieve device with public key {}: {err}", - &public_key - ), + Code::Internal, + format!("Failed to retrieve device with public key {public_key}: {err}",), )); } }; @@ -471,7 +466,7 @@ impl gateway_service_server::GatewayService for GatewayServer { if let Err(err) = stats.save(&self.pool).await { error!("Saving WireGuard peer stats to db failed: {err}"); return Err(Status::new( - tonic::Code::Internal, + Code::Internal, format!("Saving WireGuard peer stats to db failed: {err}"), )); } @@ -488,17 +483,13 @@ impl gateway_service_server::GatewayService for GatewayServer { let network_id = Self::get_network_id(request.metadata())?; let hostname = Self::get_gateway_hostname(request.metadata())?; - let pool = self.pool.clone(); - let mut network = WireguardNetwork::find_by_id(&pool, network_id) + let mut network = WireguardNetwork::find_by_id(&self.pool, network_id) .await .map_err(|e| { - error!("Network {} not found", network_id); - Status::new( - tonic::Code::Internal, - format!("Failed to retrieve network: {e}"), - ) + error!("Network {network_id} not found"); + Status::new(Code::Internal, format!("Failed to retrieve network: {e}")) })? - .ok_or_else(|| Status::new(tonic::Code::Internal, "Network not found"))?; + .ok_or_else(|| Status::new(Code::Internal, "Network not found"))?; info!("Sending configuration to gateway client, network {network}."); @@ -506,23 +497,22 @@ impl gateway_service_server::GatewayService for GatewayServer { let mut state = self.state.lock().unwrap(); state.add_gateway( network_id, - network.name.clone(), + &network.name, hostname, request.into_inner().name, - self.pool.clone(), self.mail_tx.clone(), ); } network.connected_at = Some(Utc::now().naive_utc()); - if let Err(err) = network.save(&pool).await { + if let Err(err) = network.save(&self.pool).await { error!("Failed to update network {network_id} status: {err}"); } - let peers = network.get_peers(&pool).await.map_err(|error| { + let peers = network.get_peers(&self.pool).await.map_err(|error| { error!("Failed to fetch peers for network {network_id}: {error}",); Status::new( - tonic::Code::Internal, + Code::Internal, format!("Failed to retrieve peers for network: {network_id}"), ) })?; @@ -539,7 +529,7 @@ impl gateway_service_server::GatewayService for GatewayServer { .map_err(|_| { error!("Failed to fetch network {gateway_network_id}"); Status::new( - tonic::Code::Internal, + Code::Internal, format!("Failed to retrieve network {gateway_network_id}"), ) })? @@ -556,7 +546,7 @@ impl gateway_service_server::GatewayService for GatewayServer { .connect_gateway(gateway_network_id, &hostname) .map_err(|err| { error!("Failed to connect gateway: {err}"); - Status::new(tonic::Code::Internal, "Failed to connect gateway ") + Status::new(Code::Internal, "Failed to connect gateway") })?; // clone here before moving into a closure @@ -578,6 +568,7 @@ impl gateway_service_server::GatewayService for GatewayServer { gateway_network_id, hostname, Arc::clone(&self.state), + self.pool.clone(), ))) } } diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index e40801bbc..2b8114de1 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -1,4 +1,3 @@ -use chrono::Duration as ChronoDuration; use std::{ collections::hash_map::HashMap, time::{Duration, Instant}, @@ -9,7 +8,7 @@ use std::{ sync::{Arc, Mutex}, }; -use chrono::{NaiveDateTime, Utc}; +use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc}; use serde::Serialize; use thiserror::Error; use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender}; @@ -88,31 +87,20 @@ impl GatewayMap { pub fn add_gateway( &mut self, network_id: i64, - network_name: String, + network_name: &str, hostname: String, name: Option, - pool: DbPool, mail_tx: UnboundedSender, ) { info!("Adding gateway {hostname} with to gateway map for network {network_id}",); + let gateway_state = GatewayState::new(network_id, network_name, &hostname, name, mail_tx); + if let Some(network_gateway_map) = self.0.get_mut(&network_id) { - network_gateway_map - .entry(hostname.clone()) - .or_insert(GatewayState::new( - network_id, - network_name, - hostname, - name, - pool, - mail_tx, - )); + network_gateway_map.entry(hostname).or_insert(gateway_state); } else { // no map for a given network exists yet let mut network_gateway_map = HashMap::new(); - network_gateway_map.insert( - hostname.clone(), - GatewayState::new(network_id, network_name, hostname, name, pool, mail_tx), - ); + network_gateway_map.insert(hostname, gateway_state); self.0.insert(network_id, network_gateway_map); } } @@ -177,13 +165,14 @@ impl GatewayMap { &mut self, network_id: i64, hostname: String, + pool: &DbPool, ) -> Result<(), GatewayMapError> { info!("Disconnecting gateway {hostname} in network {network_id}"); if let Some(network_gateway_map) = self.0.get_mut(&network_id) { if let Some(state) = network_gateway_map.get_mut(&hostname) { state.connected = false; state.disconnected_at = Some(Utc::now().naive_utc()); - state.send_disconnect_notification()?; + state.send_disconnect_notification(pool)?; return Ok(()); }; }; @@ -246,42 +235,39 @@ pub struct GatewayState { #[serde(skip)] pub mail_tx: UnboundedSender, #[serde(skip)] - pub pool: DbPool, - #[serde(skip)] pub last_email_notification: Option, } impl GatewayState { #[must_use] - pub fn new( + pub fn new>( network_id: i64, - network_name: String, - hostname: String, + network_name: S, + hostname: S, name: Option, - pool: DbPool, mail_tx: UnboundedSender, ) -> Self { Self { uid: Uuid::new_v4(), connected: false, network_id, - network_name, + network_name: network_name.into(), name, - hostname, + hostname: hostname.into(), connected_at: None, disconnected_at: None, mail_tx, - pool, last_email_notification: None, } } + /// Send gateway disconnected notification /// Sends notification only if last notification time is bigger than specified in config - fn send_disconnect_notification(&mut self) -> Result<(), GatewayMapError> { + fn send_disconnect_notification(&mut self, pool: &DbPool) -> Result<(), GatewayMapError> { // Clone here because self doesn't live long enough let name = self.name.clone(); let mail_tx = self.mail_tx.clone(); - let pool = self.pool.clone(); + let pool = pool.clone(); let hostname = self.hostname.clone(); let network_name = self.network_name.clone(); let send_email = if let Some(last_notification_time) = self.last_email_notification { @@ -305,12 +291,12 @@ impl GatewayState { send_gateway_disconnected_email(name, network_name, &hostname, &mail_tx, &pool) .await { - error!("Sending gateway disconnected notification failed: {e}"); + error!("Failed to send gateway disconnect notification: {e}"); } }); } else { debug!( - "Gateway {hostname} disconnected not sending email. Last notification time was at {:?}", + "Gateway {hostname} disconnected. Email notification not sent. Last notification was at {:?}", self.last_email_notification ); }; diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index fa01c4b99..be0643ea5 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -106,7 +106,7 @@ pub async fn authenticate( }; let server_config = SERVER_CONFIG.get().ok_or(WebError::ServerConfigMissing)?; - let auth_cookie = Cookie::build((SESSION_COOKIE_NAME, session.clone().id)) + let auth_cookie = Cookie::build((SESSION_COOKIE_NAME, session.id.clone())) .domain( server_config .cookie_domain diff --git a/src/handlers/group.rs b/src/handlers/group.rs index ec899c971..afa4d96eb 100644 --- a/src/handlers/group.rs +++ b/src/handlers/group.rs @@ -4,17 +4,17 @@ use axum::{ }; use serde_json::json; -use super::{ApiResponse, ApiResult, Username}; +use super::{ApiResponse, GroupInfo, Username}; use crate::{ appstate::AppState, auth::{SessionInfo, UserAdminRole}, db::{Group, User}, error::WebError, - ldap::utils::{ldap_add_user_to_group, ldap_remove_user_from_group}, + // ldap::utils::{ldap_add_user_to_group, ldap_modify_group, ldap_remove_user_from_group}, }; #[derive(Serialize)] -pub struct Groups { +pub(crate) struct Groups { groups: Vec, } @@ -25,20 +25,11 @@ impl Groups { } } -#[derive(Serialize)] -pub struct GroupInfo { - name: String, - members: Vec, -} - -impl GroupInfo { - #[must_use] - pub fn new(name: String, members: Vec) -> Self { - Self { name, members } - } -} - -pub async fn list_groups(_session: SessionInfo, State(appstate): State) -> ApiResult { +/// GET: Retrieve all groups. +pub(crate) async fn list_groups( + _session: SessionInfo, + State(appstate): State, +) -> Result { debug!("Listing groups"); let groups = Group::all(&appstate.pool) .await? @@ -52,36 +43,165 @@ pub async fn list_groups(_session: SessionInfo, State(appstate): State }) } -pub async fn get_group( +/// GET: Retrieve group with `name`. +pub(crate) async fn get_group( _session: SessionInfo, State(appstate): State, Path(name): Path, -) -> ApiResult { +) -> Result { debug!("Retrieving group {name}"); if let Some(group) = Group::find_by_name(&appstate.pool, &name).await? { let members = group.member_usernames(&appstate.pool).await?; info!("Retrieved group {name}"); Ok(ApiResponse { - json: json!(GroupInfo::new(name, members)), + json: json!(GroupInfo::new(name, Some(members))), status: StatusCode::OK, }) } else { - error!("Group {name} not found"); - Err(WebError::ObjectNotFound(format!("Group {name} not found",))) + let msg = format!("Group {name} not found"); + error!(msg); + Err(WebError::ObjectNotFound(msg)) + } +} + +/// POST: Create group with a given name and member list. +pub(crate) async fn create_group( + _role: UserAdminRole, + State(appstate): State, + Json(group_info): Json, +) -> Result { + debug!("Creating group {}", group_info.name); + + // FIXME: LDAP operations are not reverted. + let mut transaction = appstate.pool.begin().await?; + + let mut group = Group::new(&group_info.name); + // FIXME: conflicts must not return interal server error (500). + group.save(&appstate.pool).await?; + // TODO: create group in LDAP + + if let Some(ref members) = group_info.members { + for username in members { + let Some(user) = User::find_by_username(&mut *transaction, username).await? else { + let msg = format!("Failed to find user {username}"); + error!(msg); + return Err(WebError::ObjectNotFound(msg)); + }; + user.add_to_group(&mut *transaction, &group).await?; + // let _result = ldap_add_user_to_group(&mut *transaction, username, &group.name).await; + } } + + transaction.commit().await?; + + info!("Created group {}", group_info.name); + Ok(ApiResponse { + json: json!(group_info), + status: StatusCode::CREATED, + }) } -pub async fn add_group_member( +/// PUT: Rename group and/or change group members. +pub(crate) async fn modify_group( + _role: UserAdminRole, + State(appstate): State, + Path(name): Path, + Json(group_info): Json, +) -> Result { + debug!("Modifying group {}", group_info.name); + let Some(mut group) = Group::find_by_name(&appstate.pool, &name).await? else { + let msg = format!("Group {name} not found"); + error!(msg); + return Err(WebError::ObjectNotFound(msg)); + }; + + // FIXME: LDAP operations are not reverted. + let mut transaction = appstate.pool.begin().await?; + + // Rename only when needed. + if group.name != group_info.name { + group.name = group_info.name; + group.save(&mut *transaction).await?; + // let _result = ldap_modify_group(&mut *transaction, &group.name, &group).await; + } + + // Modify group members. + if let Some(ref members) = group_info.members { + let mut current_members = group.members(&mut *transaction).await?; + for username in members { + if let Some(index) = current_members + .iter() + .position(|gm| &gm.username == username) + { + // This member is already in the group. + current_members.remove(index); + continue; + } + + // Add new members to the group. + if let Some(user) = User::find_by_username(&mut *transaction, username).await? { + user.add_to_group(&mut *transaction, &group).await?; + // let _result = + // ldap_add_user_to_group(&mut *transaction, username, &group.name).await; + } + } + + // Remove outstanding members. + for user in current_members { + user.remove_from_group(&mut *transaction, &group).await?; + // let _result = + // ldap_remove_user_from_group(&mut *transaction, &user.username, &group.name).await; + } + } + + transaction.commit().await?; + + info!("Modified group {}", group.name); + Ok(ApiResponse::default()) +} + +/// DELETE: Remove group with `name`. +pub(crate) async fn delete_group( + _session: SessionInfo, + + State(appstate): State, + Path(name): Path, +) -> Result { + debug!("Deleting group {name}"); + // Administrative group must not be removed. + // Note: Group names are unique, so this condition should be sufficient. + if name == appstate.config.admin_groupname { + return Ok(ApiResponse { + json: json!({}), + status: StatusCode::BAD_REQUEST, + }); + } + + if let Some(group) = Group::find_by_name(&appstate.pool, &name).await? { + group.delete(&appstate.pool).await?; + // TODO: delete group from LDAP + + info!("Deleted group {name}"); + Ok(ApiResponse::default()) + } else { + let msg = format!("Failed to find group {name}"); + error!(msg); + Err(WebError::ObjectNotFound(msg)) + } +} + +/// POST: Find a group with `name` and add `username` as a member. +pub(crate) async fn add_group_member( _role: UserAdminRole, State(appstate): State, Path(name): Path, Json(data): Json, -) -> ApiResult { +) -> Result { if let Some(group) = Group::find_by_name(&appstate.pool, &name).await? { if let Some(user) = User::find_by_username(&appstate.pool, &data.username).await? { debug!("Adding user: {} to group: {}", user.username, group.name); user.add_to_group(&appstate.pool, &group).await?; - let _result = ldap_add_user_to_group(&appstate.pool, &user.username, &group.name).await; + // let _result = ldap_add_user_to_group(&appstate.pool, &user.username, &group.name).await; info!("Added user: {} to group: {}", user.username, group.name); Ok(ApiResponse::default()) } else { @@ -92,16 +212,18 @@ pub async fn add_group_member( ))) } } else { - error!("Group {name} not found"); - Err(WebError::ObjectNotFound(format!("Group {name} not found"))) + let msg = format!("Group {name} not found"); + error!(msg); + Err(WebError::ObjectNotFound(msg)) } } -pub async fn remove_group_member( +/// DELETE: Remove `username` from group with `name`. +pub(crate) async fn remove_group_member( _role: UserAdminRole, State(appstate): State, Path((name, username)): Path<(String, String)>, -) -> ApiResult { +) -> Result { if let Some(group) = Group::find_by_name(&appstate.pool, &name).await? { if let Some(user) = User::find_by_username(&appstate.pool, &username).await? { debug!( @@ -109,18 +231,17 @@ pub async fn remove_group_member( user.username, group.name ); user.remove_from_group(&appstate.pool, &group).await?; - let _result = - ldap_remove_user_from_group(&appstate.pool, &user.username, &group.name).await; + // let _result = + // ldap_remove_user_from_group(&appstate.pool, &user.username, &group.name).await; info!("Removed user: {} from group: {}", user.username, group.name); Ok(ApiResponse { json: json!({}), status: StatusCode::OK, }) } else { - error!("User not found {username}"); - Err(WebError::ObjectNotFound(format!( - "User {username} not found" - ))) + let msg = format!("User {username} not found"); + error!(msg); + Err(WebError::ObjectNotFound(msg)) } } else { error!("Group {name} not found"); diff --git a/src/handlers/mail.rs b/src/handlers/mail.rs index 61307237d..0a273a309 100644 --- a/src/handlers/mail.rs +++ b/src/handlers/mail.rs @@ -430,12 +430,7 @@ pub fn send_password_reset_email( let mail = Mail { to: user.email.clone(), subject: EMAIL_PASSOWRD_RESET_START_SUBJECT.into(), - content: templates::email_password_reset_mail( - service_url.clone(), - token, - ip_address, - device_info, - )?, + content: templates::email_password_reset_mail(service_url, token, ip_address, device_info)?, attachments: Vec::new(), result_tx: None, }; diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 63328dbf2..f836b0a5b 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -57,11 +57,11 @@ impl From for ApiResponse { ApiResponse::new(json!({ "msg": msg }), StatusCode::NOT_FOUND) } WebError::Authorization(msg) => { - error!("{msg}"); + error!(msg); ApiResponse::new(json!({ "msg": msg }), StatusCode::UNAUTHORIZED) } WebError::Forbidden(msg) => { - error!("{msg}"); + error!(msg); ApiResponse::new(json!({ "msg": msg }), StatusCode::FORBIDDEN) } WebError::DbError(_) @@ -92,7 +92,7 @@ impl From for ApiResponse { WebError::IncorrectUsername(msg) | WebError::PubkeyValidation(msg) | WebError::BadRequest(msg) => { - error!("{msg}"); + error!(msg); ApiResponse::new(json!({ "msg": msg }), StatusCode::BAD_REQUEST) } WebError::TemplateError(err) => { @@ -135,8 +135,11 @@ pub struct Auth { impl Auth { #[must_use] - pub fn new(username: String, password: String) -> Self { - Self { username, password } + pub fn new>(username: S, password: S) -> Self { + Self { + username: username.into(), + password: password.into(), + } } } @@ -147,8 +150,10 @@ pub struct AuthTotp { impl AuthTotp { #[must_use] - pub fn new(secret: String) -> Self { - Self { secret } + pub fn new>(secret: S) -> Self { + Self { + secret: secret.into(), + } } } @@ -164,6 +169,22 @@ impl AuthCode { } } +#[derive(Deserialize, Serialize)] +pub struct GroupInfo { + pub name: String, + pub members: Option>, +} + +impl GroupInfo { + #[must_use] + pub fn new>(name: S, members: Option>) -> Self { + Self { + name: name.into(), + members, + } + } +} + #[derive(Deserialize, Serialize)] pub struct Username { pub username: String, diff --git a/src/handlers/ssh_authorized_keys.rs b/src/handlers/ssh_authorized_keys.rs index 067fa39de..9fecb8c57 100644 --- a/src/handlers/ssh_authorized_keys.rs +++ b/src/handlers/ssh_authorized_keys.rs @@ -68,7 +68,7 @@ pub async fn get_authorized_keys( None => { debug!("Fetching SSH keys for all users in group {group_name}"); // fetch all users in group - let users = group.fetch_all_members(&appstate.pool).await?; + let users = group.members(&appstate.pool).await?; for user in users { add_user_keys_to_list(user); } diff --git a/src/handlers/user.rs b/src/handlers/user.rs index 1f6a9ae1a..c3b650632 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -113,7 +113,7 @@ pub async fn add_user( // check username if let Err(err) = check_username(&username) { - debug!("{}", err); + debug!("{err}"); return Ok(ApiResponse { json: json!({}), status: StatusCode::BAD_REQUEST, @@ -281,7 +281,7 @@ pub async fn modify_user( debug!("User {} updating user {username}", session.user.username); let mut user = user_for_admin_or_self(&appstate.pool, &session, &username).await?; if let Err(err) = check_username(&user_info.username) { - debug!("{}", err); + debug!("Failed to check username {} {err}", user_info.username); return Ok(ApiResponse { json: json!({}), status: StatusCode::BAD_REQUEST, diff --git a/src/hex.rs b/src/hex.rs index 44fb14693..3f6cd5438 100644 --- a/src/hex.rs +++ b/src/hex.rs @@ -73,7 +73,7 @@ pub fn to_lower_hex(bytes: &[u8]) -> String { mod tests { use super::*; - #[std::prelude::v1::test] + #[test] fn test_hex_decode() { assert_eq!(hex_decode("deadf00d"), Ok(vec![0xde, 0xad, 0xf0, 0x0d])); assert_eq!(hex_decode("0Xdeadf00d"), Ok(vec![0xde, 0xad, 0xf0, 0x0d])); diff --git a/src/ldap/error.rs b/src/ldap/error.rs index 185fd33ec..ac790cd33 100644 --- a/src/ldap/error.rs +++ b/src/ldap/error.rs @@ -1,29 +1,28 @@ -use ldap3::LdapError; use std::{error::Error, fmt}; #[derive(Debug)] -pub enum OriLDAPError { +pub enum LdapError { Ldap(String), ObjectNotFound(String), MissingSettings, } -impl fmt::Display for OriLDAPError { +impl fmt::Display for LdapError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - OriLDAPError::Ldap(msg) => write!(f, "LDAP error: {msg}"), - OriLDAPError::ObjectNotFound(msg) => write!(f, "Object not found: {msg}"), - OriLDAPError::MissingSettings => { + LdapError::Ldap(msg) => write!(f, "LDAP error: {msg}"), + LdapError::ObjectNotFound(msg) => write!(f, "Object not found: {msg}"), + LdapError::MissingSettings => { write!(f, "LDAP settings are missing.") } } } } -impl Error for OriLDAPError {} +impl Error for LdapError {} -impl From for OriLDAPError { - fn from(error: LdapError) -> Self { +impl From for LdapError { + fn from(error: ldap3::LdapError) -> Self { Self::Ldap(error.to_string()) } } diff --git a/src/ldap/hash.rs b/src/ldap/hash.rs index 1fd5e624b..33cc69725 100644 --- a/src/ldap/hash.rs +++ b/src/ldap/hash.rs @@ -6,6 +6,8 @@ use sha1::{ Digest, Sha1, }; +use crate::hex::to_lower_hex; + /// Calculate salted SHA1 hash from given password in SSHA password storage scheme. #[must_use] pub fn salted_sha1_hash(password: &str) -> String { @@ -32,14 +34,14 @@ pub fn nthash(password: &str) -> String { .encode_utf16() .flat_map(|c| IntoIterator::into_iter(c.to_le_bytes())) .collect(); - format!("{:x}", Md4::digest(password_utf16_le)) + to_lower_hex(&Md4::digest(password_utf16_le)) } #[cfg(test)] mod tests { use super::*; - #[std::prelude::v1::test] + #[test] fn test_hash() { assert_eq!(nthash("password"), "8846f7eaee8fb117ad06bdd830b7586c"); assert_eq!( diff --git a/src/ldap/mod.rs b/src/ldap/mod.rs index 62459a938..770a0a1a3 100644 --- a/src/ldap/mod.rs +++ b/src/ldap/mod.rs @@ -1,8 +1,11 @@ -use self::{error::OriLDAPError, model::Group}; -use crate::db::{DbPool, Settings, User}; -use ldap3::{drive, Ldap, LdapConnAsync, Mod, Scope, SearchEntry}; use std::collections::HashSet; +use ldap3::{drive, Ldap, LdapConnAsync, Mod, Scope, SearchEntry}; +use sqlx::PgExecutor; + +use self::error::LdapError; +use crate::db::{self, Settings, User}; + pub mod error; pub mod hash; pub mod model; @@ -21,7 +24,7 @@ macro_rules! hashset { }; } -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct LDAPConfig { pub ldap_bind_username: String, pub ldap_group_search_base: String, @@ -39,8 +42,8 @@ impl LDAPConfig { #[must_use] pub fn user_dn(&self, username: &str) -> String { format!( - "{}={},{}", - &self.ldap_username_attr, username, &self.ldap_user_search_base, + "{}={username},{}", + self.ldap_username_attr, self.ldap_user_search_base, ) } @@ -48,44 +51,44 @@ impl LDAPConfig { #[must_use] pub fn group_dn(&self, groupname: &str) -> String { format!( - "{}={},{}", - &self.ldap_groupname_attr, groupname, &self.ldap_group_search_base, + "{}={groupname},{}", + self.ldap_groupname_attr, self.ldap_group_search_base, ) } } impl TryFrom for LDAPConfig { - type Error = OriLDAPError; + type Error = LdapError; - fn try_from(settings: Settings) -> Result { + fn try_from(settings: Settings) -> Result { Ok(Self { ldap_member_attr: settings .ldap_member_attr - .ok_or(OriLDAPError::MissingSettings)?, + .ok_or(LdapError::MissingSettings)?, ldap_group_member_attr: settings .ldap_group_member_attr - .ok_or(OriLDAPError::MissingSettings)?, + .ok_or(LdapError::MissingSettings)?, ldap_groupname_attr: settings .ldap_groupname_attr - .ok_or(OriLDAPError::MissingSettings)?, + .ok_or(LdapError::MissingSettings)?, ldap_username_attr: settings .ldap_username_attr - .ok_or(OriLDAPError::MissingSettings)?, + .ok_or(LdapError::MissingSettings)?, ldap_group_obj_class: settings .ldap_group_obj_class - .ok_or(OriLDAPError::MissingSettings)?, + .ok_or(LdapError::MissingSettings)?, ldap_user_obj_class: settings .ldap_user_obj_class - .ok_or(OriLDAPError::MissingSettings)?, + .ok_or(LdapError::MissingSettings)?, ldap_user_search_base: settings .ldap_user_search_base - .ok_or(OriLDAPError::MissingSettings)?, + .ok_or(LdapError::MissingSettings)?, ldap_bind_username: settings .ldap_bind_username - .ok_or(OriLDAPError::MissingSettings)?, + .ok_or(LdapError::MissingSettings)?, ldap_group_search_base: settings .ldap_group_search_base - .ok_or(OriLDAPError::MissingSettings)?, + .ok_or(LdapError::MissingSettings)?, }) } } @@ -96,18 +99,21 @@ pub struct LDAPConnection { } impl LDAPConnection { - pub async fn create(pool: &DbPool) -> Result { - let settings = Settings::get_settings(pool) + pub async fn create<'e, E>(executor: E) -> Result + where + E: PgExecutor<'e>, + { + let settings = Settings::get_settings(executor) .await - .map_err(|_| OriLDAPError::MissingSettings)?; + .map_err(|_| LdapError::MissingSettings)?; let config = LDAPConfig::try_from(settings.clone())?; - let url = settings.ldap_url.ok_or(OriLDAPError::MissingSettings)?; + let url = settings.ldap_url.ok_or(LdapError::MissingSettings)?; let password = settings .ldap_bind_password - .ok_or(OriLDAPError::MissingSettings)?; + .ok_or(LdapError::MissingSettings)?; let (conn, mut ldap) = LdapConnAsync::new(&url).await?; drive!(conn); - info!("Connected to LDAP: {}", &url); + info!("Connected to LDAP: {url}"); ldap.simple_bind(&config.ldap_bind_username, password.expose_secret()) .await? .success()?; @@ -115,7 +121,7 @@ impl LDAPConnection { } /// Searches LDAP for users. - async fn search_users(&mut self, filter: &str) -> Result, OriLDAPError> { + async fn search_users(&mut self, filter: &str) -> Result, LdapError> { let (rs, _res) = self .ldap .search( @@ -126,38 +132,34 @@ impl LDAPConnection { ) .await? .success()?; - info!("Performed LDAP user search with filter = {}", filter); + info!("Performed LDAP user search with filter = {filter}"); Ok(rs.into_iter().map(SearchEntry::construct).collect()) } /// Searches LDAP for groups. - async fn search_groups(&mut self, filter: &str) -> Result, OriLDAPError> { - let (rs, _res) = self - .ldap - .search( - &self.config.ldap_group_search_base, - Scope::Subtree, - filter, - vec![ - &self.config.ldap_username_attr, - &self.config.ldap_group_member_attr, - ], - ) - .await? - .success()?; - info!("Performed LDAP group search with filter = {}", filter); - Ok(rs.into_iter().map(SearchEntry::construct).collect()) - } + // async fn search_groups(&mut self, filter: &str) -> Result, LdapError> { + // let (rs, _res) = self + // .ldap + // .search( + // &self.config.ldap_group_search_base, + // Scope::Subtree, + // filter, + // vec![ + // &self.config.ldap_username_attr, + // &self.config.ldap_group_member_attr, + // ], + // ) + // .await? + // .success()?; + // info!("Performed LDAP group search with filter = {filter}"); + // Ok(rs.into_iter().map(SearchEntry::construct).collect()) + // } /// Creates LDAP object with specified distinguished name and attributes. - async fn add( - &mut self, - dn: &str, - attrs: Vec<(&str, HashSet<&str>)>, - ) -> Result<(), OriLDAPError> { - debug!("Adding object {}", dn); + async fn add(&mut self, dn: &str, attrs: Vec<(&str, HashSet<&str>)>) -> Result<(), LdapError> { + debug!("Adding object {dn}"); self.ldap.add(dn, attrs).await?.success()?; - info!("Added object {}", dn); + info!("Added object {dn}"); Ok(()) } @@ -167,23 +169,23 @@ impl LDAPConnection { old_dn: &str, new_dn: &str, mods: Vec>, - ) -> Result<(), OriLDAPError> { - debug!("Modifying object {}", old_dn); + ) -> Result<(), LdapError> { + debug!("Modifying LDAP object {old_dn}"); self.ldap.modify(old_dn, mods).await?; if old_dn != new_dn { if let Some((new_rdn, _rest)) = new_dn.split_once(',') { self.ldap.modifydn(old_dn, new_rdn, true, None).await?; } } - info!("Modified object {}", old_dn); + info!("Modified LDAP object {old_dn}"); Ok(()) } /// Deletes LDAP object with specified distinguished name. - pub async fn delete(&mut self, dn: &str) -> Result<(), OriLDAPError> { - debug!("Deleting object {}", dn); + pub async fn delete(&mut self, dn: &str) -> Result<(), LdapError> { + debug!("Deleting LDAP object {dn}"); self.ldap.delete(dn).await?; - info!("Deleted object {}", dn); + info!("Deleted LDAP object {dn}"); Ok(()) } @@ -191,8 +193,8 @@ impl LDAPConnection { pub async fn is_username_available(&mut self, username: &str) -> bool { let users = self .search_users(&format!( - "(&({}={})(|(objectClass={})))", - self.config.ldap_username_attr, username, self.config.ldap_user_obj_class + "(&({}={username})(|(objectClass={})))", + self.config.ldap_username_attr, self.config.ldap_user_obj_class )) .await; match users { @@ -203,26 +205,26 @@ impl LDAPConnection { /// Retrieves user with given username from LDAP. /// TODO: Password must agree with the password stored in LDAP. - pub async fn get_user(&mut self, username: &str, password: &str) -> Result { + pub async fn get_user(&mut self, username: &str, password: &str) -> Result { debug!("Performing LDAP user search: {username}"); let mut entries = self .search_users(&format!( - "(&({}={})(objectClass={}))", - self.config.ldap_username_attr, username, self.config.ldap_user_obj_class + "(&({}={username})(objectClass={}))", + self.config.ldap_username_attr, self.config.ldap_user_obj_class )) .await?; if let Some(entry) = entries.pop() { info!("Performed LDAP user search: {username}"); Ok(User::from_searchentry(&entry, username, password)) } else { - Err(OriLDAPError::ObjectNotFound(format!( + Err(LdapError::ObjectNotFound(format!( "User {username} not found", ))) } } /// Adds user to LDAP. - pub async fn add_user(&mut self, user: &User, password: &str) -> Result<(), OriLDAPError> { + pub async fn add_user(&mut self, user: &User, password: &str) -> Result<(), LdapError> { debug!("Adding LDAP user {}", user.username); let dn = self.config.user_dn(&user.username); let ssha_password = hash::salted_sha1_hash(password); @@ -234,7 +236,7 @@ impl LDAPConnection { } /// Modifies LDAP user. - pub async fn modify_user(&mut self, username: &str, user: &User) -> Result<(), OriLDAPError> { + pub async fn modify_user(&mut self, username: &str, user: &User) -> Result<(), LdapError> { debug!("Modifying user {username}"); let old_dn = self.config.user_dn(username); let new_dn = self.config.user_dn(&user.username); @@ -245,7 +247,7 @@ impl LDAPConnection { } /// Deletes user from LDAP. - pub async fn delete_user(&mut self, username: &str) -> Result<(), OriLDAPError> { + pub async fn delete_user(&mut self, username: &str) -> Result<(), LdapError> { debug!("Deleting user {username}"); let dn = self.config.user_dn(username); self.delete(&dn).await?; @@ -254,11 +256,7 @@ impl LDAPConnection { } /// Changes user password. - pub async fn set_password( - &mut self, - username: &str, - password: &str, - ) -> Result<(), OriLDAPError> { + pub async fn set_password(&mut self, username: &str, password: &str) -> Result<(), LdapError> { debug!("Setting password for user {username}"); let user_dn = self.config.user_dn(username); let ssha_password = hash::salted_sha1_hash(password); @@ -277,47 +275,66 @@ impl LDAPConnection { } /// Retrieves group with given groupname from LDAP. - pub async fn get_group(&mut self, groupname: &str) -> Result { - debug!("Performing LDAP group search: {groupname}"); - let mut enties = self - .search_groups(&format!( - "(&({}={})(objectClass={}))", - self.config.ldap_groupname_attr, groupname, self.config.ldap_group_obj_class - )) - .await?; - if let Some(entry) = enties.pop() { - info!("Performed LDAP user search: {groupname}"); - Ok(Group::from_searchentry(&entry, &self.config)) - } else { - Err(OriLDAPError::ObjectNotFound(format!( - "Group {groupname} not found" - ))) - } - } + // pub async fn get_group(&mut self, groupname: &str) -> Result { + // debug!("Performing LDAP group search: {groupname}"); + // let mut enties = self + // .search_groups(&format!( + // "(&({}={})(objectClass={}))", + // self.config.ldap_groupname_attr, groupname, self.config.ldap_group_obj_class + // )) + // .await?; + // if let Some(entry) = enties.pop() { + // info!("Performed LDAP user search: {groupname}"); + // Ok(Group::from_searchentry(&entry, &self.config)) + // } else { + // Err(LdapError::ObjectNotFound(format!( + // "Group {groupname} not found" + // ))) + // } + // } - /// Lists users satisfying specified criteria - pub async fn get_groups(&mut self) -> Result, OriLDAPError> { - debug!("Performing LDAP group search"); - let mut entries = self - .search_groups(&format!( - "(objectClass={})", - self.config.ldap_group_obj_class - )) - .await?; - let users = entries - .drain(..) - .map(|entry| Group::from_searchentry(&entry, &self.config)) - .collect(); - info!("Performed LDAP group search"); - Ok(users) + /// Modifies LDAP group. + pub async fn modify_group( + &mut self, + groupname: &str, + group: &db::Group, + ) -> Result<(), LdapError> { + debug!("Modifying LDAP group {groupname}"); + let old_dn = self.config.group_dn(groupname); + let new_dn = self.config.group_dn(&group.name); + self.modify( + &old_dn, + &new_dn, + vec![Mod::Replace("cn", hashset![group.name.as_str()])], + ) + .await?; + info!("Modified LDAP group {groupname}"); + Ok(()) } + /// Lists groups satisfying specified criteria + // pub async fn get_groups(&mut self) -> Result, LdapError> { + // debug!("Performing LDAP group search"); + // let mut entries = self + // .search_groups(&format!( + // "(objectClass={})", + // self.config.ldap_group_obj_class + // )) + // .await?; + // let users = entries + // .drain(..) + // .map(|entry| Group::from_searchentry(&entry, &self.config)) + // .collect(); + // info!("Performed LDAP group search"); + // Ok(users) + // } + /// Add user to a group. pub async fn add_user_to_group( &mut self, username: &str, groupname: &str, - ) -> Result<(), OriLDAPError> { + ) -> Result<(), LdapError> { let user_dn = self.config.user_dn(username); let group_dn = self.config.group_dn(groupname); self.modify( @@ -337,7 +354,7 @@ impl LDAPConnection { &mut self, username: &str, groupname: &str, - ) -> Result<(), OriLDAPError> { + ) -> Result<(), LdapError> { let user_dn = self.config.user_dn(username); let group_dn = self.config.group_dn(groupname); self.modify( diff --git a/src/ldap/model.rs b/src/ldap/model.rs index 40494da6d..e16d0ca90 100644 --- a/src/ldap/model.rs +++ b/src/ldap/model.rs @@ -1,8 +1,9 @@ -use crate::{db::User, hashset}; -use ldap3::{Mod, SearchEntry}; use std::collections::HashSet; +use ldap3::{Mod, SearchEntry}; + use super::LDAPConfig; +use crate::{db::User, hashset}; impl User { #[must_use] @@ -67,26 +68,27 @@ impl User { } } -pub struct Group { - pub name: String, - pub members: Vec, -} +// TODO: This struct is similar to `GroupInfo`, so maybe use one? +// pub(crate) struct Group { +// pub name: String, +// pub members: Vec, +// } -impl Group { - #[must_use] - pub fn from_searchentry(entry: &SearchEntry, config: &LDAPConfig) -> Self { - Self { - name: get_value_or_default(entry, &config.ldap_groupname_attr), - members: match entry.attrs.get(&config.ldap_group_member_attr) { - Some(members) => members - .iter() - .filter_map(|member| extract_dn_value(member)) - .collect(), - None => Vec::new(), - }, - } - } -} +// impl Group { +// #[must_use] +// pub(crate) fn from_searchentry(entry: &SearchEntry, config: &LDAPConfig) -> Self { +// Self { +// name: get_value_or_default(entry, &config.ldap_groupname_attr), +// members: match entry.attrs.get(&config.ldap_group_member_attr) { +// Some(members) => members +// .iter() +// .filter_map(|member| extract_dn_value(member)) +// .collect(), +// None => Vec::new(), +// }, +// } +// } +// } fn get_value_or_default(entry: &SearchEntry, key: &str) -> String { match entry.attrs.get(key) { diff --git a/src/ldap/utils.rs b/src/ldap/utils.rs index deebde04e..1fa8e07e5 100644 --- a/src/ldap/utils.rs +++ b/src/ldap/utils.rs @@ -1,19 +1,24 @@ -use super::{error::OriLDAPError, LDAPConnection}; -use crate::db::{DbPool, User}; +use sqlx::PgExecutor; + +use super::{error::LdapError, LDAPConnection}; +use crate::db::{DbPool, Group, User}; pub async fn user_from_ldap( pool: &DbPool, username: &str, password: &str, -) -> Result { +) -> Result { let mut ldap_connection = LDAPConnection::create(pool).await?; let mut user = ldap_connection.get_user(username, password).await?; let _result = user.save(pool).await; // FIXME: do not ignore errors Ok(user) } -pub async fn ldap_add_user(pool: &DbPool, user: &User, password: &str) -> Result<(), OriLDAPError> { - let mut ldap_connection = LDAPConnection::create(pool).await?; +pub async fn ldap_add_user<'e, E>(executor: E, user: &User, password: &str) -> Result<(), LdapError> +where + E: PgExecutor<'e>, +{ + let mut ldap_connection = LDAPConnection::create(executor).await?; match ldap_connection.add_user(user, password).await { Ok(()) => Ok(()), // this user might exist in LDAP, just try to set the password @@ -21,45 +26,72 @@ pub async fn ldap_add_user(pool: &DbPool, user: &User, password: &str) -> Result } } -pub async fn ldap_modify_user( - pool: &DbPool, +pub async fn ldap_modify_user<'e, E>( + executor: E, username: &str, user: &User, -) -> Result<(), OriLDAPError> { - let mut ldap_connection = LDAPConnection::create(pool).await?; +) -> Result<(), LdapError> +where + E: PgExecutor<'e>, +{ + let mut ldap_connection = LDAPConnection::create(executor).await?; ldap_connection.modify_user(username, user).await } -pub async fn ldap_delete_user(pool: &DbPool, username: &str) -> Result<(), OriLDAPError> { - let mut ldap_connection = LDAPConnection::create(pool).await?; +pub async fn ldap_delete_user<'e, E>(executor: E, username: &str) -> Result<(), LdapError> +where + E: PgExecutor<'e>, +{ + let mut ldap_connection = LDAPConnection::create(executor).await?; ldap_connection.delete_user(username).await } -pub async fn ldap_add_user_to_group( - pool: &DbPool, +pub async fn ldap_add_user_to_group<'e, E>( + executor: E, username: &str, groupname: &str, -) -> Result<(), OriLDAPError> { - let mut ldap_connection = LDAPConnection::create(pool).await?; +) -> Result<(), LdapError> +where + E: PgExecutor<'e>, +{ + let mut ldap_connection = LDAPConnection::create(executor).await?; ldap_connection.add_user_to_group(username, groupname).await } -pub async fn ldap_remove_user_from_group( - pool: &DbPool, +pub async fn ldap_remove_user_from_group<'e, E>( + executor: E, username: &str, groupname: &str, -) -> Result<(), OriLDAPError> { - let mut ldap_connection = LDAPConnection::create(pool).await?; +) -> Result<(), LdapError> +where + E: PgExecutor<'e>, +{ + let mut ldap_connection = LDAPConnection::create(executor).await?; ldap_connection .remove_user_from_group(username, groupname) .await } -pub async fn ldap_change_password( - pool: &DbPool, +pub async fn ldap_change_password<'e, E>( + executor: E, username: &str, password: &str, -) -> Result<(), OriLDAPError> { - let mut ldap_connection = LDAPConnection::create(pool).await?; +) -> Result<(), LdapError> +where + E: PgExecutor<'e>, +{ + let mut ldap_connection = LDAPConnection::create(executor).await?; ldap_connection.set_password(username, password).await } + +pub async fn ldap_modify_group<'e, E>( + executor: E, + groupname: &str, + group: &Group, +) -> Result<(), LdapError> +where + E: PgExecutor<'e>, +{ + let mut ldap_connection = LDAPConnection::create(executor).await?; + ldap_connection.modify_group(groupname, group).await +} diff --git a/src/lib.rs b/src/lib.rs index 82fe3bef8..f84ba0bde 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ use axum::{ serve, Extension, Router, }; use handlers::{ + group::{create_group, delete_group, modify_group}, settings::{get_settings_essentials, patch_settings, test_ldap_settings}, user::reset_password, }; @@ -205,6 +206,9 @@ pub fn build_webapp( // group .route("/group", get(list_groups)) .route("/group/:name", get(get_group)) + .route("/group", post(create_group)) + .route("/group/:name", put(modify_group)) + .route("/group/:name", delete(delete_group)) .route("/group/:name", post(add_group_member)) .route("/group/:name/user/:username", delete(remove_group_member)) // mail diff --git a/src/support.rs b/src/support.rs index 5655b7f54..19836167c 100644 --- a/src/support.rs +++ b/src/support.rs @@ -2,16 +2,15 @@ use std::{collections::HashMap, fmt::Display}; use serde::Serialize; use serde_json::{json, value::to_value, Value}; -use sqlx::{Pool, Postgres}; use crate::{ config::DefGuardConfig, - db::{models::device::WireguardNetworkDevice, Settings, User, WireguardNetwork}, + db::{models::device::WireguardNetworkDevice, DbPool, Settings, User, WireguardNetwork}, VERSION, }; /// Unwraps the result returning a JSON representation of value or error -fn unwrap_json(result: Result) -> Value { +fn unwrap_json(result: Result) -> Value { match result { Ok(value) => to_value(value).expect("conversion to JSON failed"), Err(err) => json!({"error": err.to_string()}), @@ -19,7 +18,7 @@ fn unwrap_json(result: Result) -> Value { } /// Dumps all data that could be used for debugging. -pub async fn dump_config(db: &Pool, config: &DefGuardConfig) -> Value { +pub async fn dump_config(db: &DbPool, config: &DefGuardConfig) -> Value { // App settings DB records let settings = match Settings::find_by_id(db, 1).await { Ok(Some(mut settings)) => { @@ -33,7 +32,7 @@ pub async fn dump_config(db: &Pool, config: &DefGuardConfig) -> Value let (networks, devices) = match WireguardNetwork::all(db).await { Ok(networks) => { // Devices for each network - let mut devices = HashMap::::default(); + let mut devices = HashMap::::new(); for network in &networks { let Some(network_id) = network.id else { continue; diff --git a/src/templates.rs b/src/templates.rs index 5c19636aa..0f9cbb1d3 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -410,11 +410,11 @@ mod test { #[test] fn test_enrollment_admin_notification() { let test_user: User = User::new( - "test".into(), - "1234".into(), - "test_last".into(), - "test_first".into(), - "test@example.com".into(), + "test", + Some("1234"), + "test_last", + "test_first", + "test@example.com", Some("99999".into()), ); assert_ok!(enrollment_admin_notification( diff --git a/tests/auth.rs b/tests/auth.rs index 2162bf81a..05088fe62 100644 --- a/tests/auth.rs +++ b/tests/auth.rs @@ -35,10 +35,10 @@ async fn make_client() -> TestClient { let mut wallet = Wallet::new_for_user( client_state.test_user.id.unwrap(), - "0x4aF8803CBAD86BA65ED347a3fbB3fb50e96eDD3e".into(), - "test".into(), + "0x4aF8803CBAD86BA65ED347a3fbB3fb50e96eDD3e", + "test", 5, - String::new(), + "", ); wallet.save(&client_state.pool).await.unwrap(); @@ -50,10 +50,10 @@ async fn make_client_with_db() -> (TestClient, DbPool) { let mut wallet = Wallet::new_for_user( client_state.test_user.id.unwrap(), - "0x4aF8803CBAD86BA65ED347a3fbB3fb50e96eDD3e".into(), - "test".into(), + "0x4aF8803CBAD86BA65ED347a3fbB3fb50e96eDD3e", + "test", 5, - String::new(), + "", ); wallet.save(&client_state.pool).await.unwrap(); @@ -65,26 +65,21 @@ async fn make_client_with_state() -> (TestClient, ClientState) { let mut wallet = Wallet::new_for_user( client_state.test_user.id.unwrap(), - "0x4aF8803CBAD86BA65ED347a3fbB3fb50e96eDD3e".into(), - "test".into(), + "0x4aF8803CBAD86BA65ED347a3fbB3fb50e96eDD3e", + "test", 5, - String::new(), + "", ); wallet.save(&client_state.pool).await.unwrap(); (client, client_state) } -async fn make_client_with_wallet(address: String) -> TestClient { +async fn make_client_with_wallet(address: &str) -> TestClient { let (client, client_state) = make_test_client().await; - let mut wallet = Wallet::new_for_user( - client_state.test_user.id.unwrap(), - address, - "test".into(), - 5, - String::new(), - ); + let mut wallet = + Wallet::new_for_user(client_state.test_user.id.unwrap(), address, "test", 5, ""); wallet.save(&client_state.pool).await.unwrap(); client @@ -94,7 +89,7 @@ async fn make_client_with_wallet(address: String) -> TestClient { async fn test_logout() { let mut client = make_client().await; - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -123,7 +118,7 @@ async fn test_logout() { async fn test_login_bruteforce() { let client = make_client().await; - let invalid_auth = Auth::new("hpotter".into(), "invalid".into()); + let invalid_auth = Auth::new("hpotter", "invalid"); // fail login 5 times in a row for i in 0..6 { @@ -140,7 +135,7 @@ async fn test_login_bruteforce() { async fn test_cannot_enable_mfa() { let client = make_client().await; - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -163,7 +158,7 @@ async fn test_totp() { let client = make_client().await; // login - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -262,7 +257,7 @@ async fn test_totp() { assert_eq!(response.status(), StatusCode::OK); // login again - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); } @@ -284,7 +279,7 @@ async fn test_email_mfa() { assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // login - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -410,7 +405,7 @@ async fn test_email_mfa() { assert_eq!(response.status(), StatusCode::OK); // login again - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); } @@ -423,7 +418,7 @@ async fn test_webauthn() { let origin = Url::parse("http://localhost:8000").unwrap(); // login - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -451,7 +446,7 @@ async fn test_webauthn() { assert_eq!(response.status(), StatusCode::OK); // login again - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::CREATED); @@ -480,7 +475,7 @@ async fn test_webauthn() { assert_eq!(response.status(), StatusCode::OK); // login again - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -500,7 +495,7 @@ async fn test_cannot_skip_otp_by_adding_yubikey() { let client = make_client().await; // login - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -535,7 +530,7 @@ async fn test_cannot_skip_security_key_by_adding_yubikey() { let origin = Url::parse("http://localhost:8000").unwrap(); // login - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -559,7 +554,7 @@ async fn test_cannot_skip_security_key_by_adding_yubikey() { assert_eq!(response.status(), StatusCode::OK); // login again - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::CREATED); @@ -573,7 +568,7 @@ async fn test_mfa_method_is_updated_when_removing_last_webauthn_passkey() { let client = make_client().await; // login - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -651,7 +646,7 @@ async fn test_mfa_method_is_updated_when_removing_last_webauthn_passkey() { assert_eq!(response.status(), StatusCode::OK); // login again - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::CREATED); @@ -780,10 +775,10 @@ async fn test_web3() { let wallet_address = to_lower_hex(addr); // create client - let client = make_client_with_wallet(wallet_address.clone()).await; + let client = make_client_with_wallet(&wallet_address).await; // login - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -806,7 +801,7 @@ async fn test_web3() { assert_eq!(response.status(), StatusCode::OK); // login with wallet - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::CREATED); wallet_login(&client, wallet_address, &secp, secret_key).await; @@ -816,7 +811,7 @@ async fn test_web3() { assert_eq!(response.status(), StatusCode::OK); // login again - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); } @@ -836,7 +831,7 @@ async fn test_re_adding_wallet() { let client = make_client().await; // login - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -881,7 +876,7 @@ async fn test_re_adding_wallet() { assert_eq!(response.status(), StatusCode::OK); // login with wallet - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::CREATED); wallet_login(&client, wallet_address.clone(), &secp, secret_key).await; @@ -898,7 +893,7 @@ async fn test_re_adding_wallet() { assert_eq!(response.status(), StatusCode::OK); // login without MFA - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -941,7 +936,7 @@ async fn test_re_adding_wallet() { assert_eq!(response.status(), StatusCode::OK); // login with wallet - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::CREATED); wallet_login(&client, wallet_address.clone(), &secp, secret_key).await; @@ -954,7 +949,7 @@ async fn test_mfa_method_totp_enabled_mail() { let user_agent_header = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1"; // login - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client .post("/api/v1/auth") .header(USER_AGENT, user_agent_header) @@ -994,7 +989,7 @@ async fn test_new_device_login() { let user_agent_header_android = "Mozilla/5.0 (Linux; Android 7.0; SM-G930VC Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.83 Mobile Safari/537.36"; // login - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client .post("/api/v1/auth") .header(USER_AGENT, user_agent_header_iphone) @@ -1018,7 +1013,7 @@ async fn test_new_device_login() { assert_eq!(response.status(), StatusCode::OK); // login using the same device - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client .post("/api/v1/auth") .header(USER_AGENT, user_agent_header_iphone) @@ -1030,7 +1025,7 @@ async fn test_new_device_login() { assert_err!(mail_rx.try_recv()); // login using a different device - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client .post("/api/v1/auth") .header(USER_AGENT, user_agent_header_android) @@ -1057,7 +1052,7 @@ async fn test_login_ip_headers() { let user_agent_header_iphone = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1"; // Works with X-Forwarded-For header - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client .post("/api/v1/auth") .header(USER_AGENT, user_agent_header_iphone) @@ -1080,7 +1075,7 @@ async fn test_login_ip_headers() { async fn test_session_cookie() { let (client, pool) = make_client_with_db().await; - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); diff --git a/tests/common/client.rs b/tests/common/client.rs index e6a8fe07b..af1374d09 100644 --- a/tests/common/client.rs +++ b/tests/common/client.rs @@ -6,7 +6,7 @@ use reqwest::{ cookie::{Cookie, Jar}, header::{HeaderMap, HeaderName}, redirect::Policy, - Client, StatusCode, Url, + Body, Client, StatusCode, Url, }; use tokio::net::TcpListener; @@ -121,7 +121,7 @@ impl RequestBuilder { } } - pub fn body(mut self, body: impl Into) -> Self { + pub fn body>(mut self, body: B) -> Self { self.builder = self.builder.body(body); self } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index b46517f0b..7fd848b31 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -66,11 +66,11 @@ async fn initialize_users(pool: &DbPool, config: DefGuardConfig) { .unwrap(); let mut test_user = User::new( - "hpotter".into(), + "hpotter", Some("pass123"), - "Potter".into(), - "Harry".into(), - "h.potter@hogwart.edu.uk".into(), + "Potter", + "Harry", + "h.potter@hogwart.edu.uk", None, ); test_user.save(pool).await.unwrap(); diff --git a/tests/enrollment.rs b/tests/enrollment.rs index 5bb40fb66..1561e990b 100644 --- a/tests/enrollment.rs +++ b/tests/enrollment.rs @@ -19,7 +19,7 @@ async fn make_client() -> (TestClient, DbPool) { async fn test_initialize_enrollment() { let (client, pool) = make_client().await; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); diff --git a/tests/forward_auth.rs b/tests/forward_auth.rs index 6c7f9debc..9b939b354 100644 --- a/tests/forward_auth.rs +++ b/tests/forward_auth.rs @@ -10,10 +10,10 @@ async fn make_client() -> TestClient { let mut wallet = Wallet::new_for_user( client_state.test_user.id.unwrap(), - "0x4aF8803CBAD86BA65ED347a3fbB3fb50e96eDD3e".into(), - "test".into(), + "0x4aF8803CBAD86BA65ED347a3fbB3fb50e96eDD3e", + "test", 5, - String::new(), + "", ); wallet.save(&client_state.pool).await.unwrap(); @@ -43,7 +43,7 @@ async fn test_forward_auth() { ); // login - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); diff --git a/tests/group.rs b/tests/group.rs new file mode 100644 index 000000000..dc62ff9d2 --- /dev/null +++ b/tests/group.rs @@ -0,0 +1,103 @@ +mod common; + +use defguard::handlers::{Auth, GroupInfo}; +use reqwest::StatusCode; + +use self::common::make_test_client; + +#[tokio::test] +async fn test_create_group() { + let (client, _) = make_test_client().await; + + // Authorize as an administrator. + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // Create new group. + let data = GroupInfo::new("hogwards", Some(vec!["hpotter".into()])); + let response = client.post("/api/v1/group").json(&data).send().await; + assert_eq!(response.status(), StatusCode::CREATED); + + // Try to create the same group again. + let response = client.post("/api/v1/group").json(&data).send().await; + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + + // Delete the group. + let response = client.delete("/api/v1/group/hogwards").send().await; + assert_eq!(response.status(), StatusCode::OK); + + // Try to delete again. + let response = client.delete("/api/v1/group/hogwards").send().await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn test_modify_group() { + let (client, _) = make_test_client().await; + + // Authorize as an administrator. + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // Create new group. + let data = GroupInfo::new("hogwards", Some(vec!["hpotter".into()])); + let response = client.post("/api/v1/group").json(&data).send().await; + assert_eq!(response.status(), StatusCode::CREATED); + + // Rename group. + let data = GroupInfo::new("gryffindor", None); + let response = client + .put("/api/v1/group/hogwards") + .json(&data) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + + // Try to get the group by its old name. + let response = client.get("/api/v1/group/hogwards").send().await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + + // Get group info. + let response = client.get("/api/v1/group/gryffindor").send().await; + assert_eq!(response.status(), StatusCode::OK); + let group_info: GroupInfo = response.json().await; + assert_eq!(group_info.name, "gryffindor"); +} + +#[tokio::test] +async fn test_modify_group_members() { + let (client, _) = make_test_client().await; + + // Authorize as an administrator. + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // Create new group. + let data = GroupInfo::new("hogwards", Some(vec!["hpotter".into()])); + let response = client.post("/api/v1/group").json(&data).send().await; + assert_eq!(response.status(), StatusCode::CREATED); + + // Get group info. + let response = client.get("/api/v1/group/hogwards").send().await; + assert_eq!(response.status(), StatusCode::OK); + let group_info: GroupInfo = response.json().await; + assert_eq!(group_info.members.unwrap(), vec!["hpotter".to_string()]); + + // Change group members. + let data = GroupInfo::new("hogwards", Some(Vec::new())); + let response = client + .put("/api/v1/group/hogwards") + .json(&data) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + + // Get group info. + let response = client.get("/api/v1/group/hogwards").send().await; + assert_eq!(response.status(), StatusCode::OK); + let group_info: GroupInfo = response.json().await; + assert!(group_info.members.unwrap().is_empty()); +} diff --git a/tests/oauth.rs b/tests/oauth.rs index 7186206da..aebe081d1 100644 --- a/tests/oauth.rs +++ b/tests/oauth.rs @@ -26,7 +26,7 @@ async fn make_client() -> (TestClient, DbPool) { async fn test_authorize() { let (client, pool) = make_client().await; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -165,7 +165,7 @@ async fn test_openid_app_management_access() { let (client, _) = make_client().await; // login as admin - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -267,7 +267,7 @@ async fn test_openid_app_management_access() { let test_app = &apps[0]; // // login as standard user - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); diff --git a/tests/openid.rs b/tests/openid.rs index bd58b222e..260576622 100644 --- a/tests/openid.rs +++ b/tests/openid.rs @@ -49,7 +49,7 @@ pub struct AuthenticationResponse<'r> { async fn test_openid_client() { let client = make_client().await; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -107,7 +107,7 @@ async fn test_openid_client() { #[tokio::test] async fn test_openid_flow() { let client = make_client().await; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); let openid_client = NewOpenIDClient { @@ -252,7 +252,7 @@ async fn test_openid_flow() { assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // log back in - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -414,7 +414,7 @@ async fn test_openid_authorization_code() { .unwrap(); // create OAuth2 client - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); let oauth2client = NewOpenIDClient { @@ -519,7 +519,7 @@ async fn test_openid_authorization_code_with_pkce() { .unwrap(); // create OAuth2 client/application - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); let oauth2client = NewOpenIDClient { @@ -622,7 +622,7 @@ async fn test_openid_flow_new_login_mail() { let mut mail_rx = state.mail_rx; let user_agent_header = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1"; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client .post("/api/v1/auth") .header(USER_AGENT, user_agent_header) diff --git a/tests/settings.rs b/tests/settings.rs index 2c38e4a2c..9d50e95c0 100644 --- a/tests/settings.rs +++ b/tests/settings.rs @@ -17,7 +17,7 @@ async fn make_client() -> (TestClient, ClientState) { #[tokio::test] async fn test_settings() { let (client, _client_state) = make_client().await; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); // get settings diff --git a/tests/user.rs b/tests/user.rs index c28af9d4f..dfcaf57a5 100644 --- a/tests/user.rs +++ b/tests/user.rs @@ -25,15 +25,15 @@ async fn make_client() -> TestClient { async fn test_authenticate() { let client = make_client().await; - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); - let auth = Auth::new("hpotter".into(), "-wrong-".into()); + let auth = Auth::new("hpotter", "-wrong-"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - let auth = Auth::new("adumbledore".into(), "pass123".into()); + let auth = Auth::new("adumbledore", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -42,7 +42,7 @@ async fn test_authenticate() { async fn test_me() { let client = make_client().await; - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -57,7 +57,7 @@ async fn test_me() { async fn test_change_self_password() { let client = make_client().await; - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -106,7 +106,7 @@ async fn test_change_self_password() { let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - let new_auth = Auth::new("hpotter".into(), new_password.into()); + let new_auth = Auth::new("hpotter", new_password); let response = client.post("/api/v1/auth").json(&new_auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -116,7 +116,7 @@ async fn test_change_self_password() { async fn test_change_password() { let client = make_client().await; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -146,7 +146,7 @@ async fn test_change_password() { assert_eq!(response.status(), StatusCode::OK); - let auth = Auth::new("hpotter".into(), new_password.to_string()); + let auth = Auth::new("hpotter", new_password); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -167,7 +167,7 @@ async fn test_list_users() { assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // normal user cannot list users - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -175,7 +175,7 @@ async fn test_list_users() { assert_eq!(response.status(), StatusCode::FORBIDDEN); // admin can list users - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -190,7 +190,7 @@ async fn test_get_user() { let response = client.get("/api/v1/user/hpotter").send().await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -204,7 +204,7 @@ async fn test_username_available() { let client = make_client().await; // standard user cannot check username availability - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -219,7 +219,7 @@ async fn test_username_available() { assert_eq!(response.status(), StatusCode::FORBIDDEN); // log in as admin - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -258,7 +258,7 @@ async fn test_username_available() { async fn test_crud_user() { let client = make_client().await; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -296,7 +296,7 @@ async fn test_crud_user() { async fn test_admin_group() { let client = make_client().await; - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -313,7 +313,7 @@ async fn test_admin_group() { async fn test_wallet() { let client = make_client().await; - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -427,7 +427,7 @@ This request will not trigger a blockchain transaction or cost any gas fees."; async fn test_check_username() { let client = make_client().await; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -466,7 +466,7 @@ async fn test_check_password_strength() { let client = make_client().await; // auth session with admin - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -513,7 +513,7 @@ async fn test_check_password_strength() { #[tokio::test] async fn test_user_unregister_authorized_app() { let client = make_client().await; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); let openid_client = NewOpenIDClient { @@ -579,7 +579,7 @@ async fn test_user_add_device() { let user_agent_header = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1"; // log in as admin - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client .post("/api/v1/auth") .header(USER_AGENT, user_agent_header) @@ -649,7 +649,7 @@ async fn test_user_add_device() { .contains("Device type: iPhone, OS: iOS 17.1, Mobile Safari")); // log in as normal user - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client .post("/api/v1/auth") .header(USER_AGENT, user_agent_header) diff --git a/tests/webhook.rs b/tests/webhook.rs index eab72420c..fe986d582 100644 --- a/tests/webhook.rs +++ b/tests/webhook.rs @@ -14,7 +14,7 @@ async fn make_client() -> TestClient { async fn test_webhooks() { let client = make_client().await; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); diff --git a/tests/wireguard.rs b/tests/wireguard.rs index 1ec6bc110..5d4951d84 100644 --- a/tests/wireguard.rs +++ b/tests/wireguard.rs @@ -28,7 +28,7 @@ async fn test_network() { let mut wg_rx = client_state.wireguard_rx; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -96,7 +96,7 @@ async fn test_device() { let mut wg_rx = client_state.wireguard_rx; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -261,7 +261,7 @@ async fn test_device() { async fn test_device_permissions() { let (client, _) = make_test_client().await; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -323,7 +323,7 @@ async fn test_device_permissions() { assert_eq!(response.status(), StatusCode::CREATED); // normal user cannot add devices for other users or import multiple devices - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -385,7 +385,7 @@ async fn test_device_permissions() { assert_eq!(user_devices.len(), 3); // admin can list devices of other users - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -406,7 +406,7 @@ async fn test_device_pubkey() { let mut wg_rx = client_state.wireguard_rx; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); diff --git a/tests/wireguard_network_allowed_groups.rs b/tests/wireguard_network_allowed_groups.rs index 8c34295aa..bf0a48569 100644 --- a/tests/wireguard_network_allowed_groups.rs +++ b/tests/wireguard_network_allowed_groups.rs @@ -53,11 +53,11 @@ async fn setup_test_users(pool: &DbPool) -> (Vec, Vec) { // standard user in other, non-allowed group let mut other_user = User::new( - "ssnape".into(), + "ssnape", Some("pass123"), - "Snape".into(), - "Severus".into(), - "s.snape@hogwart.edu.uk".into(), + "Snape", + "Severus", + "s.snape@hogwart.edu.uk", None, ); other_user.save(pool).await.unwrap(); @@ -76,11 +76,11 @@ async fn setup_test_users(pool: &DbPool) -> (Vec, Vec) { // standard user in no groups let mut non_group_user = User::new( - "dobby".into(), + "dobby", Some("pass123"), - "Elf".into(), - "Dobby".into(), - "dobby@hogwart.edu.uk".into(), + "Elf", + "Dobby", + "dobby@hogwart.edu.uk", None, ); non_group_user.save(pool).await.unwrap(); @@ -103,7 +103,7 @@ async fn test_create_new_network() { let mut wg_rx = client_state.wireguard_rx; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -142,7 +142,7 @@ async fn test_modify_network() { let mut wg_rx = client_state.wireguard_rx; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -276,7 +276,7 @@ async fn test_import_network_existing_devices() { let mut wg_rx = client_state.wireguard_rx; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -364,7 +364,7 @@ async fn test_import_mapping_devices() { let mut wg_rx = client_state.wireguard_rx; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -472,7 +472,7 @@ async fn test_modify_user() { let mut wg_rx = client_state.wireguard_rx; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); diff --git a/tests/wireguard_network_import.rs b/tests/wireguard_network_import.rs index cedaa0587..ac80028c5 100644 --- a/tests/wireguard_network_import.rs +++ b/tests/wireguard_network_import.rs @@ -79,7 +79,7 @@ async fn test_config_import() { let mut wg_rx = client_state.wireguard_rx; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -228,7 +228,7 @@ async fn test_config_import_missing_interface() { "; let (client, _) = make_test_client().await; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -262,7 +262,7 @@ async fn test_config_import_invalid_key() { "; let (client, _) = make_test_client().await; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -321,7 +321,7 @@ async fn test_config_import_invalid_ip() { "; let (client, _) = make_test_client().await; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -354,7 +354,7 @@ async fn test_config_import_nonadmin() { PersistentKeepalive = 300 "; let (client, _) = make_test_client().await; - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); diff --git a/tests/wireguard_network_stats.rs b/tests/wireguard_network_stats.rs index 4df73325b..2d909bd66 100644 --- a/tests/wireguard_network_stats.rs +++ b/tests/wireguard_network_stats.rs @@ -32,7 +32,7 @@ async fn test_stats() { let (client, client_state) = make_test_client().await; let pool = client_state.pool; - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = &client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); diff --git a/tests/worker.rs b/tests/worker.rs index 844bd38c0..702e3bb32 100644 --- a/tests/worker.rs +++ b/tests/worker.rs @@ -29,7 +29,7 @@ async fn test_scheduling_worker_jobs() { }; // normal user can only provision keys for themselves - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -57,7 +57,7 @@ async fn test_scheduling_worker_jobs() { assert_eq!(response.status(), StatusCode::FORBIDDEN); // admin user can provision keys for other users - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -146,7 +146,7 @@ async fn test_scheduling_worker_jobs() { assert_eq!(response.status(), StatusCode::OK); // // normal user can only fetch status of their own jobs - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -182,7 +182,7 @@ async fn test_worker_management_permissions() { } // admin can create worker tokens - let auth = Auth::new("admin".into(), "pass123".into()); + let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -203,7 +203,7 @@ async fn test_worker_management_permissions() { assert_eq!(workers.len(), 2); // normal user cannot create worker tokens - let auth = Auth::new("hpotter".into(), "pass123".into()); + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); From 5fa85806bda0a122c94a38014f4502f582b832d1 Mon Sep 17 00:00:00 2001 From: Maciek Date: Thu, 21 Dec 2023 14:31:57 +0100 Subject: [PATCH 09/26] feat: update location settings (#487) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update dependencies * add preshared key to device model * update network struct * fix tests * update query data * update API handlers * update API handlers * update frontend * fix tests * add preshared key to peer message * change typing * update protos * update query data * update dependencies * change default value * update checkbox label * add checkbox spacing * Update web/src/i18n/pl/index.ts Co-authored-by: Adam * review fixes * update query data --------- Co-authored-by: Maciej Wójcik Co-authored-by: Adam --- ...df4c4cbe70d3dd64b503b387a8cf948fab2c.json} | 4 +- ...87a85eb04e2b0eec2d9bc63e8fee6abda27e.json} | 4 +- ...73a044397c5cb6bb9654dc5df420bf00d2955.json | 24 +++ ...40442a7ed65c986eef4e79e9d12aeb32edef.json} | 24 ++- ...0fe9162b8ae22444b29379c85fe0b9ca01799.json | 14 -- ...4f48275d7b1d588869791a6741fa11be38a5.json} | 4 +- ...8883b366b46937b94f25a3788604c72da0ecc.json | 14 ++ ...aeeaeb3b13a524f503b21414d1394173eb24.json} | 24 ++- ...b3844f0cedcc82d465c0cc4baeec552e8c98.json} | 12 +- ...faf0d7a9d5f523db44f7da17ca1bf31d4576.json} | 12 +- ...3e209cf075f9b128ed33908c73cd74297d81.json} | 9 +- ...809038cdddfb44133b97b5ba7392573239539.json | 29 +++ ...4622fc71857d0d459588b4f3934307611aee7.json | 29 --- ...14f2c9b6a37f25f0696d83078dae2c9f87c5f.json | 14 ++ ...c30ae1cd9dce74ec826ed875c39cc05f04f8.json} | 4 +- ...88788ae61d74d3e86bbe123b1d41c8e19af4.json} | 13 +- ...8522209f36956f65bf5a126682d5477db425e.json | 16 -- ...f85dbcf3fb1a202611480af6a7b9e28d864f.json} | 24 ++- ...c17f6a3a3b1bda1412df2b30bf2f770d70e8f.json | 14 ++ ...fbf99606684a681883600a7aca610c7c6e8b.json} | 7 +- ...753077bf164f59900f29d54ff35bc294c9b4.json} | 12 +- ...d3f18033b11db0daf4133c2abaf8823a8a86a.json | 15 ++ ...4fd82b538b4ed436ad714408a72657e8a9e2.json} | 4 +- ...6c3782f2bc80e3f2e0f015bea23717604709f.json | 34 ++++ ...5b3ca39aab9fb176fe970b9309d6371072e95.json | 14 -- ...7a9277805ca9c7a22faeb66230ce0fa87ea9.json} | 4 +- ...65f5c30566de00d9b96d8a70e3f08a31c9dc2.json | 24 --- ...05c6de14f5f73248769f9faad9c28c411fad8.json | 14 -- ...ed6027dd5c770a1330689a029cc31f731bf33.json | 16 ++ ...19b1db6a855e917cdaf619a9461ad89b96917.json | 15 -- ...19c59d9a535dc70d226de618e549c203e88b.json} | 13 +- ...2d0faa187d2bb59bedd0c66d1eae866922901.json | 59 ------ ...5c03a8f5b70a8813849956e9d32b5da5a407f.json | 16 ++ ...06ff6f0efe4fd55a198682d52d2edc7de24d.json} | 7 +- ...adf33061fbb6cf7e1e7c58d5bee1692351e2.json} | 12 +- ...001ee45dea8486fb02e8c375d7b513ac0d6d.json} | 4 +- ...845278cca5ceb21718f37082728fa052c33c.json} | 12 +- ...00203ca943272969aff90da38088f9f55097.json} | 12 +- ...d87781e7fa62a39998d54bf79d736ed7a8827.json | 28 --- ...956773817874aeccc8c9f73527018e5d10cb.json} | 12 +- ...571325c844ae1f4d222193bebd61a1859ee2.json} | 12 +- ...58601de53af70dd28f69b84caca6e36cb8956.json | 16 -- ...d4ee0967c7dc68a04ec930f863d8f3f34c14a.json | 59 ++++++ ...34cddfee21a48f981c3ca22a6e064c18a769.json} | 9 +- ...90e332d85f9162249ef56c98edc2582c288a.json} | 4 +- Cargo.lock | 140 +++++++------- .../20231220103051_add_preshared_key.down.sql | 1 + .../20231220103051_add_preshared_key.up.sql | 1 + ...20112404_update_location_settings.down.sql | 3 + ...1220112404_update_location_settings.up.sql | 3 + proto | 2 +- src/db/models/device.rs | 115 ++++++------ src/db/models/enrollment.rs | 14 +- src/db/models/user.rs | 12 +- src/db/models/wireguard.rs | 171 +++++++++--------- src/grpc/enrollment.rs | 2 +- src/grpc/gateway.rs | 32 +++- src/handlers/wireguard.rs | 12 +- src/lib.rs | 35 ++-- src/wg_config.rs | 11 +- src/wireguard_stats_purge.rs | 13 +- tests/user.rs | 3 + tests/wireguard.rs | 14 +- tests/wireguard_network_allowed_groups.rs | 25 +++ tests/wireguard_network_import.rs | 13 +- tests/wireguard_network_stats.rs | 3 + web/src/i18n/en/index.ts | 9 + web/src/i18n/i18n-types.ts | 36 ++++ web/src/i18n/pl/index.ts | 9 + .../NetworkEditForm/NetworkEditForm.tsx | 31 ++++ web/src/pages/network/style.scss | 4 + .../WizardNetworkConfiguration.tsx | 25 +++ .../WizardNetworkConfiguration/style.scss | 4 + web/src/pages/wizard/hooks/useWizardStore.ts | 6 + web/src/shared/types.ts | 3 + 75 files changed, 915 insertions(+), 559 deletions(-) rename .sqlx/{query-8880011a898ca2b97b0cfdbf027509d4855f46e4871caaa1fc95f2f612efcc4b.json => query-01ef7ff2c9dc9bbaba01b9e6bc4adf4c4cbe70d3dd64b503b387a8cf948fab2c.json} (77%) rename .sqlx/{query-e7e944f6bce4dce8cd58889dca8e38ceab97af014b51bfa24933e296e803effc.json => query-035360e0dd260aece5c89979b28e87a85eb04e2b0eec2d9bc63e8fee6abda27e.json} (53%) create mode 100644 .sqlx/query-06d1ac982dc99c5dae010089cfb73a044397c5cb6bb9654dc5df420bf00d2955.json rename .sqlx/{query-6b07d6ea56bcded73038dfd59cff3d7d15049caf771aec6b0fcd42bea7169998.json => query-0c5df6263cfa9e2bdf273c51993d40442a7ed65c986eef4e79e9d12aeb32edef.json} (69%) delete mode 100644 .sqlx/query-0fa037b89c50e40ad95ae203cf70fe9162b8ae22444b29379c85fe0b9ca01799.json rename .sqlx/{query-501e4426106d4972d7bd6d769bb6dd239be2168c4afdfc618de4c7facaad67a3.json => query-20dda55cfcf38db23d5553b65a9e4f48275d7b1d588869791a6741fa11be38a5.json} (70%) create mode 100644 .sqlx/query-26d95a39c9d11c1fcd5d35f4f0d8883b366b46937b94f25a3788604c72da0ecc.json rename .sqlx/{query-b362d79ac6b116c97f7489cbfc5801c7f94ba91677af351ef235755bf804e1f7.json => query-29809d24769e1c6cb572c666fd0caeeaeb3b13a524f503b21414d1394173eb24.json} (69%) rename .sqlx/{query-eb6dee5462657ac5ce0ecf31d1477ce7cc874d9ad8b3119977168f601b3e8072.json => query-2fadd2533949c86b10d2fb2bcac3b3844f0cedcc82d465c0cc4baeec552e8c98.json} (74%) rename .sqlx/{query-33a1e2f1904757c775d389fa99d67916b7b220d6aa1fe8bb6690f85ed1cd5666.json => query-35a4ec60785870b07495258a3ea5faf0d7a9d5f523db44f7da17ca1bf31d4576.json} (70%) rename .sqlx/{query-73ab0d514bbce0a69e1b22d9f5a2a58be7427882b368eef4d45529e9e85d885c.json => query-38f3ad2dc19d222226b85a10d6c83e209cf075f9b128ed33908c73cd74297d81.json} (65%) create mode 100644 .sqlx/query-3d5d8b6f640435a1986561c73cd809038cdddfb44133b97b5ba7392573239539.json delete mode 100644 .sqlx/query-4183b4f169126bcc511ff76c7554622fc71857d0d459588b4f3934307611aee7.json create mode 100644 .sqlx/query-4218fd109bd4a17b2a4551cfe0d14f2c9b6a37f25f0696d83078dae2c9f87c5f.json rename .sqlx/{query-05e3fce0c51856f6033195c7523a2b5fd1c2a7968a7abd27394422a7c93e8815.json => query-42ccaa218d47638ff39d9006095ac30ae1cd9dce74ec826ed875c39cc05f04f8.json} (54%) rename .sqlx/{query-9e3c1c1f52bc1a576012c57c7472f9f7601e1128b15fc0ecc7676cd5aa01c88c.json => query-43fe6ac793c1a59664383338f83488788ae61d74d3e86bbe123b1d41c8e19af4.json} (74%) delete mode 100644 .sqlx/query-45545d3190c14bb3e9528ab3d3a8522209f36956f65bf5a126682d5477db425e.json rename .sqlx/{query-bde600b9d9448806df37628d305a09b93f4d1217dfb1141a32be11673d76dd1f.json => query-45a53587f6d8bee3de695b846d5bf85dbcf3fb1a202611480af6a7b9e28d864f.json} (68%) create mode 100644 .sqlx/query-48966c8c5f8999dad107a7c49d7c17f6a3a3b1bda1412df2b30bf2f770d70e8f.json rename .sqlx/{query-9213729a9a1ce371ef77898f5792d914a67400cb4cce9a8bf86227ffe7d42eda.json => query-55aac498ce61c0b42f4895add96dfbf99606684a681883600a7aca610c7c6e8b.json} (64%) rename .sqlx/{query-06f847d99d452dafd10f4a6bec309c330d76eb5d0a34012bd75395b13e3ff659.json => query-5d629b503d4d9f76b4e9b8981139753077bf164f59900f29d54ff35bc294c9b4.json} (74%) create mode 100644 .sqlx/query-6eb3f257ef7762c8633885b2b49d3f18033b11db0daf4133c2abaf8823a8a86a.json rename .sqlx/{query-18e96b44b04c6ed00424ac497889e5952c877f1d14216f5b083fd7fa7c927bb7.json => query-7381a3487396a1d7e562e21bb2804fd82b538b4ed436ad714408a72657e8a9e2.json} (74%) create mode 100644 .sqlx/query-7878106cb8e9cc315310cf7c0946c3782f2bc80e3f2e0f015bea23717604709f.json delete mode 100644 .sqlx/query-7b7092bd5fe377c8b28862721b05b3ca39aab9fb176fe970b9309d6371072e95.json rename .sqlx/{query-66d2e2f3156ce802a0018bea5aa0718b2ce599d44de6382e854174053a639f5a.json => query-8ae08ad03e745e8f3d65707bb8637a9277805ca9c7a22faeb66230ce0fa87ea9.json} (73%) delete mode 100644 .sqlx/query-961bd7d2e2cc98e2b968ccb22e065f5c30566de00d9b96d8a70e3f08a31c9dc2.json delete mode 100644 .sqlx/query-a2a9f1e0388ce6705deba02473a05c6de14f5f73248769f9faad9c28c411fad8.json create mode 100644 .sqlx/query-a5aa90dcb89a4e7f1908171b4e4ed6027dd5c770a1330689a029cc31f731bf33.json delete mode 100644 .sqlx/query-aa8bbafc145ad47aaac28e9863c19b1db6a855e917cdaf619a9461ad89b96917.json rename .sqlx/{query-929535564469ee1701f83a1489301d0d652d5452369a2dce933f032cdb7092e2.json => query-b1b93736fd91a6445bb072e5bbe619c59d9a535dc70d226de618e549c203e88b.json} (62%) delete mode 100644 .sqlx/query-c0e30b2a0b711ea77d88a6dc9c92d0faa187d2bb59bedd0c66d1eae866922901.json create mode 100644 .sqlx/query-c3566263419aab9ba0172d59b105c03a8f5b70a8813849956e9d32b5da5a407f.json rename .sqlx/{query-6dfb9442da967755230ab2b7c5baf8eec06d64af5886e840bea1b32d2be10ca0.json => query-c91e2bc58d379bbc7ed29d008f1806ff6f0efe4fd55a198682d52d2edc7de24d.json} (57%) rename .sqlx/{query-fd92ab2977c9c170e2254f13bd99fcb569ef4b44029783648b0613ca8db2a393.json => query-ca2755ead4852207d09bcd46ffcaadf33061fbb6cf7e1e7c58d5bee1692351e2.json} (67%) rename .sqlx/{query-2da415f5b71186fcfbe7fdf3794606eb73b47c4ec120aaef50aff6c7d075dbdd.json => query-cbe6cdf1b9dd1d13bbb460726e33001ee45dea8486fb02e8c375d7b513ac0d6d.json} (82%) rename .sqlx/{query-c64f247f81e332689e35c224656847b246deb6f92a5790a4fdd0d5733defbb57.json => query-d069191fedf7628f2d62be632911845278cca5ceb21718f37082728fa052c33c.json} (73%) rename .sqlx/{query-0b53e81d6f1ce3c3a68c871e7a4b9e41db58b112d12281516e10421d46fbbd94.json => query-d5761193c11c9029731464a6824600203ca943272969aff90da38088f9f55097.json} (56%) delete mode 100644 .sqlx/query-d8b3cbc7317bfdee111b80accd5d87781e7fa62a39998d54bf79d736ed7a8827.json rename .sqlx/{query-865131a7e440c7e5f297cb8620487fe0cf641f481175adaf22982d1f983d362e.json => query-dba6275f3895b8725bdce57c4617956773817874aeccc8c9f73527018e5d10cb.json} (56%) rename .sqlx/{query-439bef62ccc846cccca2c6979e5698a7aaba2beb19645b720eefa73e4dcac942.json => query-ea731a5dd05cf3c68a4f479ec978571325c844ae1f4d222193bebd61a1859ee2.json} (69%) delete mode 100644 .sqlx/query-ee6ed3df25517a6fedf6981166e58601de53af70dd28f69b84caca6e36cb8956.json create mode 100644 .sqlx/query-efe773da199984a169b636cafabd4ee0967c7dc68a04ec930f863d8f3f34c14a.json rename .sqlx/{query-e19b29d2e52ad2a2a5c20ce205d5c949d02219fd252d78f7fffa202cc804b4c2.json => query-f222309aff56e5182091655c0a6c34cddfee21a48f981c3ca22a6e064c18a769.json} (62%) rename .sqlx/{query-d3b425017872becff45f39513c9e09b3d3ba8fc86e97c7f1ff21597a884af06d.json => query-fbfe27851d858408d81529e6e77190e332d85f9162249ef56c98edc2582c288a.json} (58%) create mode 100644 migrations/20231220103051_add_preshared_key.down.sql create mode 100644 migrations/20231220103051_add_preshared_key.up.sql create mode 100644 migrations/20231220112404_update_location_settings.down.sql create mode 100644 migrations/20231220112404_update_location_settings.up.sql diff --git a/.sqlx/query-8880011a898ca2b97b0cfdbf027509d4855f46e4871caaa1fc95f2f612efcc4b.json b/.sqlx/query-01ef7ff2c9dc9bbaba01b9e6bc4adf4c4cbe70d3dd64b503b387a8cf948fab2c.json similarity index 77% rename from .sqlx/query-8880011a898ca2b97b0cfdbf027509d4855f46e4871caaa1fc95f2f612efcc4b.json rename to .sqlx/query-01ef7ff2c9dc9bbaba01b9e6bc4adf4c4cbe70d3dd64b503b387a8cf948fab2c.json index bb241b225..621919ad8 100644 --- a/.sqlx/query-8880011a898ca2b97b0cfdbf027509d4855f46e4871caaa1fc95f2f612efcc4b.json +++ b/.sqlx/query-01ef7ff2c9dc9bbaba01b9e6bc4adf4c4cbe70d3dd64b503b387a8cf948fab2c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\"\n FROM wireguard_network_device WHERE device_id = $1", + "query": "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\" FROM wireguard_network_device WHERE device_id = $1", "describe": { "columns": [ { @@ -30,5 +30,5 @@ false ] }, - "hash": "8880011a898ca2b97b0cfdbf027509d4855f46e4871caaa1fc95f2f612efcc4b" + "hash": "01ef7ff2c9dc9bbaba01b9e6bc4adf4c4cbe70d3dd64b503b387a8cf948fab2c" } diff --git a/.sqlx/query-e7e944f6bce4dce8cd58889dca8e38ceab97af014b51bfa24933e296e803effc.json b/.sqlx/query-035360e0dd260aece5c89979b28e87a85eb04e2b0eec2d9bc63e8fee6abda27e.json similarity index 53% rename from .sqlx/query-e7e944f6bce4dce8cd58889dca8e38ceab97af014b51bfa24933e296e803effc.json rename to .sqlx/query-035360e0dd260aece5c89979b28e87a85eb04e2b0eec2d9bc63e8fee6abda27e.json index eb7659098..30fd8c341 100644 --- a/.sqlx/query-e7e944f6bce4dce8cd58889dca8e38ceab97af014b51bfa24933e296e803effc.json +++ b/.sqlx/query-035360e0dd260aece5c89979b28e87a85eb04e2b0eec2d9bc63e8fee6abda27e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT wireguard_ip\n FROM wireguard_network_device\n WHERE device_id = $1 AND wireguard_network_id = $2\n ", + "query": "SELECT wireguard_ip FROM wireguard_network_device WHERE device_id = $1 AND wireguard_network_id = $2", "describe": { "columns": [ { @@ -19,5 +19,5 @@ false ] }, - "hash": "e7e944f6bce4dce8cd58889dca8e38ceab97af014b51bfa24933e296e803effc" + "hash": "035360e0dd260aece5c89979b28e87a85eb04e2b0eec2d9bc63e8fee6abda27e" } diff --git a/.sqlx/query-06d1ac982dc99c5dae010089cfb73a044397c5cb6bb9654dc5df420bf00d2955.json b/.sqlx/query-06d1ac982dc99c5dae010089cfb73a044397c5cb6bb9654dc5df420bf00d2955.json new file mode 100644 index 000000000..e2fcfb80f --- /dev/null +++ b/.sqlx/query-06d1ac982dc99c5dae010089cfb73a044397c5cb6bb9654dc5df420bf00d2955.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT latest_handshake \"latest_handshake: NaiveDateTime\" FROM wireguard_peer_stats_view WHERE device_id = $1 AND latest_handshake IS NOT NULL AND (latest_handshake_diff > $2 * interval '1 minute' OR latest_handshake_diff IS NULL) AND network = $3 ORDER BY collected_at DESC LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "latest_handshake: NaiveDateTime", + "type_info": "Timestamp" + } + ], + "parameters": { + "Left": [ + "Int8", + "Float8", + "Int8" + ] + }, + "nullable": [ + true + ] + }, + "hash": "06d1ac982dc99c5dae010089cfb73a044397c5cb6bb9654dc5df420bf00d2955" +} diff --git a/.sqlx/query-6b07d6ea56bcded73038dfd59cff3d7d15049caf771aec6b0fcd42bea7169998.json b/.sqlx/query-0c5df6263cfa9e2bdf273c51993d40442a7ed65c986eef4e79e9d12aeb32edef.json similarity index 69% rename from .sqlx/query-6b07d6ea56bcded73038dfd59cff3d7d15049caf771aec6b0fcd42bea7169998.json rename to .sqlx/query-0c5df6263cfa9e2bdf273c51993d40442a7ed65c986eef4e79e9d12aeb32edef.json index 1794b02be..3116a4814 100644 --- a/.sqlx/query-6b07d6ea56bcded73038dfd59cff3d7d15049caf771aec6b0fcd42bea7169998.json +++ b/.sqlx/query-0c5df6263cfa9e2bdf273c51993d40442a7ed65c986eef4e79e9d12aeb32edef.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", \"name\",\"address\" \"address: _\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\" \"allowed_ips: _\",\"connected_at\" FROM \"wireguard_network\"", + "query": "SELECT id \"id?\", \"name\",\"address\" \"address: _\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\" \"allowed_ips: _\",\"connected_at\",\"mfa_enabled\",\"keepalive_interval\",\"peer_disconnect_threshold\" FROM \"wireguard_network\"", "describe": { "columns": [ { @@ -52,6 +52,21 @@ "ordinal": 9, "name": "connected_at", "type_info": "Timestamp" + }, + { + "ordinal": 10, + "name": "mfa_enabled", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "keepalive_interval", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "peer_disconnect_threshold", + "type_info": "Int4" } ], "parameters": { @@ -67,8 +82,11 @@ false, true, false, - true + true, + false, + false, + false ] }, - "hash": "6b07d6ea56bcded73038dfd59cff3d7d15049caf771aec6b0fcd42bea7169998" + "hash": "0c5df6263cfa9e2bdf273c51993d40442a7ed65c986eef4e79e9d12aeb32edef" } diff --git a/.sqlx/query-0fa037b89c50e40ad95ae203cf70fe9162b8ae22444b29379c85fe0b9ca01799.json b/.sqlx/query-0fa037b89c50e40ad95ae203cf70fe9162b8ae22444b29379c85fe0b9ca01799.json deleted file mode 100644 index c2c883025..000000000 --- a/.sqlx/query-0fa037b89c50e40ad95ae203cf70fe9162b8ae22444b29379c85fe0b9ca01799.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM token\n WHERE user_id = $1\n AND used_at IS NULL", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - }, - "hash": "0fa037b89c50e40ad95ae203cf70fe9162b8ae22444b29379c85fe0b9ca01799" -} diff --git a/.sqlx/query-501e4426106d4972d7bd6d769bb6dd239be2168c4afdfc618de4c7facaad67a3.json b/.sqlx/query-20dda55cfcf38db23d5553b65a9e4f48275d7b1d588869791a6741fa11be38a5.json similarity index 70% rename from .sqlx/query-501e4426106d4972d7bd6d769bb6dd239be2168c4afdfc618de4c7facaad67a3.json rename to .sqlx/query-20dda55cfcf38db23d5553b65a9e4f48275d7b1d588869791a6741fa11be38a5.json index 5c396350e..4b6e6f131 100644 --- a/.sqlx/query-501e4426106d4972d7bd6d769bb6dd239be2168c4afdfc618de4c7facaad67a3.json +++ b/.sqlx/query-20dda55cfcf38db23d5553b65a9e4f48275d7b1d588869791a6741fa11be38a5.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT id \"id?\", device_id \"device_id!\", collected_at \"collected_at!\", network \"network!\",\n endpoint, upload \"upload!\", download \"download!\", latest_handshake \"latest_handshake!\", allowed_ips\n FROM wireguard_peer_stats\n WHERE device_id = $1 AND network = $2\n ORDER BY collected_at DESC\n LIMIT 1\n ", + "query": "SELECT id \"id?\", device_id \"device_id!\", collected_at \"collected_at!\", network \"network!\", endpoint, upload \"upload!\", download \"download!\", latest_handshake \"latest_handshake!\", allowed_ips FROM wireguard_peer_stats WHERE device_id = $1 AND network = $2 ORDER BY collected_at DESC LIMIT 1", "describe": { "columns": [ { @@ -67,5 +67,5 @@ true ] }, - "hash": "501e4426106d4972d7bd6d769bb6dd239be2168c4afdfc618de4c7facaad67a3" + "hash": "20dda55cfcf38db23d5553b65a9e4f48275d7b1d588869791a6741fa11be38a5" } diff --git a/.sqlx/query-26d95a39c9d11c1fcd5d35f4f0d8883b366b46937b94f25a3788604c72da0ecc.json b/.sqlx/query-26d95a39c9d11c1fcd5d35f4f0d8883b366b46937b94f25a3788604c72da0ecc.json new file mode 100644 index 000000000..9076e114d --- /dev/null +++ b/.sqlx/query-26d95a39c9d11c1fcd5d35f4f0d8883b366b46937b94f25a3788604c72da0ecc.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM token WHERE user_id = $1 AND token_type = 'PASSWORD_RESET' AND used_at IS NULL", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "26d95a39c9d11c1fcd5d35f4f0d8883b366b46937b94f25a3788604c72da0ecc" +} diff --git a/.sqlx/query-b362d79ac6b116c97f7489cbfc5801c7f94ba91677af351ef235755bf804e1f7.json b/.sqlx/query-29809d24769e1c6cb572c666fd0caeeaeb3b13a524f503b21414d1394173eb24.json similarity index 69% rename from .sqlx/query-b362d79ac6b116c97f7489cbfc5801c7f94ba91677af351ef235755bf804e1f7.json rename to .sqlx/query-29809d24769e1c6cb572c666fd0caeeaeb3b13a524f503b21414d1394173eb24.json index 7634f9654..ffaf6c210 100644 --- a/.sqlx/query-b362d79ac6b116c97f7489cbfc5801c7f94ba91677af351ef235755bf804e1f7.json +++ b/.sqlx/query-29809d24769e1c6cb572c666fd0caeeaeb3b13a524f503b21414d1394173eb24.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", \"name\",\"address\" \"address: _\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\" \"allowed_ips: _\",\"connected_at\" FROM \"wireguard_network\" WHERE id = $1", + "query": "SELECT id \"id?\", \"name\",\"address\" \"address: _\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\" \"allowed_ips: _\",\"connected_at\",\"mfa_enabled\",\"keepalive_interval\",\"peer_disconnect_threshold\" FROM \"wireguard_network\" WHERE id = $1", "describe": { "columns": [ { @@ -52,6 +52,21 @@ "ordinal": 9, "name": "connected_at", "type_info": "Timestamp" + }, + { + "ordinal": 10, + "name": "mfa_enabled", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "keepalive_interval", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "peer_disconnect_threshold", + "type_info": "Int4" } ], "parameters": { @@ -69,8 +84,11 @@ false, true, false, - true + true, + false, + false, + false ] }, - "hash": "b362d79ac6b116c97f7489cbfc5801c7f94ba91677af351ef235755bf804e1f7" + "hash": "29809d24769e1c6cb572c666fd0caeeaeb3b13a524f503b21414d1394173eb24" } diff --git a/.sqlx/query-eb6dee5462657ac5ce0ecf31d1477ce7cc874d9ad8b3119977168f601b3e8072.json b/.sqlx/query-2fadd2533949c86b10d2fb2bcac3b3844f0cedcc82d465c0cc4baeec552e8c98.json similarity index 74% rename from .sqlx/query-eb6dee5462657ac5ce0ecf31d1477ce7cc874d9ad8b3119977168f601b3e8072.json rename to .sqlx/query-2fadd2533949c86b10d2fb2bcac3b3844f0cedcc82d465c0cc4baeec552e8c98.json index b0e4fe52a..878e77680 100644 --- a/.sqlx/query-eb6dee5462657ac5ce0ecf31d1477ce7cc874d9ad8b3119977168f601b3e8072.json +++ b/.sqlx/query-2fadd2533949c86b10d2fb2bcac3b3844f0cedcc82d465c0cc4baeec552e8c98.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", name, wireguard_pubkey, user_id, created FROM device WHERE wireguard_pubkey = $1", + "query": "SELECT id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key FROM device WHERE wireguard_pubkey = $1", "describe": { "columns": [ { @@ -27,6 +27,11 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -39,8 +44,9 @@ false, false, false, - false + false, + true ] }, - "hash": "eb6dee5462657ac5ce0ecf31d1477ce7cc874d9ad8b3119977168f601b3e8072" + "hash": "2fadd2533949c86b10d2fb2bcac3b3844f0cedcc82d465c0cc4baeec552e8c98" } diff --git a/.sqlx/query-33a1e2f1904757c775d389fa99d67916b7b220d6aa1fe8bb6690f85ed1cd5666.json b/.sqlx/query-35a4ec60785870b07495258a3ea5faf0d7a9d5f523db44f7da17ca1bf31d4576.json similarity index 70% rename from .sqlx/query-33a1e2f1904757c775d389fa99d67916b7b220d6aa1fe8bb6690f85ed1cd5666.json rename to .sqlx/query-35a4ec60785870b07495258a3ea5faf0d7a9d5f523db44f7da17ca1bf31d4576.json index 462ab0695..1835956e7 100644 --- a/.sqlx/query-33a1e2f1904757c775d389fa99d67916b7b220d6aa1fe8bb6690f85ed1cd5666.json +++ b/.sqlx/query-35a4ec60785870b07495258a3ea5faf0d7a9d5f523db44f7da17ca1bf31d4576.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created FROM device JOIN \"user\" ON device.user_id = \"user\".id WHERE \"user\".username = $1", + "query": "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key FROM device JOIN \"user\" ON device.user_id = \"user\".id WHERE \"user\".username = $1", "describe": { "columns": [ { @@ -27,6 +27,11 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -39,8 +44,9 @@ false, false, false, - false + false, + true ] }, - "hash": "33a1e2f1904757c775d389fa99d67916b7b220d6aa1fe8bb6690f85ed1cd5666" + "hash": "35a4ec60785870b07495258a3ea5faf0d7a9d5f523db44f7da17ca1bf31d4576" } diff --git a/.sqlx/query-73ab0d514bbce0a69e1b22d9f5a2a58be7427882b368eef4d45529e9e85d885c.json b/.sqlx/query-38f3ad2dc19d222226b85a10d6c83e209cf075f9b128ed33908c73cd74297d81.json similarity index 65% rename from .sqlx/query-73ab0d514bbce0a69e1b22d9f5a2a58be7427882b368eef4d45529e9e85d885c.json rename to .sqlx/query-38f3ad2dc19d222226b85a10d6c83e209cf075f9b128ed33908c73cd74297d81.json index 847391cec..d187d6402 100644 --- a/.sqlx/query-73ab0d514bbce0a69e1b22d9f5a2a58be7427882b368eef4d45529e9e85d885c.json +++ b/.sqlx/query-38f3ad2dc19d222226b85a10d6c83e209cf075f9b128ed33908c73cd74297d81.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"wireguard_network\" SET \"name\" = $2,\"address\" = $3,\"port\" = $4,\"pubkey\" = $5,\"prvkey\" = $6,\"endpoint\" = $7,\"dns\" = $8,\"allowed_ips\" = $9,\"connected_at\" = $10 WHERE id = $1", + "query": "UPDATE \"wireguard_network\" SET \"name\" = $2,\"address\" = $3,\"port\" = $4,\"pubkey\" = $5,\"prvkey\" = $6,\"endpoint\" = $7,\"dns\" = $8,\"allowed_ips\" = $9,\"connected_at\" = $10,\"mfa_enabled\" = $11,\"keepalive_interval\" = $12,\"peer_disconnect_threshold\" = $13 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -14,10 +14,13 @@ "Text", "Text", "InetArray", - "Timestamp" + "Timestamp", + "Bool", + "Int4", + "Int4" ] }, "nullable": [] }, - "hash": "73ab0d514bbce0a69e1b22d9f5a2a58be7427882b368eef4d45529e9e85d885c" + "hash": "38f3ad2dc19d222226b85a10d6c83e209cf075f9b128ed33908c73cd74297d81" } diff --git a/.sqlx/query-3d5d8b6f640435a1986561c73cd809038cdddfb44133b97b5ba7392573239539.json b/.sqlx/query-3d5d8b6f640435a1986561c73cd809038cdddfb44133b97b5ba7392573239539.json new file mode 100644 index 000000000..3dd50e700 --- /dev/null +++ b/.sqlx/query-3d5d8b6f640435a1986561c73cd809038cdddfb44133b97b5ba7392573239539.json @@ -0,0 +1,29 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COALESCE(COUNT(DISTINCT(u.id)), 0) as \"active_users!\", COALESCE(COUNT(DISTINCT(s.device_id)), 0) as \"active_devices!\" FROM \"user\" u JOIN device d ON d.user_id = u.id JOIN wireguard_peer_stats s ON s.device_id = d.id WHERE latest_handshake >= $1 AND s.network = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "active_users!", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "active_devices!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Timestamp", + "Int8" + ] + }, + "nullable": [ + null, + null + ] + }, + "hash": "3d5d8b6f640435a1986561c73cd809038cdddfb44133b97b5ba7392573239539" +} diff --git a/.sqlx/query-4183b4f169126bcc511ff76c7554622fc71857d0d459588b4f3934307611aee7.json b/.sqlx/query-4183b4f169126bcc511ff76c7554622fc71857d0d459588b4f3934307611aee7.json deleted file mode 100644 index e2761039f..000000000 --- a/.sqlx/query-4183b4f169126bcc511ff76c7554622fc71857d0d459588b4f3934307611aee7.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n COALESCE(COUNT(DISTINCT(u.id)), 0) as \"active_users!\",\n COALESCE(COUNT(DISTINCT(s.device_id)), 0) as \"active_devices!\"\n FROM \"user\" u\n JOIN device d ON d.user_id = u.id\n JOIN wireguard_peer_stats s ON s.device_id = d.id\n WHERE latest_handshake >= $1 AND s.network = $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "active_users!", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "active_devices!", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Timestamp", - "Int8" - ] - }, - "nullable": [ - null, - null - ] - }, - "hash": "4183b4f169126bcc511ff76c7554622fc71857d0d459588b4f3934307611aee7" -} diff --git a/.sqlx/query-4218fd109bd4a17b2a4551cfe0d14f2c9b6a37f25f0696d83078dae2c9f87c5f.json b/.sqlx/query-4218fd109bd4a17b2a4551cfe0d14f2c9b6a37f25f0696d83078dae2c9f87c5f.json new file mode 100644 index 000000000..5354828a7 --- /dev/null +++ b/.sqlx/query-4218fd109bd4a17b2a4551cfe0d14f2c9b6a37f25f0696d83078dae2c9f87c5f.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM wireguard_peer_stats WHERE collected_at < $1 AND (device_id, network, collected_at) NOT IN ( SELECT device_id, network, MAX(collected_at) FROM wireguard_peer_stats GROUP BY device_id, network)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamp" + ] + }, + "nullable": [] + }, + "hash": "4218fd109bd4a17b2a4551cfe0d14f2c9b6a37f25f0696d83078dae2c9f87c5f" +} diff --git a/.sqlx/query-05e3fce0c51856f6033195c7523a2b5fd1c2a7968a7abd27394422a7c93e8815.json b/.sqlx/query-42ccaa218d47638ff39d9006095ac30ae1cd9dce74ec826ed875c39cc05f04f8.json similarity index 54% rename from .sqlx/query-05e3fce0c51856f6033195c7523a2b5fd1c2a7968a7abd27394422a7c93e8815.json rename to .sqlx/query-42ccaa218d47638ff39d9006095ac30ae1cd9dce74ec826ed875c39cc05f04f8.json index 7b35afeb1..8b795c04a 100644 --- a/.sqlx/query-05e3fce0c51856f6033195c7523a2b5fd1c2a7968a7abd27394422a7c93e8815.json +++ b/.sqlx/query-42ccaa218d47638ff39d9006095ac30ae1cd9dce74ec826ed875c39cc05f04f8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n date_trunc($1, collected_at) \"collected_at: NaiveDateTime\",\n cast(sum(upload) AS bigint) upload, cast(sum(download) AS bigint) download\n FROM wireguard_peer_stats_view\n WHERE collected_at >= $2 AND network = $3\n GROUP BY 1\n ORDER BY 1\n LIMIT $4\n ", + "query": "SELECT date_trunc($1, collected_at) \"collected_at: NaiveDateTime\", cast(sum(upload) AS bigint) upload, cast(sum(download) AS bigint) download FROM wireguard_peer_stats_view WHERE collected_at >= $2 AND network = $3 GROUP BY 1 ORDER BY 1 LIMIT $4", "describe": { "columns": [ { @@ -33,5 +33,5 @@ null ] }, - "hash": "05e3fce0c51856f6033195c7523a2b5fd1c2a7968a7abd27394422a7c93e8815" + "hash": "42ccaa218d47638ff39d9006095ac30ae1cd9dce74ec826ed875c39cc05f04f8" } diff --git a/.sqlx/query-9e3c1c1f52bc1a576012c57c7472f9f7601e1128b15fc0ecc7676cd5aa01c88c.json b/.sqlx/query-43fe6ac793c1a59664383338f83488788ae61d74d3e86bbe123b1d41c8e19af4.json similarity index 74% rename from .sqlx/query-9e3c1c1f52bc1a576012c57c7472f9f7601e1128b15fc0ecc7676cd5aa01c88c.json rename to .sqlx/query-43fe6ac793c1a59664383338f83488788ae61d74d3e86bbe123b1d41c8e19af4.json index 82874b3d7..a4455a8ac 100644 --- a/.sqlx/query-9e3c1c1f52bc1a576012c57c7472f9f7601e1128b15fc0ecc7676cd5aa01c88c.json +++ b/.sqlx/query-43fe6ac793c1a59664383338f83488788ae61d74d3e86bbe123b1d41c8e19af4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created FROM device JOIN \"user\" ON device.user_id = \"user\".id WHERE device.id = $1 AND \"user\".id = $2", + "query": "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key FROM device WHERE user_id = $1", "describe": { "columns": [ { @@ -27,11 +27,15 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { "Left": [ - "Int8", "Int8" ] }, @@ -40,8 +44,9 @@ false, false, false, - false + false, + true ] }, - "hash": "9e3c1c1f52bc1a576012c57c7472f9f7601e1128b15fc0ecc7676cd5aa01c88c" + "hash": "43fe6ac793c1a59664383338f83488788ae61d74d3e86bbe123b1d41c8e19af4" } diff --git a/.sqlx/query-45545d3190c14bb3e9528ab3d3a8522209f36956f65bf5a126682d5477db425e.json b/.sqlx/query-45545d3190c14bb3e9528ab3d3a8522209f36956f65bf5a126682d5477db425e.json deleted file mode 100644 index 02e887ea1..000000000 --- a/.sqlx/query-45545d3190c14bb3e9528ab3d3a8522209f36956f65bf5a126682d5477db425e.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE wireguard_network_device\n SET wireguard_ip = $3\n WHERE device_id = $1 AND wireguard_network_id = $2\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Inet" - ] - }, - "nullable": [] - }, - "hash": "45545d3190c14bb3e9528ab3d3a8522209f36956f65bf5a126682d5477db425e" -} diff --git a/.sqlx/query-bde600b9d9448806df37628d305a09b93f4d1217dfb1141a32be11673d76dd1f.json b/.sqlx/query-45a53587f6d8bee3de695b846d5bf85dbcf3fb1a202611480af6a7b9e28d864f.json similarity index 68% rename from .sqlx/query-bde600b9d9448806df37628d305a09b93f4d1217dfb1141a32be11673d76dd1f.json rename to .sqlx/query-45a53587f6d8bee3de695b846d5bf85dbcf3fb1a202611480af6a7b9e28d864f.json index 840991111..57db8db09 100644 --- a/.sqlx/query-bde600b9d9448806df37628d305a09b93f4d1217dfb1141a32be11673d76dd1f.json +++ b/.sqlx/query-45a53587f6d8bee3de695b846d5bf85dbcf3fb1a202611480af6a7b9e28d864f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id as \"id?\", name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at FROM wireguard_network WHERE name = $1", + "query": "SELECT id as \"id?\", name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, mfa_enabled, keepalive_interval, peer_disconnect_threshold FROM wireguard_network WHERE name = $1", "describe": { "columns": [ { @@ -52,6 +52,21 @@ "ordinal": 9, "name": "connected_at", "type_info": "Timestamp" + }, + { + "ordinal": 10, + "name": "mfa_enabled", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "keepalive_interval", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "peer_disconnect_threshold", + "type_info": "Int4" } ], "parameters": { @@ -69,8 +84,11 @@ false, true, false, - true + true, + false, + false, + false ] }, - "hash": "bde600b9d9448806df37628d305a09b93f4d1217dfb1141a32be11673d76dd1f" + "hash": "45a53587f6d8bee3de695b846d5bf85dbcf3fb1a202611480af6a7b9e28d864f" } diff --git a/.sqlx/query-48966c8c5f8999dad107a7c49d7c17f6a3a3b1bda1412df2b30bf2f770d70e8f.json b/.sqlx/query-48966c8c5f8999dad107a7c49d7c17f6a3a3b1bda1412df2b30bf2f770d70e8f.json new file mode 100644 index 000000000..20ce12f88 --- /dev/null +++ b/.sqlx/query-48966c8c5f8999dad107a7c49d7c17f6a3a3b1bda1412df2b30bf2f770d70e8f.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM token WHERE user_id = $1 AND used_at IS NULL", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "48966c8c5f8999dad107a7c49d7c17f6a3a3b1bda1412df2b30bf2f770d70e8f" +} diff --git a/.sqlx/query-9213729a9a1ce371ef77898f5792d914a67400cb4cce9a8bf86227ffe7d42eda.json b/.sqlx/query-55aac498ce61c0b42f4895add96dfbf99606684a681883600a7aca610c7c6e8b.json similarity index 64% rename from .sqlx/query-9213729a9a1ce371ef77898f5792d914a67400cb4cce9a8bf86227ffe7d42eda.json rename to .sqlx/query-55aac498ce61c0b42f4895add96dfbf99606684a681883600a7aca610c7c6e8b.json index 285e7121c..5f222745a 100644 --- a/.sqlx/query-9213729a9a1ce371ef77898f5792d914a67400cb4cce9a8bf86227ffe7d42eda.json +++ b/.sqlx/query-55aac498ce61c0b42f4895add96dfbf99606684a681883600a7aca610c7c6e8b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"device\" (\"name\",\"wireguard_pubkey\",\"user_id\",\"created\") VALUES ($1,$2,$3,$4) RETURNING id", + "query": "INSERT INTO \"device\" (\"name\",\"wireguard_pubkey\",\"user_id\",\"created\",\"preshared_key\") VALUES ($1,$2,$3,$4,$5) RETURNING id", "describe": { "columns": [ { @@ -14,12 +14,13 @@ "Text", "Text", "Int8", - "Timestamp" + "Timestamp", + "Text" ] }, "nullable": [ false ] }, - "hash": "9213729a9a1ce371ef77898f5792d914a67400cb4cce9a8bf86227ffe7d42eda" + "hash": "55aac498ce61c0b42f4895add96dfbf99606684a681883600a7aca610c7c6e8b" } diff --git a/.sqlx/query-06f847d99d452dafd10f4a6bec309c330d76eb5d0a34012bd75395b13e3ff659.json b/.sqlx/query-5d629b503d4d9f76b4e9b8981139753077bf164f59900f29d54ff35bc294c9b4.json similarity index 74% rename from .sqlx/query-06f847d99d452dafd10f4a6bec309c330d76eb5d0a34012bd75395b13e3ff659.json rename to .sqlx/query-5d629b503d4d9f76b4e9b8981139753077bf164f59900f29d54ff35bc294c9b4.json index c291b5a4e..c89652bd4 100644 --- a/.sqlx/query-06f847d99d452dafd10f4a6bec309c330d76eb5d0a34012bd75395b13e3ff659.json +++ b/.sqlx/query-5d629b503d4d9f76b4e9b8981139753077bf164f59900f29d54ff35bc294c9b4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", \"name\",\"wireguard_pubkey\",\"user_id\",\"created\" FROM \"device\"", + "query": "SELECT id \"id?\", \"name\",\"wireguard_pubkey\",\"user_id\",\"created\",\"preshared_key\" FROM \"device\"", "describe": { "columns": [ { @@ -27,6 +27,11 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -37,8 +42,9 @@ false, false, false, - false + false, + true ] }, - "hash": "06f847d99d452dafd10f4a6bec309c330d76eb5d0a34012bd75395b13e3ff659" + "hash": "5d629b503d4d9f76b4e9b8981139753077bf164f59900f29d54ff35bc294c9b4" } diff --git a/.sqlx/query-6eb3f257ef7762c8633885b2b49d3f18033b11db0daf4133c2abaf8823a8a86a.json b/.sqlx/query-6eb3f257ef7762c8633885b2b49d3f18033b11db0daf4133c2abaf8823a8a86a.json new file mode 100644 index 000000000..ba14f8502 --- /dev/null +++ b/.sqlx/query-6eb3f257ef7762c8633885b2b49d3f18033b11db0daf4133c2abaf8823a8a86a.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM wireguard_network_device WHERE device_id = $1 AND wireguard_network_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "6eb3f257ef7762c8633885b2b49d3f18033b11db0daf4133c2abaf8823a8a86a" +} diff --git a/.sqlx/query-18e96b44b04c6ed00424ac497889e5952c877f1d14216f5b083fd7fa7c927bb7.json b/.sqlx/query-7381a3487396a1d7e562e21bb2804fd82b538b4ed436ad714408a72657e8a9e2.json similarity index 74% rename from .sqlx/query-18e96b44b04c6ed00424ac497889e5952c877f1d14216f5b083fd7fa7c927bb7.json rename to .sqlx/query-7381a3487396a1d7e562e21bb2804fd82b538b4ed436ad714408a72657e8a9e2.json index 210810108..eb365a6f6 100644 --- a/.sqlx/query-18e96b44b04c6ed00424ac497889e5952c877f1d14216f5b083fd7fa7c927bb7.json +++ b/.sqlx/query-7381a3487396a1d7e562e21bb2804fd82b538b4ed436ad714408a72657e8a9e2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\" FROM\n wireguard_network_device\n WHERE wireguard_network_id = $1", + "query": "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\" FROM wireguard_network_device WHERE wireguard_network_id = $1", "describe": { "columns": [ { @@ -30,5 +30,5 @@ false ] }, - "hash": "18e96b44b04c6ed00424ac497889e5952c877f1d14216f5b083fd7fa7c927bb7" + "hash": "7381a3487396a1d7e562e21bb2804fd82b538b4ed436ad714408a72657e8a9e2" } diff --git a/.sqlx/query-7878106cb8e9cc315310cf7c0946c3782f2bc80e3f2e0f015bea23717604709f.json b/.sqlx/query-7878106cb8e9cc315310cf7c0946c3782f2bc80e3f2e0f015bea23717604709f.json new file mode 100644 index 000000000..2a247f079 --- /dev/null +++ b/.sqlx/query-7878106cb8e9cc315310cf7c0946c3782f2bc80e3f2e0f015bea23717604709f.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT d.wireguard_pubkey as pubkey, preshared_key, array[host(wnd.wireguard_ip)] as \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id WHERE wireguard_network_id = $1 ORDER BY d.id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "pubkey", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "preshared_key", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "allowed_ips!: Vec", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + true, + null + ] + }, + "hash": "7878106cb8e9cc315310cf7c0946c3782f2bc80e3f2e0f015bea23717604709f" +} diff --git a/.sqlx/query-7b7092bd5fe377c8b28862721b05b3ca39aab9fb176fe970b9309d6371072e95.json b/.sqlx/query-7b7092bd5fe377c8b28862721b05b3ca39aab9fb176fe970b9309d6371072e95.json deleted file mode 100644 index d53ea77b7..000000000 --- a/.sqlx/query-7b7092bd5fe377c8b28862721b05b3ca39aab9fb176fe970b9309d6371072e95.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM wireguard_peer_stats\n WHERE collected_at < $1\n AND (device_id, network, collected_at) NOT IN (\n SELECT device_id, network, MAX(collected_at)\n FROM wireguard_peer_stats\n GROUP BY device_id, network\n )", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Timestamp" - ] - }, - "nullable": [] - }, - "hash": "7b7092bd5fe377c8b28862721b05b3ca39aab9fb176fe970b9309d6371072e95" -} diff --git a/.sqlx/query-66d2e2f3156ce802a0018bea5aa0718b2ce599d44de6382e854174053a639f5a.json b/.sqlx/query-8ae08ad03e745e8f3d65707bb8637a9277805ca9c7a22faeb66230ce0fa87ea9.json similarity index 73% rename from .sqlx/query-66d2e2f3156ce802a0018bea5aa0718b2ce599d44de6382e854174053a639f5a.json rename to .sqlx/query-8ae08ad03e745e8f3d65707bb8637a9277805ca9c7a22faeb66230ce0fa87ea9.json index f1a5f025f..5c37678ae 100644 --- a/.sqlx/query-66d2e2f3156ce802a0018bea5aa0718b2ce599d44de6382e854174053a639f5a.json +++ b/.sqlx/query-8ae08ad03e745e8f3d65707bb8637a9277805ca9c7a22faeb66230ce0fa87ea9.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\" FROM\n wireguard_network_device\n WHERE device_id = $1 AND wireguard_network_id = $2", + "query": "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\" FROM wireguard_network_device WHERE device_id = $1 AND wireguard_network_id = $2", "describe": { "columns": [ { @@ -31,5 +31,5 @@ false ] }, - "hash": "66d2e2f3156ce802a0018bea5aa0718b2ce599d44de6382e854174053a639f5a" + "hash": "8ae08ad03e745e8f3d65707bb8637a9277805ca9c7a22faeb66230ce0fa87ea9" } diff --git a/.sqlx/query-961bd7d2e2cc98e2b968ccb22e065f5c30566de00d9b96d8a70e3f08a31c9dc2.json b/.sqlx/query-961bd7d2e2cc98e2b968ccb22e065f5c30566de00d9b96d8a70e3f08a31c9dc2.json deleted file mode 100644 index 3d581539c..000000000 --- a/.sqlx/query-961bd7d2e2cc98e2b968ccb22e065f5c30566de00d9b96d8a70e3f08a31c9dc2.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n latest_handshake \"latest_handshake: NaiveDateTime\"\n FROM wireguard_peer_stats_view\n WHERE device_id = $1\n AND latest_handshake IS NOT NULL\n AND (latest_handshake_diff > $2 * interval '1 minute' OR latest_handshake_diff IS NULL)\n AND network = $3\n ORDER BY collected_at DESC\n LIMIT 1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "latest_handshake: NaiveDateTime", - "type_info": "Timestamp" - } - ], - "parameters": { - "Left": [ - "Int8", - "Float8", - "Int8" - ] - }, - "nullable": [ - true - ] - }, - "hash": "961bd7d2e2cc98e2b968ccb22e065f5c30566de00d9b96d8a70e3f08a31c9dc2" -} diff --git a/.sqlx/query-a2a9f1e0388ce6705deba02473a05c6de14f5f73248769f9faad9c28c411fad8.json b/.sqlx/query-a2a9f1e0388ce6705deba02473a05c6de14f5f73248769f9faad9c28c411fad8.json deleted file mode 100644 index adb908c90..000000000 --- a/.sqlx/query-a2a9f1e0388ce6705deba02473a05c6de14f5f73248769f9faad9c28c411fad8.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM token\n WHERE user_id = $1\n AND token_type = 'PASSWORD_RESET'\n AND used_at IS NULL", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - }, - "hash": "a2a9f1e0388ce6705deba02473a05c6de14f5f73248769f9faad9c28c411fad8" -} diff --git a/.sqlx/query-a5aa90dcb89a4e7f1908171b4e4ed6027dd5c770a1330689a029cc31f731bf33.json b/.sqlx/query-a5aa90dcb89a4e7f1908171b4e4ed6027dd5c770a1330689a029cc31f731bf33.json new file mode 100644 index 000000000..8bbfec8e8 --- /dev/null +++ b/.sqlx/query-a5aa90dcb89a4e7f1908171b4e4ed6027dd5c770a1330689a029cc31f731bf33.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO wireguard_network_device (device_id, wireguard_network_id, wireguard_ip) VALUES ($1, $2, $3) ON CONFLICT ON CONSTRAINT device_network DO UPDATE SET wireguard_ip = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Inet" + ] + }, + "nullable": [] + }, + "hash": "a5aa90dcb89a4e7f1908171b4e4ed6027dd5c770a1330689a029cc31f731bf33" +} diff --git a/.sqlx/query-aa8bbafc145ad47aaac28e9863c19b1db6a855e917cdaf619a9461ad89b96917.json b/.sqlx/query-aa8bbafc145ad47aaac28e9863c19b1db6a855e917cdaf619a9461ad89b96917.json deleted file mode 100644 index 96f0ec290..000000000 --- a/.sqlx/query-aa8bbafc145ad47aaac28e9863c19b1db6a855e917cdaf619a9461ad89b96917.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n DELETE FROM wireguard_network_device\n WHERE device_id = $1 AND wireguard_network_id = $2\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - }, - "nullable": [] - }, - "hash": "aa8bbafc145ad47aaac28e9863c19b1db6a855e917cdaf619a9461ad89b96917" -} diff --git a/.sqlx/query-929535564469ee1701f83a1489301d0d652d5452369a2dce933f032cdb7092e2.json b/.sqlx/query-b1b93736fd91a6445bb072e5bbe619c59d9a535dc70d226de618e549c203e88b.json similarity index 62% rename from .sqlx/query-929535564469ee1701f83a1489301d0d652d5452369a2dce933f032cdb7092e2.json rename to .sqlx/query-b1b93736fd91a6445bb072e5bbe619c59d9a535dc70d226de618e549c203e88b.json index 8a1e90ee9..afd9e39e3 100644 --- a/.sqlx/query-929535564469ee1701f83a1489301d0d652d5452369a2dce933f032cdb7092e2.json +++ b/.sqlx/query-b1b93736fd91a6445bb072e5bbe619c59d9a535dc70d226de618e549c203e88b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created\n FROM device WHERE user_id = $1\n ", + "query": "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key FROM device JOIN \"user\" ON device.user_id = \"user\".id WHERE device.id = $1 AND \"user\".id = $2", "describe": { "columns": [ { @@ -27,10 +27,16 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { "Left": [ + "Int8", "Int8" ] }, @@ -39,8 +45,9 @@ false, false, false, - false + false, + true ] }, - "hash": "929535564469ee1701f83a1489301d0d652d5452369a2dce933f032cdb7092e2" + "hash": "b1b93736fd91a6445bb072e5bbe619c59d9a535dc70d226de618e549c203e88b" } diff --git a/.sqlx/query-c0e30b2a0b711ea77d88a6dc9c92d0faa187d2bb59bedd0c66d1eae866922901.json b/.sqlx/query-c0e30b2a0b711ea77d88a6dc9c92d0faa187d2bb59bedd0c66d1eae866922901.json deleted file mode 100644 index 768f9b53d..000000000 --- a/.sqlx/query-c0e30b2a0b711ea77d88a6dc9c92d0faa187d2bb59bedd0c66d1eae866922901.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n WITH stats AS (\n SELECT DISTINCT ON (network) network, endpoint, latest_handshake\n FROM wireguard_peer_stats\n WHERE device_id = $2\n ORDER BY network, collected_at DESC\n )\n SELECT\n n.id as network_id, n.name as network_name, n.endpoint as gateway_endpoint,\n wnd.wireguard_ip as \"device_wireguard_ip: IpAddr\", stats.endpoint as device_endpoint,\n stats.latest_handshake as \"latest_handshake?\",\n COALESCE (((NOW() - stats.latest_handshake) < $1 * interval '1 minute'), false) as \"is_active!\"\n FROM wireguard_network_device wnd\n JOIN wireguard_network n ON n.id = wnd.wireguard_network_id\n LEFT JOIN stats on n.id = stats.network\n WHERE wnd.device_id = $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "network_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "network_name", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "gateway_endpoint", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "device_wireguard_ip: IpAddr", - "type_info": "Inet" - }, - { - "ordinal": 4, - "name": "device_endpoint", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "latest_handshake?", - "type_info": "Timestamp" - }, - { - "ordinal": 6, - "name": "is_active!", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Float8", - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - true, - false, - null - ] - }, - "hash": "c0e30b2a0b711ea77d88a6dc9c92d0faa187d2bb59bedd0c66d1eae866922901" -} diff --git a/.sqlx/query-c3566263419aab9ba0172d59b105c03a8f5b70a8813849956e9d32b5da5a407f.json b/.sqlx/query-c3566263419aab9ba0172d59b105c03a8f5b70a8813849956e9d32b5da5a407f.json new file mode 100644 index 000000000..82ebe7c72 --- /dev/null +++ b/.sqlx/query-c3566263419aab9ba0172d59b105c03a8f5b70a8813849956e9d32b5da5a407f.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE wireguard_network_device SET wireguard_ip = $3 WHERE device_id = $1 AND wireguard_network_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Inet" + ] + }, + "nullable": [] + }, + "hash": "c3566263419aab9ba0172d59b105c03a8f5b70a8813849956e9d32b5da5a407f" +} diff --git a/.sqlx/query-6dfb9442da967755230ab2b7c5baf8eec06d64af5886e840bea1b32d2be10ca0.json b/.sqlx/query-c91e2bc58d379bbc7ed29d008f1806ff6f0efe4fd55a198682d52d2edc7de24d.json similarity index 57% rename from .sqlx/query-6dfb9442da967755230ab2b7c5baf8eec06d64af5886e840bea1b32d2be10ca0.json rename to .sqlx/query-c91e2bc58d379bbc7ed29d008f1806ff6f0efe4fd55a198682d52d2edc7de24d.json index 1a836e180..e9d042263 100644 --- a/.sqlx/query-6dfb9442da967755230ab2b7c5baf8eec06d64af5886e840bea1b32d2be10ca0.json +++ b/.sqlx/query-c91e2bc58d379bbc7ed29d008f1806ff6f0efe4fd55a198682d52d2edc7de24d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"device\" SET \"name\" = $2,\"wireguard_pubkey\" = $3,\"user_id\" = $4,\"created\" = $5 WHERE id = $1", + "query": "UPDATE \"device\" SET \"name\" = $2,\"wireguard_pubkey\" = $3,\"user_id\" = $4,\"created\" = $5,\"preshared_key\" = $6 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -9,10 +9,11 @@ "Text", "Text", "Int8", - "Timestamp" + "Timestamp", + "Text" ] }, "nullable": [] }, - "hash": "6dfb9442da967755230ab2b7c5baf8eec06d64af5886e840bea1b32d2be10ca0" + "hash": "c91e2bc58d379bbc7ed29d008f1806ff6f0efe4fd55a198682d52d2edc7de24d" } diff --git a/.sqlx/query-fd92ab2977c9c170e2254f13bd99fcb569ef4b44029783648b0613ca8db2a393.json b/.sqlx/query-ca2755ead4852207d09bcd46ffcaadf33061fbb6cf7e1e7c58d5bee1692351e2.json similarity index 67% rename from .sqlx/query-fd92ab2977c9c170e2254f13bd99fcb569ef4b44029783648b0613ca8db2a393.json rename to .sqlx/query-ca2755ead4852207d09bcd46ffcaadf33061fbb6cf7e1e7c58d5bee1692351e2.json index 900c1d364..4b0364ac1 100644 --- a/.sqlx/query-fd92ab2977c9c170e2254f13bd99fcb569ef4b44029783648b0613ca8db2a393.json +++ b/.sqlx/query-ca2755ead4852207d09bcd46ffcaadf33061fbb6cf7e1e7c58d5bee1692351e2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT d.id \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created\n FROM device d\n JOIN wireguard_network_device wnd\n ON d.id = wnd.device_id\n WHERE wnd.wireguard_ip = $1 AND wnd.wireguard_network_id = $2", + "query": "SELECT d.id \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created, d.preshared_key FROM device d JOIN wireguard_network_device wnd ON d.id = wnd.device_id WHERE wnd.wireguard_ip = $1 AND wnd.wireguard_network_id = $2", "describe": { "columns": [ { @@ -27,6 +27,11 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -40,8 +45,9 @@ false, false, false, - false + false, + true ] }, - "hash": "fd92ab2977c9c170e2254f13bd99fcb569ef4b44029783648b0613ca8db2a393" + "hash": "ca2755ead4852207d09bcd46ffcaadf33061fbb6cf7e1e7c58d5bee1692351e2" } diff --git a/.sqlx/query-2da415f5b71186fcfbe7fdf3794606eb73b47c4ec120aaef50aff6c7d075dbdd.json b/.sqlx/query-cbe6cdf1b9dd1d13bbb460726e33001ee45dea8486fb02e8c375d7b513ac0d6d.json similarity index 82% rename from .sqlx/query-2da415f5b71186fcfbe7fdf3794606eb73b47c4ec120aaef50aff6c7d075dbdd.json rename to .sqlx/query-cbe6cdf1b9dd1d13bbb460726e33001ee45dea8486fb02e8c375d7b513ac0d6d.json index 9581c1f52..26232ef8b 100644 --- a/.sqlx/query-2da415f5b71186fcfbe7fdf3794606eb73b47c4ec120aaef50aff6c7d075dbdd.json +++ b/.sqlx/query-cbe6cdf1b9dd1d13bbb460726e33001ee45dea8486fb02e8c375d7b513ac0d6d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT id, mfa_enabled, totp_enabled, email_mfa_enabled, mfa_method as \"mfa_method: MFAMethod\", password_hash FROM \"user\"\n ", + "query": "SELECT id, mfa_enabled, totp_enabled, email_mfa_enabled, mfa_method as \"mfa_method: MFAMethod\", password_hash FROM \"user\"", "describe": { "columns": [ { @@ -59,5 +59,5 @@ true ] }, - "hash": "2da415f5b71186fcfbe7fdf3794606eb73b47c4ec120aaef50aff6c7d075dbdd" + "hash": "cbe6cdf1b9dd1d13bbb460726e33001ee45dea8486fb02e8c375d7b513ac0d6d" } diff --git a/.sqlx/query-c64f247f81e332689e35c224656847b246deb6f92a5790a4fdd0d5733defbb57.json b/.sqlx/query-d069191fedf7628f2d62be632911845278cca5ceb21718f37082728fa052c33c.json similarity index 73% rename from .sqlx/query-c64f247f81e332689e35c224656847b246deb6f92a5790a4fdd0d5733defbb57.json rename to .sqlx/query-d069191fedf7628f2d62be632911845278cca5ceb21718f37082728fa052c33c.json index 4a03705f0..3cf4488e3 100644 --- a/.sqlx/query-c64f247f81e332689e35c224656847b246deb6f92a5790a4fdd0d5733defbb57.json +++ b/.sqlx/query-d069191fedf7628f2d62be632911845278cca5ceb21718f37082728fa052c33c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", \"name\",\"wireguard_pubkey\",\"user_id\",\"created\" FROM \"device\" WHERE id = $1", + "query": "SELECT id \"id?\", \"name\",\"wireguard_pubkey\",\"user_id\",\"created\",\"preshared_key\" FROM \"device\" WHERE id = $1", "describe": { "columns": [ { @@ -27,6 +27,11 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -39,8 +44,9 @@ false, false, false, - false + false, + true ] }, - "hash": "c64f247f81e332689e35c224656847b246deb6f92a5790a4fdd0d5733defbb57" + "hash": "d069191fedf7628f2d62be632911845278cca5ceb21718f37082728fa052c33c" } diff --git a/.sqlx/query-0b53e81d6f1ce3c3a68c871e7a4b9e41db58b112d12281516e10421d46fbbd94.json b/.sqlx/query-d5761193c11c9029731464a6824600203ca943272969aff90da38088f9f55097.json similarity index 56% rename from .sqlx/query-0b53e81d6f1ce3c3a68c871e7a4b9e41db58b112d12281516e10421d46fbbd94.json rename to .sqlx/query-d5761193c11c9029731464a6824600203ca943272969aff90da38088f9f55097.json index 828f2523d..8bad0a958 100644 --- a/.sqlx/query-0b53e81d6f1ce3c3a68c871e7a4b9e41db58b112d12281516e10421d46fbbd94.json +++ b/.sqlx/query-d5761193c11c9029731464a6824600203ca943272969aff90da38088f9f55097.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT DISTINCT ON (d.id) d.id as \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created\n FROM device d\n JOIN \"user\" u ON d.user_id = u.id\n JOIN group_user gu ON u.id = gu.user_id\n JOIN \"group\" g ON gu.group_id = g.id\n WHERE g.\"name\" IN (SELECT * FROM UNNEST($1::text[]))\n ORDER BY d.id ASC\n ", + "query": "SELECT DISTINCT ON (d.id) d.id as \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created, d.preshared_key FROM device d JOIN \"user\" u ON d.user_id = u.id JOIN group_user gu ON u.id = gu.user_id JOIN \"group\" g ON gu.group_id = g.id WHERE g.\"name\" IN (SELECT * FROM UNNEST($1::text[]))\n ORDER BY d.id ASC", "describe": { "columns": [ { @@ -27,6 +27,11 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -39,8 +44,9 @@ false, false, false, - false + false, + true ] }, - "hash": "0b53e81d6f1ce3c3a68c871e7a4b9e41db58b112d12281516e10421d46fbbd94" + "hash": "d5761193c11c9029731464a6824600203ca943272969aff90da38088f9f55097" } diff --git a/.sqlx/query-d8b3cbc7317bfdee111b80accd5d87781e7fa62a39998d54bf79d736ed7a8827.json b/.sqlx/query-d8b3cbc7317bfdee111b80accd5d87781e7fa62a39998d54bf79d736ed7a8827.json deleted file mode 100644 index 2c1f0f69e..000000000 --- a/.sqlx/query-d8b3cbc7317bfdee111b80accd5d87781e7fa62a39998d54bf79d736ed7a8827.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT d.wireguard_pubkey as pubkey, array[host(wnd.wireguard_ip)] as \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id WHERE wireguard_network_id = $1 ORDER BY d.id ASC", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "pubkey", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "allowed_ips!: Vec", - "type_info": "TextArray" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - null - ] - }, - "hash": "d8b3cbc7317bfdee111b80accd5d87781e7fa62a39998d54bf79d736ed7a8827" -} diff --git a/.sqlx/query-865131a7e440c7e5f297cb8620487fe0cf641f481175adaf22982d1f983d362e.json b/.sqlx/query-dba6275f3895b8725bdce57c4617956773817874aeccc8c9f73527018e5d10cb.json similarity index 56% rename from .sqlx/query-865131a7e440c7e5f297cb8620487fe0cf641f481175adaf22982d1f983d362e.json rename to .sqlx/query-dba6275f3895b8725bdce57c4617956773817874aeccc8c9f73527018e5d10cb.json index 53638cdc0..265377aa3 100644 --- a/.sqlx/query-865131a7e440c7e5f297cb8620487fe0cf641f481175adaf22982d1f983d362e.json +++ b/.sqlx/query-dba6275f3895b8725bdce57c4617956773817874aeccc8c9f73527018e5d10cb.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n WITH s AS (\n SELECT DISTINCT ON (device_id) *\n FROM wireguard_peer_stats\n ORDER BY device_id, latest_handshake DESC\n )\n SELECT\n d.id \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created\n FROM device d\n JOIN s ON d.id = s.device_id\n WHERE s.latest_handshake >= $1 AND s.network = $2\n ", + "query": "WITH s AS ( SELECT DISTINCT ON (device_id) * FROM wireguard_peer_stats ORDER BY device_id, latest_handshake DESC ) SELECT d.id \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created, d.preshared_key FROM device d JOIN s ON d.id = s.device_id WHERE s.latest_handshake >= $1 AND s.network = $2", "describe": { "columns": [ { @@ -27,6 +27,11 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -40,8 +45,9 @@ false, false, false, - false + false, + true ] }, - "hash": "865131a7e440c7e5f297cb8620487fe0cf641f481175adaf22982d1f983d362e" + "hash": "dba6275f3895b8725bdce57c4617956773817874aeccc8c9f73527018e5d10cb" } diff --git a/.sqlx/query-439bef62ccc846cccca2c6979e5698a7aaba2beb19645b720eefa73e4dcac942.json b/.sqlx/query-ea731a5dd05cf3c68a4f479ec978571325c844ae1f4d222193bebd61a1859ee2.json similarity index 69% rename from .sqlx/query-439bef62ccc846cccca2c6979e5698a7aaba2beb19645b720eefa73e4dcac942.json rename to .sqlx/query-ea731a5dd05cf3c68a4f479ec978571325c844ae1f4d222193bebd61a1859ee2.json index b5ca2d931..2e83afa95 100644 --- a/.sqlx/query-439bef62ccc846cccca2c6979e5698a7aaba2beb19645b720eefa73e4dcac942.json +++ b/.sqlx/query-ea731a5dd05cf3c68a4f479ec978571325c844ae1f4d222193bebd61a1859ee2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created FROM device JOIN \"user\" ON device.user_id = \"user\".id WHERE device.id = $1 AND \"user\".username = $2", + "query": "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key FROM device JOIN \"user\" ON device.user_id = \"user\".id WHERE device.id = $1 AND \"user\".username = $2", "describe": { "columns": [ { @@ -27,6 +27,11 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -40,8 +45,9 @@ false, false, false, - false + false, + true ] }, - "hash": "439bef62ccc846cccca2c6979e5698a7aaba2beb19645b720eefa73e4dcac942" + "hash": "ea731a5dd05cf3c68a4f479ec978571325c844ae1f4d222193bebd61a1859ee2" } diff --git a/.sqlx/query-ee6ed3df25517a6fedf6981166e58601de53af70dd28f69b84caca6e36cb8956.json b/.sqlx/query-ee6ed3df25517a6fedf6981166e58601de53af70dd28f69b84caca6e36cb8956.json deleted file mode 100644 index d59b060a1..000000000 --- a/.sqlx/query-ee6ed3df25517a6fedf6981166e58601de53af70dd28f69b84caca6e36cb8956.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO wireguard_network_device\n (device_id, wireguard_network_id, wireguard_ip)\n VALUES ($1, $2, $3)\n ON CONFLICT ON CONSTRAINT device_network\n DO UPDATE SET wireguard_ip = $3", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Inet" - ] - }, - "nullable": [] - }, - "hash": "ee6ed3df25517a6fedf6981166e58601de53af70dd28f69b84caca6e36cb8956" -} diff --git a/.sqlx/query-efe773da199984a169b636cafabd4ee0967c7dc68a04ec930f863d8f3f34c14a.json b/.sqlx/query-efe773da199984a169b636cafabd4ee0967c7dc68a04ec930f863d8f3f34c14a.json new file mode 100644 index 000000000..408dde103 --- /dev/null +++ b/.sqlx/query-efe773da199984a169b636cafabd4ee0967c7dc68a04ec930f863d8f3f34c14a.json @@ -0,0 +1,59 @@ +{ + "db_name": "PostgreSQL", + "query": "WITH stats AS ( SELECT DISTINCT ON (network) network, endpoint, latest_handshake FROM wireguard_peer_stats WHERE device_id = $2 ORDER BY network, collected_at DESC ) SELECT n.id as network_id, n.name as network_name, n.endpoint as gateway_endpoint, wnd.wireguard_ip as \"device_wireguard_ip: IpAddr\", stats.endpoint as device_endpoint, stats.latest_handshake as \"latest_handshake?\", COALESCE (((NOW() - stats.latest_handshake) < $1 * interval '1 minute'), false) as \"is_active!\" FROM wireguard_network_device wnd JOIN wireguard_network n ON n.id = wnd.wireguard_network_id LEFT JOIN stats on n.id = stats.network WHERE wnd.device_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "network_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "network_name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "gateway_endpoint", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "device_wireguard_ip: IpAddr", + "type_info": "Inet" + }, + { + "ordinal": 4, + "name": "device_endpoint", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "latest_handshake?", + "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "is_active!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Float8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + null + ] + }, + "hash": "efe773da199984a169b636cafabd4ee0967c7dc68a04ec930f863d8f3f34c14a" +} diff --git a/.sqlx/query-e19b29d2e52ad2a2a5c20ce205d5c949d02219fd252d78f7fffa202cc804b4c2.json b/.sqlx/query-f222309aff56e5182091655c0a6c34cddfee21a48f981c3ca22a6e064c18a769.json similarity index 62% rename from .sqlx/query-e19b29d2e52ad2a2a5c20ce205d5c949d02219fd252d78f7fffa202cc804b4c2.json rename to .sqlx/query-f222309aff56e5182091655c0a6c34cddfee21a48f981c3ca22a6e064c18a769.json index e1fe0b4b9..25ae273d7 100644 --- a/.sqlx/query-e19b29d2e52ad2a2a5c20ce205d5c949d02219fd252d78f7fffa202cc804b4c2.json +++ b/.sqlx/query-f222309aff56e5182091655c0a6c34cddfee21a48f981c3ca22a6e064c18a769.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"wireguard_network\" (\"name\",\"address\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\",\"connected_at\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING id", + "query": "INSERT INTO \"wireguard_network\" (\"name\",\"address\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\",\"connected_at\",\"mfa_enabled\",\"keepalive_interval\",\"peer_disconnect_threshold\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) RETURNING id", "describe": { "columns": [ { @@ -19,12 +19,15 @@ "Text", "Text", "InetArray", - "Timestamp" + "Timestamp", + "Bool", + "Int4", + "Int4" ] }, "nullable": [ false ] }, - "hash": "e19b29d2e52ad2a2a5c20ce205d5c949d02219fd252d78f7fffa202cc804b4c2" + "hash": "f222309aff56e5182091655c0a6c34cddfee21a48f981c3ca22a6e064c18a769" } diff --git a/.sqlx/query-d3b425017872becff45f39513c9e09b3d3ba8fc86e97c7f1ff21597a884af06d.json b/.sqlx/query-fbfe27851d858408d81529e6e77190e332d85f9162249ef56c98edc2582c288a.json similarity index 58% rename from .sqlx/query-d3b425017872becff45f39513c9e09b3d3ba8fc86e97c7f1ff21597a884af06d.json rename to .sqlx/query-fbfe27851d858408d81529e6e77190e332d85f9162249ef56c98edc2582c288a.json index 4c499f9f3..cc2c45570 100644 --- a/.sqlx/query-d3b425017872becff45f39513c9e09b3d3ba8fc86e97c7f1ff21597a884af06d.json +++ b/.sqlx/query-fbfe27851d858408d81529e6e77190e332d85f9162249ef56c98edc2582c288a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT wireguard_network_id as network_id, wireguard_ip as \"device_wireguard_ip: IpAddr\"\n FROM wireguard_network_device\n WHERE device_id = $1\n ", + "query": "SELECT wireguard_network_id as network_id, wireguard_ip as \"device_wireguard_ip: IpAddr\" FROM wireguard_network_device WHERE device_id = $1", "describe": { "columns": [ { @@ -24,5 +24,5 @@ false ] }, - "hash": "d3b425017872becff45f39513c9e09b3d3ba8fc86e97c7f1ff21597a884af06d" + "hash": "fbfe27851d858408d81529e6e77190e332d85f9162249ef56c98edc2582c288a" } diff --git a/Cargo.lock b/Cargo.lock index 9f38afd68..5c004874c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,9 +145,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355" [[package]] name = "argon2" @@ -225,18 +225,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -289,7 +289,7 @@ dependencies = [ "futures-util", "http 0.2.11", "http-body 0.4.6", - "hyper 0.14.27", + "hyper 0.14.28", "itoa", "matchit", "memchr", @@ -317,7 +317,7 @@ dependencies = [ "http 1.0.0", "http-body 1.0.0", "http-body-util", - "hyper 1.0.1", + "hyper 1.1.0", "hyper-util", "itoa", "matchit", @@ -667,7 +667,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -941,7 +941,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -965,7 +965,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -976,7 +976,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -1124,7 +1124,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -1529,7 +1529,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -1889,9 +1889,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -1904,7 +1904,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2", "tokio", "tower-service", "tracing", @@ -1913,9 +1913,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f9214f3e703236b221f1a9cd88ec8b4adfa5296de01ab96216361f4692f56" +checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" dependencies = [ "bytes", "futures-channel", @@ -1938,7 +1938,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.11", - "hyper 0.14.27", + "hyper 0.14.28", "rustls", "tokio", "tokio-rustls", @@ -1950,7 +1950,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.27", + "hyper 0.14.28", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -1963,7 +1963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.27", + "hyper 0.14.28", "native-tls", "tokio", "tokio-native-tls", @@ -1971,21 +1971,19 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca339002caeb0d159cc6e023dff48e199f081e42fa039895c7c6f38b37f2e9d" +checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67" dependencies = [ "bytes", "futures-channel", "futures-util", "http 1.0.0", "http-body 1.0.0", - "hyper 1.0.1", + "hyper 1.1.0", "pin-project-lite", - "socket2 0.5.5", + "socket2", "tokio", - "tower", - "tower-service", "tracing", ] @@ -2293,7 +2291,7 @@ dependencies = [ "nom", "once_cell", "quoted_printable", - "socket2 0.5.5", + "socket2", "tokio", "tokio-native-tls", "url", @@ -2458,7 +2456,7 @@ name = "model_derive" version = "0.1.2" dependencies = [ "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -2609,7 +2607,7 @@ dependencies = [ "proc-macro-crate 2.0.1", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -2741,7 +2739,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -2953,7 +2951,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -3032,7 +3030,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -3070,9 +3068,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" [[package]] name = "platforms" @@ -3111,7 +3109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -3233,7 +3231,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.41", + "syn 2.0.42", "tempfile", "which", ] @@ -3248,7 +3246,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -3412,9 +3410,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.22" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ "base64 0.21.5", "bytes", @@ -3426,7 +3424,7 @@ dependencies = [ "h2 0.3.22", "http 0.2.11", "http-body 0.4.6", - "hyper 0.14.27", + "hyper 0.14.28", "hyper-rustls", "hyper-tls", "ipnet", @@ -3824,7 +3822,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -3906,7 +3904,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -4026,16 +4024,6 @@ version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.5" @@ -4343,7 +4331,7 @@ checksum = "f14a349c27ebe59faba22f933c9c734d428da7231e88a247e9d8c61eea964ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -4365,7 +4353,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -4387,9 +4375,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.41" +version = "2.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +checksum = "5b7d0a2c048d661a1a59fcd7355baa232f7ed34e0ee4df2eef3c1c1c0d3852d8" dependencies = [ "proc-macro2", "quote", @@ -4493,7 +4481,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -4508,9 +4496,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" dependencies = [ "deranged", "itoa", @@ -4528,9 +4516,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" dependencies = [ "time-core", ] @@ -4561,9 +4549,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.0" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ "backtrace", "bytes", @@ -4572,7 +4560,7 @@ dependencies = [ "num_cpus", "parking_lot", "pin-project-lite", - "socket2 0.5.5", + "socket2", "tokio-macros", "windows-sys 0.48.0", ] @@ -4595,7 +4583,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -4686,7 +4674,7 @@ dependencies = [ "h2 0.3.22", "http 0.2.11", "http-body 0.4.6", - "hyper 0.14.27", + "hyper 0.14.28", "hyper-timeout", "percent-encoding", "pin-project", @@ -4713,7 +4701,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -4793,7 +4781,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -5109,7 +5097,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", "wasm-bindgen-shared", ] @@ -5143,7 +5131,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5444,9 +5432,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.28" +version = "0.5.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c830786f7720c2fd27a1a0e27a709dbd3c4d009b56d098fc742d4f4eab91fe2" +checksum = "9b5c3db89721d50d0e2a673f5043fc4722f76dcc352d7b1ab8b8288bed4ed2c5" dependencies = [ "memchr", ] @@ -5526,7 +5514,7 @@ checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] [[package]] @@ -5546,5 +5534,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.42", ] diff --git a/migrations/20231220103051_add_preshared_key.down.sql b/migrations/20231220103051_add_preshared_key.down.sql new file mode 100644 index 000000000..8466e6f04 --- /dev/null +++ b/migrations/20231220103051_add_preshared_key.down.sql @@ -0,0 +1 @@ +ALTER TABLE device DROP COLUMN preshared_key; diff --git a/migrations/20231220103051_add_preshared_key.up.sql b/migrations/20231220103051_add_preshared_key.up.sql new file mode 100644 index 000000000..d12c2c1c2 --- /dev/null +++ b/migrations/20231220103051_add_preshared_key.up.sql @@ -0,0 +1 @@ +ALTER TABLE device ADD COLUMN preshared_key text NULL; diff --git a/migrations/20231220112404_update_location_settings.down.sql b/migrations/20231220112404_update_location_settings.down.sql new file mode 100644 index 000000000..899287ebd --- /dev/null +++ b/migrations/20231220112404_update_location_settings.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE wireguard_network DROP COLUMN mfa_enabled; +ALTER TABLE wireguard_network DROP COLUMN keepalive_interval; +ALTER TABLE wireguard_network DROP COLUMN peer_disconnect_threshold; diff --git a/migrations/20231220112404_update_location_settings.up.sql b/migrations/20231220112404_update_location_settings.up.sql new file mode 100644 index 000000000..a0210bfdb --- /dev/null +++ b/migrations/20231220112404_update_location_settings.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE wireguard_network ADD COLUMN mfa_enabled bool NOT NULL DEFAULT false; +ALTER TABLE wireguard_network ADD COLUMN keepalive_interval int4 NOT NULL DEFAULT 25; +ALTER TABLE wireguard_network ADD COLUMN peer_disconnect_threshold int4 NOT NULL DEFAULT 75; diff --git a/proto b/proto index 67ce8c13c..44b0593cd 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 67ce8c13ca9cef8f49b6f8dbcf1dfdf8cc9aa9f4 +Subproject commit 44b0593cdd8a0b7cf44386cf6097add59ef79781 diff --git a/src/db/models/device.rs b/src/db/models/device.rs index f1c9fc9a3..a74df7ce7 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -35,6 +35,7 @@ pub struct Device { pub wireguard_pubkey: String, pub user_id: i64, pub created: NaiveDateTime, + pub preshared_key: Option, } impl Display for Device { @@ -69,11 +70,9 @@ impl DeviceInfo { let device_id = device.get_id()?; let network_info = query_as!( DeviceNetworkInfo, - r#" - SELECT wireguard_network_id as network_id, wireguard_ip as "device_wireguard_ip: IpAddr" - FROM wireguard_network_device - WHERE device_id = $1 - "#, + "SELECT wireguard_network_id as network_id, wireguard_ip as \"device_wireguard_ip: IpAddr\" \ + FROM wireguard_network_device \ + WHERE device_id = $1", device_id ) .fetch_all(executor) @@ -112,23 +111,21 @@ impl UserDevice { if let Some(device_id) = device.id { // fetch device config and connection info for all networks let result = query!( - r#" - WITH stats AS ( - SELECT DISTINCT ON (network) network, endpoint, latest_handshake - FROM wireguard_peer_stats - WHERE device_id = $2 - ORDER BY network, collected_at DESC - ) - SELECT - n.id as network_id, n.name as network_name, n.endpoint as gateway_endpoint, - wnd.wireguard_ip as "device_wireguard_ip: IpAddr", stats.endpoint as device_endpoint, - stats.latest_handshake as "latest_handshake?", - COALESCE (((NOW() - stats.latest_handshake) < $1 * interval '1 minute'), false) as "is_active!" - FROM wireguard_network_device wnd - JOIN wireguard_network n ON n.id = wnd.wireguard_network_id - LEFT JOIN stats on n.id = stats.network - WHERE wnd.device_id = $2 - "#, + "WITH stats AS ( \ + SELECT DISTINCT ON (network) network, endpoint, latest_handshake \ + FROM wireguard_peer_stats \ + WHERE device_id = $2 \ + ORDER BY network, collected_at DESC \ + ) \ + SELECT \ + n.id as network_id, n.name as network_name, n.endpoint as gateway_endpoint, \ + wnd.wireguard_ip as \"device_wireguard_ip: IpAddr\", stats.endpoint as device_endpoint, \ + stats.latest_handshake as \"latest_handshake?\", \ + COALESCE (((NOW() - stats.latest_handshake) < $1 * interval '1 minute'), false) as \"is_active!\" \ + FROM wireguard_network_device wnd \ + JOIN wireguard_network n ON n.id = wnd.wireguard_network_id \ + LEFT JOIN stats on n.id = stats.network \ + WHERE wnd.device_id = $2", WIREGUARD_MAX_HANDSHAKE_MINUTES as f64, device_id, ) @@ -197,10 +194,10 @@ impl WireguardNetworkDevice { E: PgExecutor<'e>, { query!( - "INSERT INTO wireguard_network_device - (device_id, wireguard_network_id, wireguard_ip) - VALUES ($1, $2, $3) - ON CONFLICT ON CONSTRAINT device_network + "INSERT INTO wireguard_network_device \ + (device_id, wireguard_network_id, wireguard_ip) \ + VALUES ($1, $2, $3) \ + ON CONFLICT ON CONSTRAINT device_network \ DO UPDATE SET wireguard_ip = $3", self.device_id, self.wireguard_network_id, @@ -216,11 +213,9 @@ impl WireguardNetworkDevice { E: PgExecutor<'e>, { query!( - r#" - UPDATE wireguard_network_device - SET wireguard_ip = $3 - WHERE device_id = $1 AND wireguard_network_id = $2 - "#, + "UPDATE wireguard_network_device \ + SET wireguard_ip = $3 \ + WHERE device_id = $1 AND wireguard_network_id = $2", self.device_id, self.wireguard_network_id, IpNetwork::from(self.wireguard_ip.clone()) @@ -235,10 +230,8 @@ impl WireguardNetworkDevice { E: PgExecutor<'e>, { query!( - r#" - DELETE FROM wireguard_network_device - WHERE device_id = $1 AND wireguard_network_id = $2 - "#, + "DELETE FROM wireguard_network_device \ + WHERE device_id = $1 AND wireguard_network_id = $2", self.device_id, self.wireguard_network_id, ) @@ -257,9 +250,9 @@ impl WireguardNetworkDevice { { let res = query_as!( Self, - r#"SELECT device_id, wireguard_network_id, wireguard_ip as "wireguard_ip: IpAddr" FROM - wireguard_network_device - WHERE device_id = $1 AND wireguard_network_id = $2"#, + "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\" FROM \ + wireguard_network_device \ + WHERE device_id = $1 AND wireguard_network_id = $2", device_id, network_id ) @@ -274,8 +267,8 @@ impl WireguardNetworkDevice { ) -> Result>, SqlxError> { let result = query_as!( Self, - r#"SELECT device_id, wireguard_network_id, wireguard_ip as "wireguard_ip: IpAddr" - FROM wireguard_network_device WHERE device_id = $1"#, + "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\" \ + FROM wireguard_network_device WHERE device_id = $1", device_id ) .fetch_all(pool) @@ -295,9 +288,9 @@ impl WireguardNetworkDevice { { let res = query_as!( Self, - r#"SELECT device_id, wireguard_network_id, wireguard_ip as "wireguard_ip: IpAddr" FROM - wireguard_network_device - WHERE wireguard_network_id = $1"#, + "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\" FROM \ + wireguard_network_device \ + WHERE wireguard_network_id = $1", network_id ) .fetch_all(executor) @@ -320,13 +313,19 @@ pub enum DeviceError { impl Device { #[must_use] - pub fn new(name: String, wireguard_pubkey: String, user_id: i64) -> Self { + pub fn new( + name: String, + wireguard_pubkey: String, + preshared_key: Option, + user_id: i64, + ) -> Self { Self { id: None, name, wireguard_pubkey, user_id, created: Utc::now().naive_utc(), + preshared_key, } } @@ -392,11 +391,11 @@ impl Device { { query_as!( Self, - r#"SELECT d.id "id?", d.name, d.wireguard_pubkey, d.user_id, d.created - FROM device d - JOIN wireguard_network_device wnd - ON d.id = wnd.device_id - WHERE wnd.wireguard_ip = $1 AND wnd.wireguard_network_id = $2"#, + "SELECT d.id \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created, d.preshared_key \ + FROM device d \ + JOIN wireguard_network_device wnd \ + ON d.id = wnd.device_id \ + WHERE wnd.wireguard_ip = $1 AND wnd.wireguard_network_id = $2", IpNetwork::from(ip), network_id ) @@ -410,7 +409,7 @@ impl Device { { query_as!( Self, - "SELECT id \"id?\", name, wireguard_pubkey, user_id, created \ + "SELECT id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key \ FROM device WHERE wireguard_pubkey = $1", pubkey ) @@ -425,7 +424,7 @@ impl Device { ) -> Result, SqlxError> { query_as!( Self, - "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created \ + "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key \ FROM device JOIN \"user\" ON device.user_id = \"user\".id \ WHERE device.id = $1 AND \"user\".username = $2", id, @@ -442,7 +441,7 @@ impl Device { ) -> Result, SqlxError> { query_as!( Self, - "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created \ + "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key \ FROM device JOIN \"user\" ON device.user_id = \"user\".id \ WHERE device.id = $1 AND \"user\".id = $2", id, @@ -459,11 +458,9 @@ impl Device { ) -> Result, SqlxError> { if let Some(device_id) = self.id { let result = query!( - r#" - SELECT wireguard_ip - FROM wireguard_network_device - WHERE device_id = $1 AND wireguard_network_id = $2 - "#, + "SELECT wireguard_ip \ + FROM wireguard_network_device \ + WHERE device_id = $1 AND wireguard_network_id = $2", device_id, network_id ) @@ -478,7 +475,7 @@ impl Device { pub async fn all_for_username(pool: &DbPool, username: &str) -> Result, SqlxError> { query_as!( Self, - "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created \ + "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key \ FROM device JOIN \"user\" ON device.user_id = \"user\".id \ WHERE \"user\".username = $1", username @@ -633,7 +630,7 @@ mod test { } // Break loop if IP is unassigned and return device if Self::find_by_ip(pool, ip, network_id).await?.is_none() { - let mut device = Self::new(name.clone(), pubkey, user_id); + let mut device = Self::new(name.clone(), pubkey, None, user_id); device.save(pool).await?; info!("Created device: {}", device.name); debug!("For user: {}", device.user_id); diff --git a/src/db/models/enrollment.rs b/src/db/models/enrollment.rs index e7f20161e..165b54769 100644 --- a/src/db/models/enrollment.rs +++ b/src/db/models/enrollment.rs @@ -231,9 +231,9 @@ impl Token { ) -> Result<(), TokenError> { debug!("Deleting unused enrollment tokens for user {user_id}"); let result = query!( - r#"DELETE FROM token - WHERE user_id = $1 - AND used_at IS NULL"#, + "DELETE FROM token \ + WHERE user_id = $1 \ + AND used_at IS NULL", user_id ) .execute(transaction) @@ -252,10 +252,10 @@ impl Token { ) -> Result<(), TokenError> { debug!("Deleting unused password reset tokens for user {user_id}"); let result = query!( - r#"DELETE FROM token - WHERE user_id = $1 - AND token_type = 'PASSWORD_RESET' - AND used_at IS NULL"#, + "DELETE FROM token \ + WHERE user_id = $1 \ + AND token_type = 'PASSWORD_RESET' \ + AND used_at IS NULL", user_id ) .execute(transaction) diff --git a/src/db/models/user.rs b/src/db/models/user.rs index aa3902886..0ad9a3cba 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -451,9 +451,9 @@ impl User { pool: &DbPool, ) -> Result, SqlxError> { let users = query!( - r#" - SELECT id, mfa_enabled, totp_enabled, email_mfa_enabled, mfa_method as "mfa_method: MFAMethod", password_hash FROM "user" - "# + "SELECT id, mfa_enabled, totp_enabled, email_mfa_enabled, \ + mfa_method as \"mfa_method: MFAMethod\", password_hash \ + FROM \"user\"" ) .fetch_all(pool) .await?; @@ -633,10 +633,8 @@ impl User { if let Some(id) = self.id { let devices = query_as!( Device, - r#" - SELECT device.id "id?", name, wireguard_pubkey, user_id, created - FROM device WHERE user_id = $1 - "#, + "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key \ + FROM device WHERE user_id = $1", id ) .fetch_all(pool) diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 2961b4b98..ab58d69ca 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -24,6 +24,9 @@ use crate::{ wg_config::ImportedDevice, }; +pub const DEFAULT_KEEPALIVE_INTERVAL: i32 = 25; +pub const DEFAULT_DISCONNECT_THRESHOLD: i32 = 25; + // Used in process of importing network from wireguard config #[derive(Debug, Clone, Deserialize, Serialize)] pub struct MappedDevice { @@ -79,6 +82,9 @@ pub struct WireguardNetwork { #[model(ref)] pub allowed_ips: Vec, pub connected_at: Option, + pub mfa_enabled: bool, + pub keepalive_interval: i32, + pub peer_disconnect_threshold: i32, } pub struct WireguardKey { @@ -123,6 +129,9 @@ impl WireguardNetwork { endpoint: String, dns: Option, allowed_ips: Vec, + mfa_enabled: bool, + keepalive_interval: i32, + peer_disconnect_threshold: i32, ) -> Result { let prvkey = StaticSecret::random_from_rng(OsRng); let pubkey = PublicKey::from(&prvkey); @@ -137,6 +146,9 @@ impl WireguardNetwork { dns, allowed_ips, connected_at: None, + mfa_enabled, + keepalive_interval, + peer_disconnect_threshold, }) } @@ -154,7 +166,10 @@ impl WireguardNetwork { { let networks = query_as!( WireguardNetwork, - r#"SELECT id as "id?", name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at FROM wireguard_network WHERE name = $1"#, + "SELECT \ + id as \"id?\", name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \ + connected_at, mfa_enabled, keepalive_interval, peer_disconnect_threshold \ + FROM wireguard_network WHERE name = $1", name ) .fetch_all(executor) @@ -297,15 +312,13 @@ impl WireguardNetwork { Some(allowed_groups) => { query_as!( Device, - r#" - SELECT DISTINCT ON (d.id) d.id as "id?", d.name, d.wireguard_pubkey, d.user_id, d.created - FROM device d - JOIN "user" u ON d.user_id = u.id - JOIN group_user gu ON u.id = gu.user_id - JOIN "group" g ON gu.group_id = g.id - WHERE g."name" IN (SELECT * FROM UNNEST($1::text[])) - ORDER BY d.id ASC - "#, + "SELECT DISTINCT ON (d.id) d.id as \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created, d.preshared_key \ + FROM device d \ + JOIN \"user\" u ON d.user_id = u.id \ + JOIN group_user gu ON u.id = gu.user_id \ + JOIN \"group\" g ON gu.group_id = g.id \ + WHERE g.\"name\" IN (SELECT * FROM UNNEST($1::text[])) + ORDER BY d.id ASC", &allowed_groups ) .fetch_all(&mut *transaction) @@ -556,6 +569,7 @@ impl WireguardNetwork { let mut device = Device::new( mapped_device.name.clone(), mapped_device.wireguard_pubkey.clone(), + None, mapped_device.user_id, ); device.save(&mut *transaction).await?; @@ -634,14 +648,12 @@ impl WireguardNetwork { ) -> Result, SqlxError> { let stats = query_as!( WireguardPeerStats, - r#" - SELECT id "id?", device_id "device_id!", collected_at "collected_at!", network "network!", - endpoint, upload "upload!", download "download!", latest_handshake "latest_handshake!", allowed_ips - FROM wireguard_peer_stats - WHERE device_id = $1 AND network = $2 - ORDER BY collected_at DESC - LIMIT 1 - "#, + "SELECT id \"id?\", device_id \"device_id!\", collected_at \"collected_at!\", network \"network!\", \ + endpoint, upload \"upload!\", download \"download!\", latest_handshake \"latest_handshake!\", allowed_ips \ + FROM wireguard_peer_stats \ + WHERE device_id = $1 AND network = $2 \ + ORDER BY collected_at DESC \ + LIMIT 1", device_id, self.id ) @@ -673,17 +685,15 @@ impl WireguardNetwork { device_id: i64, ) -> Result, SqlxError> { let connected_at = query_scalar!( - r#" - SELECT - latest_handshake "latest_handshake: NaiveDateTime" - FROM wireguard_peer_stats_view - WHERE device_id = $1 - AND latest_handshake IS NOT NULL - AND (latest_handshake_diff > $2 * interval '1 minute' OR latest_handshake_diff IS NULL) - AND network = $3 - ORDER BY collected_at DESC - LIMIT 1 - "#, + "SELECT \ + latest_handshake \"latest_handshake: NaiveDateTime\" \ + FROM wireguard_peer_stats_view \ + WHERE device_id = $1 \ + AND latest_handshake IS NOT NULL \ + AND (latest_handshake_diff > $2 * interval '1 minute' OR latest_handshake_diff IS NULL) \ + AND network = $3 \ + ORDER BY collected_at DESC \ + LIMIT 1", device_id, WIREGUARD_MAX_HANDSHAKE_MINUTES as f64, self.id @@ -714,19 +724,17 @@ impl WireguardNetwork { .collect::>() .join(","); let query = format!( - r#" - SELECT - device_id, - date_trunc($1, collected_at) as collected_at, - cast(sum(download) as bigint) as download, - cast(sum(upload) as bigint) as upload - FROM wireguard_peer_stats_view - WHERE device_id IN ({device_ids}) - AND collected_at >= $2 - AND network = $3 - GROUP BY 1, 2 - ORDER BY 1, 2 - "# + "SELECT \ + device_id, \ + date_trunc($1, collected_at) as collected_at, \ + cast(sum(download) as bigint) as download, \ + cast(sum(upload) as bigint) as upload \ + FROM wireguard_peer_stats_view \ + WHERE device_id IN ({device_ids}) \ + AND collected_at >= $2 \ + AND network = $3 \ + GROUP BY 1, 2 \ + ORDER BY 1, 2" ); let stats: Vec = query_as(&query) .bind(aggregation.fstring()) @@ -769,18 +777,16 @@ impl WireguardNetwork { // Retrieve connected devices from database let devices = query_as!( Device, - r#" - WITH s AS ( - SELECT DISTINCT ON (device_id) * - FROM wireguard_peer_stats - ORDER BY device_id, latest_handshake DESC - ) - SELECT - d.id "id?", d.name, d.wireguard_pubkey, d.user_id, d.created - FROM device d - JOIN s ON d.id = s.device_id - WHERE s.latest_handshake >= $1 AND s.network = $2 - "#, + "WITH s AS ( \ + SELECT DISTINCT ON (device_id) * \ + FROM wireguard_peer_stats \ + ORDER BY device_id, latest_handshake DESC \ + ) \ + SELECT \ + d.id \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created, d.preshared_key \ + FROM device d \ + JOIN s ON d.id = s.device_id \ + WHERE s.latest_handshake >= $1 AND s.network = $2", oldest_handshake, self.id, ) @@ -813,15 +819,13 @@ impl WireguardNetwork { ) -> Result { let activity_stats = query_as!( WireguardNetworkActivityStats, - r#" - SELECT - COALESCE(COUNT(DISTINCT(u.id)), 0) as "active_users!", - COALESCE(COUNT(DISTINCT(s.device_id)), 0) as "active_devices!" - FROM "user" u - JOIN device d ON d.user_id = u.id - JOIN wireguard_peer_stats s ON s.device_id = d.id - WHERE latest_handshake >= $1 AND s.network = $2 - "#, + "SELECT \ + COALESCE(COUNT(DISTINCT(u.id)), 0) as \"active_users!\", \ + COALESCE(COUNT(DISTINCT(s.device_id)), 0) as \"active_devices!\" \ + FROM \"user\" u \ + JOIN device d ON d.user_id = u.id \ + JOIN wireguard_peer_stats s ON s.device_id = d.id \ + WHERE latest_handshake >= $1 AND s.network = $2", from, self.id, ) @@ -839,15 +843,13 @@ impl WireguardNetwork { (Utc::now() - Duration::minutes(WIREGUARD_MAX_HANDSHAKE_MINUTES.into())).naive_utc(); let activity_stats = query_as!( WireguardNetworkActivityStats, - r#" - SELECT - COALESCE(COUNT(DISTINCT(u.id)), 0) as "active_users!", - COALESCE(COUNT(DISTINCT(s.device_id)), 0) as "active_devices!" - FROM "user" u - JOIN device d ON d.user_id = u.id - JOIN wireguard_peer_stats s ON s.device_id = d.id - WHERE latest_handshake >= $1 AND s.network = $2 - "#, + "SELECT \ + COALESCE(COUNT(DISTINCT(u.id)), 0) as \"active_users!\", \ + COALESCE(COUNT(DISTINCT(s.device_id)), 0) as \"active_devices!\" \ + FROM \"user\" u \ + JOIN device d ON d.user_id = u.id \ + JOIN wireguard_peer_stats s ON s.device_id = d.id \ + WHERE latest_handshake >= $1 AND s.network = $2", from, self.id ) @@ -866,16 +868,14 @@ impl WireguardNetwork { ) -> Result, SqlxError> { let stats = query_as!( WireguardStatsRow, - r#" - SELECT - date_trunc($1, collected_at) "collected_at: NaiveDateTime", - cast(sum(upload) AS bigint) upload, cast(sum(download) AS bigint) download - FROM wireguard_peer_stats_view - WHERE collected_at >= $2 AND network = $3 - GROUP BY 1 - ORDER BY 1 - LIMIT $4 - "#, + "SELECT \ + date_trunc($1, collected_at) \"collected_at: NaiveDateTime\", \ + cast(sum(upload) AS bigint) upload, cast(sum(download) AS bigint) download \ + FROM wireguard_peer_stats_view \ + WHERE collected_at >= $2 AND network = $3 \ + GROUP BY 1 \ + ORDER BY 1 \ + LIMIT $4", aggregation.fstring(), from, self.id, @@ -922,6 +922,9 @@ impl Default for WireguardNetwork { dns: Option::default(), allowed_ips: Vec::default(), connected_at: Option::default(), + mfa_enabled: false, + keepalive_interval: DEFAULT_KEEPALIVE_INTERVAL, + peer_disconnect_threshold: DEFAULT_DISCONNECT_THRESHOLD, } } } @@ -1102,7 +1105,7 @@ mod test { None, ); user.save(&pool).await.unwrap(); - let mut device = Device::new(String::new(), String::new(), user.id.unwrap()); + let mut device = Device::new(String::new(), String::new(), None, user.id.unwrap()); device.save(&pool).await.unwrap(); // insert stats @@ -1152,7 +1155,7 @@ mod test { None, ); user.save(&pool).await.unwrap(); - let mut device = Device::new(String::new(), String::new(), user.id.unwrap()); + let mut device = Device::new(String::new(), String::new(), None, user.id.unwrap()); device.save(&pool).await.unwrap(); // insert stats diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 5b055e2d5..a7fe62836 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -320,7 +320,7 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { error!("Invalid pubkey {}", request.pubkey); Status::invalid_argument("invalid pubkey") })?; - let mut device = Device::new(request.name, request.pubkey, enrollment.user_id); + let mut device = Device::new(request.name, request.pubkey, None, enrollment.user_id); let mut transaction = self.pool.begin().await.map_err(|_| { error!("Failed to begin transaction"); diff --git a/src/grpc/gateway.rs b/src/grpc/gateway.rs index 6b35d5387..6f5b83bbd 100644 --- a/src/grpc/gateway.rs +++ b/src/grpc/gateway.rs @@ -5,7 +5,7 @@ use std::{ }; use chrono::{NaiveDateTime, Utc}; -use sqlx::{query_as, Error as SqlxError, PgExecutor}; +use sqlx::{query, Error as SqlxError, PgExecutor}; use tokio::{ sync::{ broadcast::{Receiver as BroadcastReceiver, Sender}, @@ -41,18 +41,30 @@ impl WireguardNetwork { E: PgExecutor<'e>, { debug!("Fetching all peers for network {}", self.id.unwrap()); - let result = query_as!( - Peer, - "SELECT d.wireguard_pubkey as pubkey, array[host(wnd.wireguard_ip)] as \"allowed_ips!: Vec\" \ + let rows = query!( + "SELECT d.wireguard_pubkey as pubkey, preshared_key, \ + array[host(wnd.wireguard_ip)] as \"allowed_ips!: Vec\" \ FROM wireguard_network_device wnd \ JOIN device d ON wnd.device_id = d.id \ WHERE wireguard_network_id = $1 \ ORDER BY d.id ASC", - self.id + self.id, ) .fetch_all(executor) .await?; + // keepalive has to be added manually because Postgres + // doesn't support unsigned integers + let result = rows + .into_iter() + .map(|row| Peer { + pubkey: row.pubkey, + allowed_ips: row.allowed_ips, + preshared_key: row.preshared_key, + keepalive_interval: Some(self.keepalive_interval as u32), + }) + .collect(); + Ok(result) } } @@ -218,6 +230,10 @@ impl GatewayUpdatesHandler { Peer { pubkey: device.device.wireguard_pubkey, allowed_ips: vec![network_info.device_wireguard_ip.to_string()], + preshared_key: device.device.preshared_key, + keepalive_interval: Some( + self.network.keepalive_interval as u32, + ), }, 0, ) @@ -238,6 +254,10 @@ impl GatewayUpdatesHandler { Peer { pubkey: device.device.wireguard_pubkey, allowed_ips: vec![network_info.device_wireguard_ip.to_string()], + preshared_key: device.device.preshared_key, + keepalive_interval: Some( + self.network.keepalive_interval as u32, + ), }, 1, ) @@ -360,6 +380,8 @@ impl GatewayUpdatesHandler { update: Some(update::Update::Peer(Peer { pubkey: peer_pubkey.into(), allowed_ips: Vec::new(), + preshared_key: None, + keepalive_interval: None, })), })) .await diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index 9387dd38b..7096e2a88 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -42,6 +42,9 @@ pub struct WireguardNetworkData { pub allowed_ips: Option, pub dns: Option, pub allowed_groups: Vec, + pub mfa_enabled: bool, + pub keepalive_interval: i32, + pub peer_disconnect_threshold: i32, } impl WireguardNetworkData { @@ -98,6 +101,9 @@ pub async fn create_network( data.endpoint, data.dns, allowed_ips, + data.mfa_enabled, + data.keepalive_interval, + data.peer_disconnect_threshold, ) .map_err(|_| WebError::Serialization("Invalid network address".into()))?; @@ -167,6 +173,10 @@ pub async fn modify_network( network.port = data.port; network.dns = data.dns; network.address = data.address; + network.mfa_enabled = data.mfa_enabled; + network.keepalive_interval = data.keepalive_interval; + network.peer_disconnect_threshold = data.peer_disconnect_threshold; + network.save(&mut *transaction).await?; network .set_allowed_groups(&mut transaction, data.allowed_groups) @@ -478,7 +488,7 @@ pub async fn add_device( let Some(user_id) = user.id else { return Err(WebError::ModelError("User has no id".to_string())); }; - let mut device = Device::new(add_device.name, add_device.wireguard_pubkey, user_id); + let mut device = Device::new(add_device.name, add_device.wireguard_pubkey, None, user_id); let mut transaction = appstate.pool.begin().await?; device.save(&mut *transaction).await?; diff --git a/src/lib.rs b/src/lib.rs index f84ba0bde..ed0fdc81b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,6 @@ use std::{ sync::{Arc, Mutex}, }; -use crate::handlers::ssh_authorized_keys::get_authorized_keys; use anyhow::anyhow; use axum::{ handler::HandlerWithoutStateExt, @@ -12,11 +11,7 @@ use axum::{ routing::{delete, get, patch, post, put}, serve, Extension, Router, }; -use handlers::{ - group::{create_group, delete_group, modify_group}, - settings::{get_settings_essentials, patch_settings, test_ldap_settings}, - user::reset_password, -}; + use ipnetwork::IpNetwork; use secrecy::ExposeSecret; use tokio::{ @@ -38,7 +33,11 @@ use self::{ appstate::AppState, auth::{Claims, ClaimsType}, config::{DefGuardConfig, InitVpnLocationArgs}, - db::{init_db, AppEvent, DbPool, Device, GatewayEvent, User, WireguardNetwork}, + db::{ + init_db, + models::wireguard::{DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL}, + AppEvent, DbPool, Device, GatewayEvent, User, WireguardNetwork, + }, handlers::{ auth::{ authenticate, email_mfa_code, email_mfa_disable, email_mfa_enable, email_mfa_init, @@ -47,15 +46,22 @@ use self::{ webauthn_finish, webauthn_init, webauthn_start, }, forward_auth::forward_auth, - group::{add_group_member, get_group, list_groups, remove_group_member}, + group::{ + add_group_member, create_group, delete_group, get_group, list_groups, modify_group, + remove_group_member, + }, mail::{send_support_data, test_mail}, - settings::{get_settings, set_default_branding, update_settings}, + settings::{ + get_settings, get_settings_essentials, patch_settings, set_default_branding, + test_ldap_settings, update_settings, + }, + ssh_authorized_keys::get_authorized_keys, support::{configuration, logs}, user::{ add_user, change_password, change_self_password, delete_authorized_app, delete_security_key, delete_user, delete_wallet, get_user, list_users, me, modify_user, - set_wallet, start_enrollment, start_remote_desktop_configuration, update_wallet, - username_available, wallet_challenge, + reset_password, set_wallet, start_enrollment, start_remote_desktop_configuration, + update_wallet, username_available, wallet_challenge, }, webhooks::{ add_webhook, change_enabled, change_webhook, delete_webhook, get_webhook, list_webhooks, @@ -407,6 +413,9 @@ pub async fn init_dev_env(config: &DefGuardConfig) { "0.0.0.0".to_string(), None, vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::new(10, 1, 1, 0)), 24).unwrap()], + false, + DEFAULT_KEEPALIVE_INTERVAL, + DEFAULT_DISCONNECT_THRESHOLD, ) .expect("Could not create network"); network.pubkey = "zGMeVGm9HV9I4wSKF9AXmYnnAIhDySyqLMuKpcfIaQo=".to_string(); @@ -432,6 +441,7 @@ pub async fn init_dev_env(config: &DefGuardConfig) { let mut device = Device::new( "TestDevice".to_string(), "gQYL5eMeFDj0R+lpC7oZyIl0/sNVmQDC6ckP7husZjc=".to_string(), + None, 1, ); device @@ -486,6 +496,9 @@ pub async fn init_vpn_location( args.endpoint.clone(), args.dns.clone(), args.allowed_ips.clone(), + false, + DEFAULT_KEEPALIVE_INTERVAL, + DEFAULT_DISCONNECT_THRESHOLD, )?; network.save(pool).await?; let network_id = network.get_id()?; diff --git a/src/wg_config.rs b/src/wg_config.rs index c1b480a3f..c4560ef06 100644 --- a/src/wg_config.rs +++ b/src/wg_config.rs @@ -1,10 +1,16 @@ -use crate::db::{models::wireguard::WireguardNetworkError, Device, WireguardNetwork}; use base64::{prelude::BASE64_STANDARD, DecodeError, Engine}; use ipnetwork::{IpNetwork, IpNetworkError}; use std::{array::TryFromSliceError, net::IpAddr}; use thiserror::Error; use x25519_dalek::{PublicKey, StaticSecret}; +use crate::db::{ + models::wireguard::{ + WireguardNetworkError, DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL, + }, + Device, WireguardNetwork, +}; + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ImportedDevice { pub user_id: Option, @@ -81,6 +87,9 @@ pub fn parse_wireguard_config( String::new(), dns, vec![allowed_ips], + false, + DEFAULT_KEEPALIVE_INTERVAL, + DEFAULT_DISCONNECT_THRESHOLD, )?; network.pubkey = pubkey; network.prvkey = prvkey.to_string(); diff --git a/src/wireguard_stats_purge.rs b/src/wireguard_stats_purge.rs index b053420ed..2280a075f 100644 --- a/src/wireguard_stats_purge.rs +++ b/src/wireguard_stats_purge.rs @@ -27,13 +27,12 @@ impl WireguardPeerStats { - ChronoDuration::from_std(stats_purge_threshold).expect("Failed to parse duration")) .naive_utc(); let result = query!( - r#"DELETE FROM wireguard_peer_stats - WHERE collected_at < $1 - AND (device_id, network, collected_at) NOT IN ( - SELECT device_id, network, MAX(collected_at) - FROM wireguard_peer_stats - GROUP BY device_id, network - )"#, + "DELETE FROM wireguard_peer_stats \ + WHERE collected_at < $1 \ + AND (device_id, network, collected_at) NOT IN ( \ + SELECT device_id, network, MAX(collected_at) \ + FROM wireguard_peer_stats \ + GROUP BY device_id, network)", threshold ) .execute(pool) diff --git a/tests/user.rs b/tests/user.rs index dfcaf57a5..b1ecd55f8 100644 --- a/tests/user.rs +++ b/tests/user.rs @@ -569,6 +569,9 @@ fn make_network() -> Value { "allowed_ips": "10.1.1.0/24", "dns": "1.1.1.1", "allowed_groups": [], + "mfa_enabled": false, + "keepalive_interval": 25, + "peer_disconnect_threshold": 75 }) } diff --git a/tests/wireguard.rs b/tests/wireguard.rs index 5d4951d84..c999da6cd 100644 --- a/tests/wireguard.rs +++ b/tests/wireguard.rs @@ -1,7 +1,13 @@ mod common; use defguard::{ - db::{models::device::WireguardNetworkDevice, Device, GatewayEvent, WireguardNetwork}, + db::{ + models::{ + device::WireguardNetworkDevice, + wireguard::{DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL}, + }, + Device, GatewayEvent, WireguardNetwork, + }, handlers::{wireguard::WireguardNetworkData, Auth}, }; use matches::assert_matches; @@ -19,6 +25,9 @@ fn make_network() -> Value { "allowed_ips": "10.1.1.0/24", "dns": "1.1.1.1", "allowed_groups": [], + "mfa_enabled": false, + "keepalive_interval": 25, + "peer_disconnect_threshold": 75 }) } @@ -53,6 +62,9 @@ async fn test_network() { allowed_ips: Some("10.1.1.0/24".into()), dns: None, allowed_groups: vec![], + mfa_enabled: false, + keepalive_interval: DEFAULT_KEEPALIVE_INTERVAL, + peer_disconnect_threshold: DEFAULT_DISCONNECT_THRESHOLD, }; let response = client .put(format!("/api/v1/network/{}", network.id.unwrap())) diff --git a/tests/wireguard_network_allowed_groups.rs b/tests/wireguard_network_allowed_groups.rs index bf0a48569..4b8c53ab9 100644 --- a/tests/wireguard_network_allowed_groups.rs +++ b/tests/wireguard_network_allowed_groups.rs @@ -30,6 +30,7 @@ async fn setup_test_users(pool: &DbPool) -> (Vec, Vec) { let mut admin_device = Device::new( "admin device".into(), "nst4lmZz9kPTq6OdeQq2G2th3n+QneHKmG1wJJ3Jrq0=".into(), + None, admin_user.id.unwrap(), ); admin_device.save(pool).await.unwrap(); @@ -45,6 +46,7 @@ async fn setup_test_users(pool: &DbPool) -> (Vec, Vec) { let mut test_device = Device::new( "test device".into(), "wYOt6ImBaQ3BEMQ3Xf5P5fTnbqwOvjcqYkkSBt+1xOg=".into(), + None, test_user.id.unwrap(), ); test_device.save(pool).await.unwrap(); @@ -68,6 +70,7 @@ async fn setup_test_users(pool: &DbPool) -> (Vec, Vec) { let mut other_device = Device::new( "other device".into(), "v2U14sjNN4tOYD3P15z0WkjriKY9Hl85I3vIEPomrYs=".into(), + None, other_user.id.unwrap(), ); other_device.save(pool).await.unwrap(); @@ -87,6 +90,7 @@ async fn setup_test_users(pool: &DbPool) -> (Vec, Vec) { let mut non_group_device = Device::new( "non group device".into(), "6xmL/jRuxmzQ3J2/kVZnKnh+6dwODcEEczmmkIKU4sM=".into(), + None, non_group_user.id.unwrap(), ); non_group_device.save(pool).await.unwrap(); @@ -118,6 +122,9 @@ async fn test_create_new_network() { "allowed_ips": "10.1.1.0/24", "dns": "1.1.1.1", "allowed_groups": ["allowed group"], + "mfa_enabled": false, + "keepalive_interval": 25, + "peer_disconnect_threshold": 75 })) .send() .await; @@ -157,6 +164,9 @@ async fn test_modify_network() { "allowed_ips": "10.1.1.0/24", "dns": "1.1.1.1", "allowed_groups": [], + "mfa_enabled": false, + "keepalive_interval": 25, + "peer_disconnect_threshold": 75 })) .send() .await; @@ -185,6 +195,9 @@ async fn test_modify_network() { "allowed_ips": "10.1.1.0/24", "dns": "1.1.1.1", "allowed_groups": ["allowed group"], + "mfa_enabled": false, + "keepalive_interval": 25, + "peer_disconnect_threshold": 75 })) .send() .await; @@ -207,6 +220,9 @@ async fn test_modify_network() { "allowed_ips": "10.1.1.0/24", "dns": "1.1.1.1", "allowed_groups": ["allowed group", "not allowed group"], + "mfa_enabled": false, + "keepalive_interval": 25, + "peer_disconnect_threshold": 75 })) .send() .await; @@ -230,6 +246,9 @@ async fn test_modify_network() { "allowed_ips": "10.1.1.0/24", "dns": "1.1.1.1", "allowed_groups": ["not allowed group"], + "mfa_enabled": false, + "keepalive_interval": 25, + "peer_disconnect_threshold": 75 })) .send() .await; @@ -252,6 +271,9 @@ async fn test_modify_network() { "allowed_ips": "10.1.1.0/24", "dns": "1.1.1.1", "allowed_groups": [], + "mfa_enabled": false, + "keepalive_interval": 25, + "peer_disconnect_threshold": 75 })) .send() .await; @@ -487,6 +509,9 @@ async fn test_modify_user() { "allowed_ips": "10.1.1.0/24", "dns": "1.1.1.1", "allowed_groups": ["allowed group"], + "mfa_enabled": false, + "keepalive_interval": 25, + "peer_disconnect_threshold": 75 })) .send() .await; diff --git a/tests/wireguard_network_import.rs b/tests/wireguard_network_import.rs index ac80028c5..aeeb7e4f8 100644 --- a/tests/wireguard_network_import.rs +++ b/tests/wireguard_network_import.rs @@ -1,7 +1,13 @@ mod common; use defguard::{ - db::{models::device::UserDevice, Device, GatewayEvent, WireguardNetwork}, + db::{ + models::{ + device::UserDevice, + wireguard::{DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL}, + }, + Device, GatewayEvent, WireguardNetwork, + }, handlers::{wireguard::ImportedNetworkData, Auth}, }; use matches::assert_matches; @@ -46,6 +52,9 @@ async fn test_config_import() { String::new(), None, vec![], + false, + DEFAULT_KEEPALIVE_INTERVAL, + DEFAULT_DISCONNECT_THRESHOLD, ) .unwrap(); initial_network.save(&pool).await.unwrap(); @@ -56,6 +65,7 @@ async fn test_config_import() { let mut device_1 = Device::new( "test device".into(), "l07+qPWs4jzW3Gp1DKbHgBMRRm4Jg3q2BJxw0ZYl6c4=".into(), + None, 1, ); device_1.save(&mut *transaction).await.unwrap(); @@ -67,6 +77,7 @@ async fn test_config_import() { let mut device_2 = Device::new( "another test device".into(), "v2U14sjNN4tOYD3P15z0WkjriKY9Hl85I3vIEPomrYs=".into(), + None, 1, ); device_2.save(&mut *transaction).await.unwrap(); diff --git a/tests/wireguard_network_stats.rs b/tests/wireguard_network_stats.rs index 2d909bd66..71f69f97f 100644 --- a/tests/wireguard_network_stats.rs +++ b/tests/wireguard_network_stats.rs @@ -24,6 +24,9 @@ fn make_network() -> Value { "allowed_ips": "10.1.1.0/24", "dns": "1.1.1.1", "allowed_groups": [], + "mfa_enabled": false, + "keepalive_interval": 25, + "peer_disconnect_threshold": 75 }) } diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index 49ec386ef..deed5ae3b 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -1282,6 +1282,15 @@ const en: BaseTranslation = { label: 'Allowed groups', placeholder: 'All groups', }, + mfa_enabled: { + label: 'Require MFA for this Location', + }, + keepalive_interval: { + label: 'Keepalive interval', + }, + peer_disconnect_threshold: { + label: 'Peer disconnect threshold', + }, }, controls: { submit: 'Save changes', diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index ed43021f8..1d30e200b 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -2999,6 +2999,24 @@ type RootTranslation = { */ placeholder: string } + mfa_enabled: { + /** + * R​e​q​u​i​r​e​ ​M​F​A​ ​f​o​r​ ​t​h​i​s​ ​L​o​c​a​t​i​o​n + */ + label: string + } + keepalive_interval: { + /** + * K​e​e​p​a​l​i​v​e​ ​i​n​t​e​r​v​a​l + */ + label: string + } + peer_disconnect_threshold: { + /** + * P​e​e​r​ ​d​i​s​c​o​n​n​e​c​t​ ​t​h​r​e​s​h​o​l​d + */ + label: string + } } controls: { /** @@ -6507,6 +6525,24 @@ export type TranslationFunctions = { */ placeholder: () => LocalizedString } + mfa_enabled: { + /** + * Require MFA for this Location + */ + label: () => LocalizedString + } + keepalive_interval: { + /** + * Keepalive interval + */ + label: () => LocalizedString + } + peer_disconnect_threshold: { + /** + * Peer disconnect threshold + */ + label: () => LocalizedString + } } controls: { /** diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index 75affc6dc..d4327383c 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -1267,6 +1267,15 @@ Uwaga, konfiguracje tutaj podane, nie posiadają twojego klucza prywatnego. Musi label: 'Dozwolone grupy', placeholder: 'Wszystkie grupy', }, + mfa_enabled: { + label: 'Wymagaj MFA dla tej lokalizacji', + }, + keepalive_interval: { + label: 'Utrzymanie połączenia', + }, + peer_disconnect_threshold: { + label: 'Peer disconnect threshold', + }, }, controls: { submit: 'Zapisz zmiany', diff --git a/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx b/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx index b68529135..a4ddc9ae3 100644 --- a/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx +++ b/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx @@ -9,6 +9,7 @@ import * as yup from 'yup'; import { shallow } from 'zustand/shallow'; import { useI18nContext } from '../../../i18n/i18n-react'; +import { FormCheckBox } from '../../../shared/defguard-ui/components/Form/FormCheckBox/FormCheckBox.tsx'; import { FormInput } from '../../../shared/defguard-ui/components/Form/FormInput/FormInput'; import { FormSelect } from '../../../shared/defguard-ui/components/Form/FormSelect/FormSelect'; import { MessageBox } from '../../../shared/defguard-ui/components/Layout/MessageBox/MessageBox'; @@ -34,6 +35,9 @@ type FormFields = { allowed_groups: string[]; name: string; dns: string; + mfa_enabled: boolean; + keepalive_interval: number; + peer_disconnect_threshold: number; }; const defaultValues: FormFields = { @@ -44,6 +48,9 @@ const defaultValues: FormFields = { allowed_ips: '', allowed_groups: [], dns: '', + mfa_enabled: false, + keepalive_interval: 25, + peer_disconnect_threshold: 75, }; const networkToForm = (data?: Network): FormFields => { @@ -181,6 +188,17 @@ export const NetworkEditForm = () => { } return validateIpOrDomainList(val, ',', true); }), + mfa_enabled: yup.boolean().required(LL.form.error.required()), + keepalive_interval: yup + .number() + .positive() + .min(1) + .required(LL.form.error.required()), + peer_disconnect_threshold: yup + .number() + .positive() + .min(1) + .required(LL.form.error.required()), }) .required(); @@ -275,6 +293,19 @@ export const NetworkEditForm = () => { displayValue: titleCase(val), })} /> + + + diff --git a/web/src/pages/network/style.scss b/web/src/pages/network/style.scss index 509cc9399..cc62fa838 100644 --- a/web/src/pages/network/style.scss +++ b/web/src/pages/network/style.scss @@ -118,6 +118,10 @@ margin-bottom: 32px; } } + + & > .form-checkbox { + margin-bottom: 25px; + } } } } diff --git a/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx b/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx index 02e32691a..c9370c1bd 100644 --- a/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx +++ b/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx @@ -8,6 +8,7 @@ import * as yup from 'yup'; import { shallow } from 'zustand/shallow'; import { useI18nContext } from '../../../../i18n/i18n-react'; +import { FormCheckBox } from '../../../../shared/defguard-ui/components/Form/FormCheckBox/FormCheckBox.tsx'; import { FormInput } from '../../../../shared/defguard-ui/components/Form/FormInput/FormInput'; import { FormSelect } from '../../../../shared/defguard-ui/components/Form/FormSelect/FormSelect'; import { Card } from '../../../../shared/defguard-ui/components/Layout/Card/Card'; @@ -130,6 +131,17 @@ export const WizardNetworkConfiguration = () => { return validateIpOrDomainList(val, ',', true); }), allowed_groups: yup.array().optional(), + mfa_enabled: yup.boolean().required(LL.form.error.required()), + keepalive_interval: yup + .number() + .positive() + .min(1) + .required(LL.form.error.required()), + peer_disconnect_threshold: yup + .number() + .positive() + .min(1) + .required(LL.form.error.required()), }) .required(), [LL.form.error], @@ -217,6 +229,19 @@ export const WizardNetworkConfiguration = () => { displayValue: titleCase(group), })} /> + + + diff --git a/web/src/pages/wizard/components/WizardNetworkConfiguration/style.scss b/web/src/pages/wizard/components/WizardNetworkConfiguration/style.scss index 96f866a7d..5d705890a 100644 --- a/web/src/pages/wizard/components/WizardNetworkConfiguration/style.scss +++ b/web/src/pages/wizard/components/WizardNetworkConfiguration/style.scss @@ -20,5 +20,9 @@ & > .message-box { margin-bottom: 25px; } + + & > .form-checkbox { + margin-bottom: 25px; + } } } diff --git a/web/src/pages/wizard/hooks/useWizardStore.ts b/web/src/pages/wizard/hooks/useWizardStore.ts index 3facee36b..5d19275fb 100644 --- a/web/src/pages/wizard/hooks/useWizardStore.ts +++ b/web/src/pages/wizard/hooks/useWizardStore.ts @@ -25,6 +25,9 @@ const defaultValues: StoreFields = { allowed_ips: '', allowed_groups: [], dns: '', + mfa_enabled: false, + keepalive_interval: 25, + peer_disconnect_threshold: 75, }, }; @@ -76,6 +79,9 @@ type StoreFields = { allowed_ips: string; allowed_groups: string[]; dns?: string; + mfa_enabled: boolean; + keepalive_interval: number; + peer_disconnect_threshold: number; }; }; diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index ecacd44ea..cbc797c28 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -119,6 +119,9 @@ export interface Network { allowed_ips?: string[]; allowed_groups?: string[]; dns?: string; + mfa_enabled: boolean; + keepalive_interval: number; + peer_disconnect_threshold: number; } export type ModifyNetworkRequest = { From d9bdd105fd44840e966745081dde3bc74f74fdfd Mon Sep 17 00:00:00 2001 From: Maciek Date: Wed, 27 Dec 2023 17:06:01 +0100 Subject: [PATCH 10/26] feat: disconnect unauthorized devices on gateway (#491) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * send only allowed peers to gateway * add docs * WIP: preparing disconnect task * lint fix * finish disconnect process * use a separate transaction for each device * update query data * disregard is_authorized flag in non-MFA locations * store a preshared_key for each location * store a preshared_key for each location * change default value * update dependencies * formatting * update query data * add newline * Update src/wireguard_peer_disconnect.rs Co-authored-by: Adam --------- Co-authored-by: Maciej Wójcik Co-authored-by: Adam --- ...9c330d76eb5d0a34012bd75395b13e3ff659.json} | 12 +- ...d649018244de3c7f23d98bfab71faa1a9fae1.json | 92 ++++++++++ ...7916b7b220d6aa1fe8bb6690f85ed1cd5666.json} | 12 +- ...0cb5b5e76fbcf424a79b94862f623ea975fc.json} | 12 +- ...98a7aaba2beb19645b720eefa73e4dcac942.json} | 12 +- ...582a6face350a9c4c11db8b32fc251605cd6.json} | 16 +- ...ecafa1166f16151c69b465cd49f334de7190.json} | 17 +- ...cd907893fedd70f7d752521d515230397f3ee.json | 47 +++++ ...38d6f74286fd5660c1ccae20ce43617bbc8f.json} | 12 +- ...f8eec06d64af5886e840bea1b32d2be10ca0.json} | 7 +- ...561dfa68bc80db59bc000dc217ffa639b53b.json} | 12 +- ...a69451102dae52ffb5a4df99e160a1ec8907.json} | 12 +- ...412e872ae9c96792ae9722f66ccfb24f0a144.json | 17 ++ ...d914a67400cb4cce9a8bf86227ffe7d42eda.json} | 7 +- ...f9f7601e1128b15fc0ecc7676cd5aa01c88c.json} | 12 +- ...deaf8eb6824c4246d4135274e760b6af73e0.json} | 7 +- ...ed6027dd5c770a1330689a029cc31f731bf33.json | 16 -- ...47b246deb6f92a5790a4fdd0d5733defbb57.json} | 12 +- ...76a2d14a75d04f4ea5474e06adc9466a544d.json} | 7 +- ...7ce7cc874d9ad8b3119977168f601b3e8072.json} | 12 +- ...79ed4bafb1ee079fd8b23ac9c2ef95db2312.json} | 12 +- ...edea0baba7447d4557f681f26c666a2c47bc.json} | 17 +- Cargo.lock | 160 ++++++++---------- ...094917_support_gateway_disconnect.down.sql | 1 + ...22094917_support_gateway_disconnect.up.sql | 1 + .../20231227091628_fix_preshared_key.down.sql | 4 + .../20231227091628_fix_preshared_key.up.sql | 4 + src/bin/defguard.rs | 4 +- src/db/models/device.rs | 52 +++--- src/db/models/user.rs | 2 +- src/db/models/wireguard.rs | 37 ++-- src/grpc/enrollment.rs | 2 +- src/grpc/gateway.rs | 13 +- src/handlers/wireguard.rs | 3 +- src/lib.rs | 2 +- src/wireguard_peer_disconnect.rs | 126 ++++++++++++++ tests/wireguard_network_allowed_groups.rs | 4 - tests/wireguard_network_import.rs | 2 - 38 files changed, 528 insertions(+), 271 deletions(-) rename .sqlx/{query-5d629b503d4d9f76b4e9b8981139753077bf164f59900f29d54ff35bc294c9b4.json => query-06f847d99d452dafd10f4a6bec309c330d76eb5d0a34012bd75395b13e3ff659.json} (74%) create mode 100644 .sqlx/query-1a01b8b88444b493abf74b2ec0ad649018244de3c7f23d98bfab71faa1a9fae1.json rename .sqlx/{query-35a4ec60785870b07495258a3ea5faf0d7a9d5f523db44f7da17ca1bf31d4576.json => query-33a1e2f1904757c775d389fa99d67916b7b220d6aa1fe8bb6690f85ed1cd5666.json} (70%) rename .sqlx/{query-43fe6ac793c1a59664383338f83488788ae61d74d3e86bbe123b1d41c8e19af4.json => query-3e6fa53cc900724e25e127f472cb0cb5b5e76fbcf424a79b94862f623ea975fc.json} (74%) rename .sqlx/{query-ea731a5dd05cf3c68a4f479ec978571325c844ae1f4d222193bebd61a1859ee2.json => query-439bef62ccc846cccca2c6979e5698a7aaba2beb19645b720eefa73e4dcac942.json} (69%) rename .sqlx/{query-01ef7ff2c9dc9bbaba01b9e6bc4adf4c4cbe70d3dd64b503b387a8cf948fab2c.json => query-442aac6474b466acc15578483d7d582a6face350a9c4c11db8b32fc251605cd6.json} (58%) rename .sqlx/{query-7381a3487396a1d7e562e21bb2804fd82b538b4ed436ad714408a72657e8a9e2.json => query-481658620e98faa574e26fe49abbecafa1166f16151c69b465cd49f334de7190.json} (55%) create mode 100644 .sqlx/query-580741c18880eb98a7073dbb8e1cd907893fedd70f7d752521d515230397f3ee.json rename .sqlx/{query-dba6275f3895b8725bdce57c4617956773817874aeccc8c9f73527018e5d10cb.json => query-5f1da7400599669d9591f6dded6c38d6f74286fd5660c1ccae20ce43617bbc8f.json} (74%) rename .sqlx/{query-c91e2bc58d379bbc7ed29d008f1806ff6f0efe4fd55a198682d52d2edc7de24d.json => query-6dfb9442da967755230ab2b7c5baf8eec06d64af5886e840bea1b32d2be10ca0.json} (57%) rename .sqlx/{query-fbfe27851d858408d81529e6e77190e332d85f9162249ef56c98edc2582c288a.json => query-6eff81e0ddc89652014c10c9b5c1561dfa68bc80db59bc000dc217ffa639b53b.json} (59%) rename .sqlx/{query-ca2755ead4852207d09bcd46ffcaadf33061fbb6cf7e1e7c58d5bee1692351e2.json => query-84679835466cb41a74dd9ef281c9a69451102dae52ffb5a4df99e160a1ec8907.json} (67%) create mode 100644 .sqlx/query-9132a40b7729383ce9c108baa1b412e872ae9c96792ae9722f66ccfb24f0a144.json rename .sqlx/{query-55aac498ce61c0b42f4895add96dfbf99606684a681883600a7aca610c7c6e8b.json => query-9213729a9a1ce371ef77898f5792d914a67400cb4cce9a8bf86227ffe7d42eda.json} (64%) rename .sqlx/{query-b1b93736fd91a6445bb072e5bbe619c59d9a535dc70d226de618e549c203e88b.json => query-9e3c1c1f52bc1a576012c57c7472f9f7601e1128b15fc0ecc7676cd5aa01c88c.json} (69%) rename .sqlx/{query-c3566263419aab9ba0172d59b105c03a8f5b70a8813849956e9d32b5da5a407f.json => query-9e659e4a6d973f6603f8c64ea6e7deaf8eb6824c4246d4135274e760b6af73e0.json} (56%) delete mode 100644 .sqlx/query-a5aa90dcb89a4e7f1908171b4e4ed6027dd5c770a1330689a029cc31f731bf33.json rename .sqlx/{query-d069191fedf7628f2d62be632911845278cca5ceb21718f37082728fa052c33c.json => query-c64f247f81e332689e35c224656847b246deb6f92a5790a4fdd0d5733defbb57.json} (73%) rename .sqlx/{query-7878106cb8e9cc315310cf7c0946c3782f2bc80e3f2e0f015bea23717604709f.json => query-caf97c3a058eac0f9deb4e474b0d76a2d14a75d04f4ea5474e06adc9466a544d.json} (77%) rename .sqlx/{query-2fadd2533949c86b10d2fb2bcac3b3844f0cedcc82d465c0cc4baeec552e8c98.json => query-eb6dee5462657ac5ce0ecf31d1477ce7cc874d9ad8b3119977168f601b3e8072.json} (74%) rename .sqlx/{query-d5761193c11c9029731464a6824600203ca943272969aff90da38088f9f55097.json => query-fdbb9308a58ade3fd1cba272fe3979ed4bafb1ee079fd8b23ac9c2ef95db2312.json} (61%) rename .sqlx/{query-8ae08ad03e745e8f3d65707bb8637a9277805ca9c7a22faeb66230ce0fa87ea9.json => query-ff9f1363df5b9dc633767b0d3addedea0baba7447d4557f681f26c666a2c47bc.json} (57%) create mode 100644 migrations/20231222094917_support_gateway_disconnect.down.sql create mode 100644 migrations/20231222094917_support_gateway_disconnect.up.sql create mode 100644 migrations/20231227091628_fix_preshared_key.down.sql create mode 100644 migrations/20231227091628_fix_preshared_key.up.sql create mode 100644 src/wireguard_peer_disconnect.rs diff --git a/.sqlx/query-5d629b503d4d9f76b4e9b8981139753077bf164f59900f29d54ff35bc294c9b4.json b/.sqlx/query-06f847d99d452dafd10f4a6bec309c330d76eb5d0a34012bd75395b13e3ff659.json similarity index 74% rename from .sqlx/query-5d629b503d4d9f76b4e9b8981139753077bf164f59900f29d54ff35bc294c9b4.json rename to .sqlx/query-06f847d99d452dafd10f4a6bec309c330d76eb5d0a34012bd75395b13e3ff659.json index c89652bd4..c291b5a4e 100644 --- a/.sqlx/query-5d629b503d4d9f76b4e9b8981139753077bf164f59900f29d54ff35bc294c9b4.json +++ b/.sqlx/query-06f847d99d452dafd10f4a6bec309c330d76eb5d0a34012bd75395b13e3ff659.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", \"name\",\"wireguard_pubkey\",\"user_id\",\"created\",\"preshared_key\" FROM \"device\"", + "query": "SELECT id \"id?\", \"name\",\"wireguard_pubkey\",\"user_id\",\"created\" FROM \"device\"", "describe": { "columns": [ { @@ -27,11 +27,6 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" - }, - { - "ordinal": 5, - "name": "preshared_key", - "type_info": "Text" } ], "parameters": { @@ -42,9 +37,8 @@ false, false, false, - false, - true + false ] }, - "hash": "5d629b503d4d9f76b4e9b8981139753077bf164f59900f29d54ff35bc294c9b4" + "hash": "06f847d99d452dafd10f4a6bec309c330d76eb5d0a34012bd75395b13e3ff659" } diff --git a/.sqlx/query-1a01b8b88444b493abf74b2ec0ad649018244de3c7f23d98bfab71faa1a9fae1.json b/.sqlx/query-1a01b8b88444b493abf74b2ec0ad649018244de3c7f23d98bfab71faa1a9fae1.json new file mode 100644 index 000000000..c6790b55f --- /dev/null +++ b/.sqlx/query-1a01b8b88444b493abf74b2ec0ad649018244de3c7f23d98bfab71faa1a9fae1.json @@ -0,0 +1,92 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id as \"id?\", name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, mfa_enabled, keepalive_interval, peer_disconnect_threshold FROM wireguard_network WHERE mfa_enabled = true", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id?", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "address", + "type_info": "Inet" + }, + { + "ordinal": 3, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "pubkey", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "prvkey", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "endpoint", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "dns", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "allowed_ips", + "type_info": "InetArray" + }, + { + "ordinal": 9, + "name": "connected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 10, + "name": "mfa_enabled", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "keepalive_interval", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "peer_disconnect_threshold", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + false + ] + }, + "hash": "1a01b8b88444b493abf74b2ec0ad649018244de3c7f23d98bfab71faa1a9fae1" +} diff --git a/.sqlx/query-35a4ec60785870b07495258a3ea5faf0d7a9d5f523db44f7da17ca1bf31d4576.json b/.sqlx/query-33a1e2f1904757c775d389fa99d67916b7b220d6aa1fe8bb6690f85ed1cd5666.json similarity index 70% rename from .sqlx/query-35a4ec60785870b07495258a3ea5faf0d7a9d5f523db44f7da17ca1bf31d4576.json rename to .sqlx/query-33a1e2f1904757c775d389fa99d67916b7b220d6aa1fe8bb6690f85ed1cd5666.json index 1835956e7..462ab0695 100644 --- a/.sqlx/query-35a4ec60785870b07495258a3ea5faf0d7a9d5f523db44f7da17ca1bf31d4576.json +++ b/.sqlx/query-33a1e2f1904757c775d389fa99d67916b7b220d6aa1fe8bb6690f85ed1cd5666.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key FROM device JOIN \"user\" ON device.user_id = \"user\".id WHERE \"user\".username = $1", + "query": "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created FROM device JOIN \"user\" ON device.user_id = \"user\".id WHERE \"user\".username = $1", "describe": { "columns": [ { @@ -27,11 +27,6 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" - }, - { - "ordinal": 5, - "name": "preshared_key", - "type_info": "Text" } ], "parameters": { @@ -44,9 +39,8 @@ false, false, false, - false, - true + false ] }, - "hash": "35a4ec60785870b07495258a3ea5faf0d7a9d5f523db44f7da17ca1bf31d4576" + "hash": "33a1e2f1904757c775d389fa99d67916b7b220d6aa1fe8bb6690f85ed1cd5666" } diff --git a/.sqlx/query-43fe6ac793c1a59664383338f83488788ae61d74d3e86bbe123b1d41c8e19af4.json b/.sqlx/query-3e6fa53cc900724e25e127f472cb0cb5b5e76fbcf424a79b94862f623ea975fc.json similarity index 74% rename from .sqlx/query-43fe6ac793c1a59664383338f83488788ae61d74d3e86bbe123b1d41c8e19af4.json rename to .sqlx/query-3e6fa53cc900724e25e127f472cb0cb5b5e76fbcf424a79b94862f623ea975fc.json index a4455a8ac..431b7f8fa 100644 --- a/.sqlx/query-43fe6ac793c1a59664383338f83488788ae61d74d3e86bbe123b1d41c8e19af4.json +++ b/.sqlx/query-3e6fa53cc900724e25e127f472cb0cb5b5e76fbcf424a79b94862f623ea975fc.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key FROM device WHERE user_id = $1", + "query": "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created FROM device WHERE user_id = $1", "describe": { "columns": [ { @@ -27,11 +27,6 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" - }, - { - "ordinal": 5, - "name": "preshared_key", - "type_info": "Text" } ], "parameters": { @@ -44,9 +39,8 @@ false, false, false, - false, - true + false ] }, - "hash": "43fe6ac793c1a59664383338f83488788ae61d74d3e86bbe123b1d41c8e19af4" + "hash": "3e6fa53cc900724e25e127f472cb0cb5b5e76fbcf424a79b94862f623ea975fc" } diff --git a/.sqlx/query-ea731a5dd05cf3c68a4f479ec978571325c844ae1f4d222193bebd61a1859ee2.json b/.sqlx/query-439bef62ccc846cccca2c6979e5698a7aaba2beb19645b720eefa73e4dcac942.json similarity index 69% rename from .sqlx/query-ea731a5dd05cf3c68a4f479ec978571325c844ae1f4d222193bebd61a1859ee2.json rename to .sqlx/query-439bef62ccc846cccca2c6979e5698a7aaba2beb19645b720eefa73e4dcac942.json index 2e83afa95..b5ca2d931 100644 --- a/.sqlx/query-ea731a5dd05cf3c68a4f479ec978571325c844ae1f4d222193bebd61a1859ee2.json +++ b/.sqlx/query-439bef62ccc846cccca2c6979e5698a7aaba2beb19645b720eefa73e4dcac942.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key FROM device JOIN \"user\" ON device.user_id = \"user\".id WHERE device.id = $1 AND \"user\".username = $2", + "query": "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created FROM device JOIN \"user\" ON device.user_id = \"user\".id WHERE device.id = $1 AND \"user\".username = $2", "describe": { "columns": [ { @@ -27,11 +27,6 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" - }, - { - "ordinal": 5, - "name": "preshared_key", - "type_info": "Text" } ], "parameters": { @@ -45,9 +40,8 @@ false, false, false, - false, - true + false ] }, - "hash": "ea731a5dd05cf3c68a4f479ec978571325c844ae1f4d222193bebd61a1859ee2" + "hash": "439bef62ccc846cccca2c6979e5698a7aaba2beb19645b720eefa73e4dcac942" } diff --git a/.sqlx/query-01ef7ff2c9dc9bbaba01b9e6bc4adf4c4cbe70d3dd64b503b387a8cf948fab2c.json b/.sqlx/query-442aac6474b466acc15578483d7d582a6face350a9c4c11db8b32fc251605cd6.json similarity index 58% rename from .sqlx/query-01ef7ff2c9dc9bbaba01b9e6bc4adf4c4cbe70d3dd64b503b387a8cf948fab2c.json rename to .sqlx/query-442aac6474b466acc15578483d7d582a6face350a9c4c11db8b32fc251605cd6.json index 621919ad8..7e6961bbf 100644 --- a/.sqlx/query-01ef7ff2c9dc9bbaba01b9e6bc4adf4c4cbe70d3dd64b503b387a8cf948fab2c.json +++ b/.sqlx/query-442aac6474b466acc15578483d7d582a6face350a9c4c11db8b32fc251605cd6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\" FROM wireguard_network_device WHERE device_id = $1", + "query": "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\", preshared_key, is_authorized FROM wireguard_network_device WHERE device_id = $1", "describe": { "columns": [ { @@ -17,6 +17,16 @@ "ordinal": 2, "name": "wireguard_ip: IpAddr", "type_info": "Inet" + }, + { + "ordinal": 3, + "name": "preshared_key", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "is_authorized", + "type_info": "Bool" } ], "parameters": { @@ -27,8 +37,10 @@ "nullable": [ false, false, + false, + true, false ] }, - "hash": "01ef7ff2c9dc9bbaba01b9e6bc4adf4c4cbe70d3dd64b503b387a8cf948fab2c" + "hash": "442aac6474b466acc15578483d7d582a6face350a9c4c11db8b32fc251605cd6" } diff --git a/.sqlx/query-7381a3487396a1d7e562e21bb2804fd82b538b4ed436ad714408a72657e8a9e2.json b/.sqlx/query-481658620e98faa574e26fe49abbecafa1166f16151c69b465cd49f334de7190.json similarity index 55% rename from .sqlx/query-7381a3487396a1d7e562e21bb2804fd82b538b4ed436ad714408a72657e8a9e2.json rename to .sqlx/query-481658620e98faa574e26fe49abbecafa1166f16151c69b465cd49f334de7190.json index eb365a6f6..b3830140f 100644 --- a/.sqlx/query-7381a3487396a1d7e562e21bb2804fd82b538b4ed436ad714408a72657e8a9e2.json +++ b/.sqlx/query-481658620e98faa574e26fe49abbecafa1166f16151c69b465cd49f334de7190.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\" FROM wireguard_network_device WHERE wireguard_network_id = $1", + "query": "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\", preshared_key, is_authorized FROM wireguard_network_device WHERE device_id = $1 AND wireguard_network_id = $2", "describe": { "columns": [ { @@ -17,18 +17,31 @@ "ordinal": 2, "name": "wireguard_ip: IpAddr", "type_info": "Inet" + }, + { + "ordinal": 3, + "name": "preshared_key", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "is_authorized", + "type_info": "Bool" } ], "parameters": { "Left": [ + "Int8", "Int8" ] }, "nullable": [ false, false, + false, + true, false ] }, - "hash": "7381a3487396a1d7e562e21bb2804fd82b538b4ed436ad714408a72657e8a9e2" + "hash": "481658620e98faa574e26fe49abbecafa1166f16151c69b465cd49f334de7190" } diff --git a/.sqlx/query-580741c18880eb98a7073dbb8e1cd907893fedd70f7d752521d515230397f3ee.json b/.sqlx/query-580741c18880eb98a7073dbb8e1cd907893fedd70f7d752521d515230397f3ee.json new file mode 100644 index 000000000..8323d4d07 --- /dev/null +++ b/.sqlx/query-580741c18880eb98a7073dbb8e1cd907893fedd70f7d752521d515230397f3ee.json @@ -0,0 +1,47 @@ +{ + "db_name": "PostgreSQL", + "query": "WITH stats AS ( SELECT DISTINCT ON (device_id) device_id, endpoint, latest_handshake FROM wireguard_peer_stats WHERE network = $1 ORDER BY device_id, collected_at DESC ) SELECT d.id as \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created FROM device d JOIN wireguard_network_device wnd ON wnd.device_id = d.id LEFT JOIN stats on d.id = stats.device_id WHERE wnd.wireguard_network_id = $1 AND wnd.is_authorized = true AND (NOW() - stats.latest_handshake) > $2 * interval '1 second'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id?", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "wireguard_pubkey", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "created", + "type_info": "Timestamp" + } + ], + "parameters": { + "Left": [ + "Int8", + "Float8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "580741c18880eb98a7073dbb8e1cd907893fedd70f7d752521d515230397f3ee" +} diff --git a/.sqlx/query-dba6275f3895b8725bdce57c4617956773817874aeccc8c9f73527018e5d10cb.json b/.sqlx/query-5f1da7400599669d9591f6dded6c38d6f74286fd5660c1ccae20ce43617bbc8f.json similarity index 74% rename from .sqlx/query-dba6275f3895b8725bdce57c4617956773817874aeccc8c9f73527018e5d10cb.json rename to .sqlx/query-5f1da7400599669d9591f6dded6c38d6f74286fd5660c1ccae20ce43617bbc8f.json index 265377aa3..fbd018000 100644 --- a/.sqlx/query-dba6275f3895b8725bdce57c4617956773817874aeccc8c9f73527018e5d10cb.json +++ b/.sqlx/query-5f1da7400599669d9591f6dded6c38d6f74286fd5660c1ccae20ce43617bbc8f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "WITH s AS ( SELECT DISTINCT ON (device_id) * FROM wireguard_peer_stats ORDER BY device_id, latest_handshake DESC ) SELECT d.id \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created, d.preshared_key FROM device d JOIN s ON d.id = s.device_id WHERE s.latest_handshake >= $1 AND s.network = $2", + "query": "WITH s AS ( SELECT DISTINCT ON (device_id) * FROM wireguard_peer_stats ORDER BY device_id, latest_handshake DESC ) SELECT d.id \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created FROM device d JOIN s ON d.id = s.device_id WHERE s.latest_handshake >= $1 AND s.network = $2", "describe": { "columns": [ { @@ -27,11 +27,6 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" - }, - { - "ordinal": 5, - "name": "preshared_key", - "type_info": "Text" } ], "parameters": { @@ -45,9 +40,8 @@ false, false, false, - false, - true + false ] }, - "hash": "dba6275f3895b8725bdce57c4617956773817874aeccc8c9f73527018e5d10cb" + "hash": "5f1da7400599669d9591f6dded6c38d6f74286fd5660c1ccae20ce43617bbc8f" } diff --git a/.sqlx/query-c91e2bc58d379bbc7ed29d008f1806ff6f0efe4fd55a198682d52d2edc7de24d.json b/.sqlx/query-6dfb9442da967755230ab2b7c5baf8eec06d64af5886e840bea1b32d2be10ca0.json similarity index 57% rename from .sqlx/query-c91e2bc58d379bbc7ed29d008f1806ff6f0efe4fd55a198682d52d2edc7de24d.json rename to .sqlx/query-6dfb9442da967755230ab2b7c5baf8eec06d64af5886e840bea1b32d2be10ca0.json index e9d042263..1a836e180 100644 --- a/.sqlx/query-c91e2bc58d379bbc7ed29d008f1806ff6f0efe4fd55a198682d52d2edc7de24d.json +++ b/.sqlx/query-6dfb9442da967755230ab2b7c5baf8eec06d64af5886e840bea1b32d2be10ca0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"device\" SET \"name\" = $2,\"wireguard_pubkey\" = $3,\"user_id\" = $4,\"created\" = $5,\"preshared_key\" = $6 WHERE id = $1", + "query": "UPDATE \"device\" SET \"name\" = $2,\"wireguard_pubkey\" = $3,\"user_id\" = $4,\"created\" = $5 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -9,11 +9,10 @@ "Text", "Text", "Int8", - "Timestamp", - "Text" + "Timestamp" ] }, "nullable": [] }, - "hash": "c91e2bc58d379bbc7ed29d008f1806ff6f0efe4fd55a198682d52d2edc7de24d" + "hash": "6dfb9442da967755230ab2b7c5baf8eec06d64af5886e840bea1b32d2be10ca0" } diff --git a/.sqlx/query-fbfe27851d858408d81529e6e77190e332d85f9162249ef56c98edc2582c288a.json b/.sqlx/query-6eff81e0ddc89652014c10c9b5c1561dfa68bc80db59bc000dc217ffa639b53b.json similarity index 59% rename from .sqlx/query-fbfe27851d858408d81529e6e77190e332d85f9162249ef56c98edc2582c288a.json rename to .sqlx/query-6eff81e0ddc89652014c10c9b5c1561dfa68bc80db59bc000dc217ffa639b53b.json index cc2c45570..15d12798f 100644 --- a/.sqlx/query-fbfe27851d858408d81529e6e77190e332d85f9162249ef56c98edc2582c288a.json +++ b/.sqlx/query-6eff81e0ddc89652014c10c9b5c1561dfa68bc80db59bc000dc217ffa639b53b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT wireguard_network_id as network_id, wireguard_ip as \"device_wireguard_ip: IpAddr\" FROM wireguard_network_device WHERE device_id = $1", + "query": "SELECT wireguard_network_id as network_id, wireguard_ip as \"device_wireguard_ip: IpAddr\", preshared_key FROM wireguard_network_device WHERE device_id = $1", "describe": { "columns": [ { @@ -12,6 +12,11 @@ "ordinal": 1, "name": "device_wireguard_ip: IpAddr", "type_info": "Inet" + }, + { + "ordinal": 2, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -21,8 +26,9 @@ }, "nullable": [ false, - false + false, + true ] }, - "hash": "fbfe27851d858408d81529e6e77190e332d85f9162249ef56c98edc2582c288a" + "hash": "6eff81e0ddc89652014c10c9b5c1561dfa68bc80db59bc000dc217ffa639b53b" } diff --git a/.sqlx/query-ca2755ead4852207d09bcd46ffcaadf33061fbb6cf7e1e7c58d5bee1692351e2.json b/.sqlx/query-84679835466cb41a74dd9ef281c9a69451102dae52ffb5a4df99e160a1ec8907.json similarity index 67% rename from .sqlx/query-ca2755ead4852207d09bcd46ffcaadf33061fbb6cf7e1e7c58d5bee1692351e2.json rename to .sqlx/query-84679835466cb41a74dd9ef281c9a69451102dae52ffb5a4df99e160a1ec8907.json index 4b0364ac1..15003141b 100644 --- a/.sqlx/query-ca2755ead4852207d09bcd46ffcaadf33061fbb6cf7e1e7c58d5bee1692351e2.json +++ b/.sqlx/query-84679835466cb41a74dd9ef281c9a69451102dae52ffb5a4df99e160a1ec8907.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT d.id \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created, d.preshared_key FROM device d JOIN wireguard_network_device wnd ON d.id = wnd.device_id WHERE wnd.wireguard_ip = $1 AND wnd.wireguard_network_id = $2", + "query": "SELECT d.id \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created FROM device d JOIN wireguard_network_device wnd ON d.id = wnd.device_id WHERE wnd.wireguard_ip = $1 AND wnd.wireguard_network_id = $2", "describe": { "columns": [ { @@ -27,11 +27,6 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" - }, - { - "ordinal": 5, - "name": "preshared_key", - "type_info": "Text" } ], "parameters": { @@ -45,9 +40,8 @@ false, false, false, - false, - true + false ] }, - "hash": "ca2755ead4852207d09bcd46ffcaadf33061fbb6cf7e1e7c58d5bee1692351e2" + "hash": "84679835466cb41a74dd9ef281c9a69451102dae52ffb5a4df99e160a1ec8907" } diff --git a/.sqlx/query-9132a40b7729383ce9c108baa1b412e872ae9c96792ae9722f66ccfb24f0a144.json b/.sqlx/query-9132a40b7729383ce9c108baa1b412e872ae9c96792ae9722f66ccfb24f0a144.json new file mode 100644 index 000000000..caa6121c9 --- /dev/null +++ b/.sqlx/query-9132a40b7729383ce9c108baa1b412e872ae9c96792ae9722f66ccfb24f0a144.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO wireguard_network_device (device_id, wireguard_network_id, wireguard_ip, is_authorized) VALUES ($1, $2, $3, $4) ON CONFLICT ON CONSTRAINT device_network DO UPDATE SET wireguard_ip = $3, is_authorized = $4", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Inet", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "9132a40b7729383ce9c108baa1b412e872ae9c96792ae9722f66ccfb24f0a144" +} diff --git a/.sqlx/query-55aac498ce61c0b42f4895add96dfbf99606684a681883600a7aca610c7c6e8b.json b/.sqlx/query-9213729a9a1ce371ef77898f5792d914a67400cb4cce9a8bf86227ffe7d42eda.json similarity index 64% rename from .sqlx/query-55aac498ce61c0b42f4895add96dfbf99606684a681883600a7aca610c7c6e8b.json rename to .sqlx/query-9213729a9a1ce371ef77898f5792d914a67400cb4cce9a8bf86227ffe7d42eda.json index 5f222745a..285e7121c 100644 --- a/.sqlx/query-55aac498ce61c0b42f4895add96dfbf99606684a681883600a7aca610c7c6e8b.json +++ b/.sqlx/query-9213729a9a1ce371ef77898f5792d914a67400cb4cce9a8bf86227ffe7d42eda.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"device\" (\"name\",\"wireguard_pubkey\",\"user_id\",\"created\",\"preshared_key\") VALUES ($1,$2,$3,$4,$5) RETURNING id", + "query": "INSERT INTO \"device\" (\"name\",\"wireguard_pubkey\",\"user_id\",\"created\") VALUES ($1,$2,$3,$4) RETURNING id", "describe": { "columns": [ { @@ -14,13 +14,12 @@ "Text", "Text", "Int8", - "Timestamp", - "Text" + "Timestamp" ] }, "nullable": [ false ] }, - "hash": "55aac498ce61c0b42f4895add96dfbf99606684a681883600a7aca610c7c6e8b" + "hash": "9213729a9a1ce371ef77898f5792d914a67400cb4cce9a8bf86227ffe7d42eda" } diff --git a/.sqlx/query-b1b93736fd91a6445bb072e5bbe619c59d9a535dc70d226de618e549c203e88b.json b/.sqlx/query-9e3c1c1f52bc1a576012c57c7472f9f7601e1128b15fc0ecc7676cd5aa01c88c.json similarity index 69% rename from .sqlx/query-b1b93736fd91a6445bb072e5bbe619c59d9a535dc70d226de618e549c203e88b.json rename to .sqlx/query-9e3c1c1f52bc1a576012c57c7472f9f7601e1128b15fc0ecc7676cd5aa01c88c.json index afd9e39e3..82874b3d7 100644 --- a/.sqlx/query-b1b93736fd91a6445bb072e5bbe619c59d9a535dc70d226de618e549c203e88b.json +++ b/.sqlx/query-9e3c1c1f52bc1a576012c57c7472f9f7601e1128b15fc0ecc7676cd5aa01c88c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key FROM device JOIN \"user\" ON device.user_id = \"user\".id WHERE device.id = $1 AND \"user\".id = $2", + "query": "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created FROM device JOIN \"user\" ON device.user_id = \"user\".id WHERE device.id = $1 AND \"user\".id = $2", "describe": { "columns": [ { @@ -27,11 +27,6 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" - }, - { - "ordinal": 5, - "name": "preshared_key", - "type_info": "Text" } ], "parameters": { @@ -45,9 +40,8 @@ false, false, false, - false, - true + false ] }, - "hash": "b1b93736fd91a6445bb072e5bbe619c59d9a535dc70d226de618e549c203e88b" + "hash": "9e3c1c1f52bc1a576012c57c7472f9f7601e1128b15fc0ecc7676cd5aa01c88c" } diff --git a/.sqlx/query-c3566263419aab9ba0172d59b105c03a8f5b70a8813849956e9d32b5da5a407f.json b/.sqlx/query-9e659e4a6d973f6603f8c64ea6e7deaf8eb6824c4246d4135274e760b6af73e0.json similarity index 56% rename from .sqlx/query-c3566263419aab9ba0172d59b105c03a8f5b70a8813849956e9d32b5da5a407f.json rename to .sqlx/query-9e659e4a6d973f6603f8c64ea6e7deaf8eb6824c4246d4135274e760b6af73e0.json index 82ebe7c72..7cf799593 100644 --- a/.sqlx/query-c3566263419aab9ba0172d59b105c03a8f5b70a8813849956e9d32b5da5a407f.json +++ b/.sqlx/query-9e659e4a6d973f6603f8c64ea6e7deaf8eb6824c4246d4135274e760b6af73e0.json @@ -1,16 +1,17 @@ { "db_name": "PostgreSQL", - "query": "UPDATE wireguard_network_device SET wireguard_ip = $3 WHERE device_id = $1 AND wireguard_network_id = $2", + "query": "UPDATE wireguard_network_device SET wireguard_ip = $3, is_authorized = $4 WHERE device_id = $1 AND wireguard_network_id = $2", "describe": { "columns": [], "parameters": { "Left": [ "Int8", "Int8", - "Inet" + "Inet", + "Bool" ] }, "nullable": [] }, - "hash": "c3566263419aab9ba0172d59b105c03a8f5b70a8813849956e9d32b5da5a407f" + "hash": "9e659e4a6d973f6603f8c64ea6e7deaf8eb6824c4246d4135274e760b6af73e0" } diff --git a/.sqlx/query-a5aa90dcb89a4e7f1908171b4e4ed6027dd5c770a1330689a029cc31f731bf33.json b/.sqlx/query-a5aa90dcb89a4e7f1908171b4e4ed6027dd5c770a1330689a029cc31f731bf33.json deleted file mode 100644 index 8bbfec8e8..000000000 --- a/.sqlx/query-a5aa90dcb89a4e7f1908171b4e4ed6027dd5c770a1330689a029cc31f731bf33.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO wireguard_network_device (device_id, wireguard_network_id, wireguard_ip) VALUES ($1, $2, $3) ON CONFLICT ON CONSTRAINT device_network DO UPDATE SET wireguard_ip = $3", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Inet" - ] - }, - "nullable": [] - }, - "hash": "a5aa90dcb89a4e7f1908171b4e4ed6027dd5c770a1330689a029cc31f731bf33" -} diff --git a/.sqlx/query-d069191fedf7628f2d62be632911845278cca5ceb21718f37082728fa052c33c.json b/.sqlx/query-c64f247f81e332689e35c224656847b246deb6f92a5790a4fdd0d5733defbb57.json similarity index 73% rename from .sqlx/query-d069191fedf7628f2d62be632911845278cca5ceb21718f37082728fa052c33c.json rename to .sqlx/query-c64f247f81e332689e35c224656847b246deb6f92a5790a4fdd0d5733defbb57.json index 3cf4488e3..4a03705f0 100644 --- a/.sqlx/query-d069191fedf7628f2d62be632911845278cca5ceb21718f37082728fa052c33c.json +++ b/.sqlx/query-c64f247f81e332689e35c224656847b246deb6f92a5790a4fdd0d5733defbb57.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", \"name\",\"wireguard_pubkey\",\"user_id\",\"created\",\"preshared_key\" FROM \"device\" WHERE id = $1", + "query": "SELECT id \"id?\", \"name\",\"wireguard_pubkey\",\"user_id\",\"created\" FROM \"device\" WHERE id = $1", "describe": { "columns": [ { @@ -27,11 +27,6 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" - }, - { - "ordinal": 5, - "name": "preshared_key", - "type_info": "Text" } ], "parameters": { @@ -44,9 +39,8 @@ false, false, false, - false, - true + false ] }, - "hash": "d069191fedf7628f2d62be632911845278cca5ceb21718f37082728fa052c33c" + "hash": "c64f247f81e332689e35c224656847b246deb6f92a5790a4fdd0d5733defbb57" } diff --git a/.sqlx/query-7878106cb8e9cc315310cf7c0946c3782f2bc80e3f2e0f015bea23717604709f.json b/.sqlx/query-caf97c3a058eac0f9deb4e474b0d76a2d14a75d04f4ea5474e06adc9466a544d.json similarity index 77% rename from .sqlx/query-7878106cb8e9cc315310cf7c0946c3782f2bc80e3f2e0f015bea23717604709f.json rename to .sqlx/query-caf97c3a058eac0f9deb4e474b0d76a2d14a75d04f4ea5474e06adc9466a544d.json index 2a247f079..72c3f6bd6 100644 --- a/.sqlx/query-7878106cb8e9cc315310cf7c0946c3782f2bc80e3f2e0f015bea23717604709f.json +++ b/.sqlx/query-caf97c3a058eac0f9deb4e474b0d76a2d14a75d04f4ea5474e06adc9466a544d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT d.wireguard_pubkey as pubkey, preshared_key, array[host(wnd.wireguard_ip)] as \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id WHERE wireguard_network_id = $1 ORDER BY d.id ASC", + "query": "SELECT d.wireguard_pubkey as pubkey, preshared_key, array[host(wnd.wireguard_ip)] as \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id WHERE wireguard_network_id = $1 AND (is_authorized = true OR NOT $2) ORDER BY d.id ASC", "describe": { "columns": [ { @@ -21,7 +21,8 @@ ], "parameters": { "Left": [ - "Int8" + "Int8", + "Bool" ] }, "nullable": [ @@ -30,5 +31,5 @@ null ] }, - "hash": "7878106cb8e9cc315310cf7c0946c3782f2bc80e3f2e0f015bea23717604709f" + "hash": "caf97c3a058eac0f9deb4e474b0d76a2d14a75d04f4ea5474e06adc9466a544d" } diff --git a/.sqlx/query-2fadd2533949c86b10d2fb2bcac3b3844f0cedcc82d465c0cc4baeec552e8c98.json b/.sqlx/query-eb6dee5462657ac5ce0ecf31d1477ce7cc874d9ad8b3119977168f601b3e8072.json similarity index 74% rename from .sqlx/query-2fadd2533949c86b10d2fb2bcac3b3844f0cedcc82d465c0cc4baeec552e8c98.json rename to .sqlx/query-eb6dee5462657ac5ce0ecf31d1477ce7cc874d9ad8b3119977168f601b3e8072.json index 878e77680..b0e4fe52a 100644 --- a/.sqlx/query-2fadd2533949c86b10d2fb2bcac3b3844f0cedcc82d465c0cc4baeec552e8c98.json +++ b/.sqlx/query-eb6dee5462657ac5ce0ecf31d1477ce7cc874d9ad8b3119977168f601b3e8072.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key FROM device WHERE wireguard_pubkey = $1", + "query": "SELECT id \"id?\", name, wireguard_pubkey, user_id, created FROM device WHERE wireguard_pubkey = $1", "describe": { "columns": [ { @@ -27,11 +27,6 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" - }, - { - "ordinal": 5, - "name": "preshared_key", - "type_info": "Text" } ], "parameters": { @@ -44,9 +39,8 @@ false, false, false, - false, - true + false ] }, - "hash": "2fadd2533949c86b10d2fb2bcac3b3844f0cedcc82d465c0cc4baeec552e8c98" + "hash": "eb6dee5462657ac5ce0ecf31d1477ce7cc874d9ad8b3119977168f601b3e8072" } diff --git a/.sqlx/query-d5761193c11c9029731464a6824600203ca943272969aff90da38088f9f55097.json b/.sqlx/query-fdbb9308a58ade3fd1cba272fe3979ed4bafb1ee079fd8b23ac9c2ef95db2312.json similarity index 61% rename from .sqlx/query-d5761193c11c9029731464a6824600203ca943272969aff90da38088f9f55097.json rename to .sqlx/query-fdbb9308a58ade3fd1cba272fe3979ed4bafb1ee079fd8b23ac9c2ef95db2312.json index 8bad0a958..dd54ba7fc 100644 --- a/.sqlx/query-d5761193c11c9029731464a6824600203ca943272969aff90da38088f9f55097.json +++ b/.sqlx/query-fdbb9308a58ade3fd1cba272fe3979ed4bafb1ee079fd8b23ac9c2ef95db2312.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT DISTINCT ON (d.id) d.id as \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created, d.preshared_key FROM device d JOIN \"user\" u ON d.user_id = u.id JOIN group_user gu ON u.id = gu.user_id JOIN \"group\" g ON gu.group_id = g.id WHERE g.\"name\" IN (SELECT * FROM UNNEST($1::text[]))\n ORDER BY d.id ASC", + "query": "SELECT DISTINCT ON (d.id) d.id as \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created FROM device d JOIN \"user\" u ON d.user_id = u.id JOIN group_user gu ON u.id = gu.user_id JOIN \"group\" g ON gu.group_id = g.id WHERE g.\"name\" IN (SELECT * FROM UNNEST($1::text[]))\n ORDER BY d.id ASC", "describe": { "columns": [ { @@ -27,11 +27,6 @@ "ordinal": 4, "name": "created", "type_info": "Timestamp" - }, - { - "ordinal": 5, - "name": "preshared_key", - "type_info": "Text" } ], "parameters": { @@ -44,9 +39,8 @@ false, false, false, - false, - true + false ] }, - "hash": "d5761193c11c9029731464a6824600203ca943272969aff90da38088f9f55097" + "hash": "fdbb9308a58ade3fd1cba272fe3979ed4bafb1ee079fd8b23ac9c2ef95db2312" } diff --git a/.sqlx/query-8ae08ad03e745e8f3d65707bb8637a9277805ca9c7a22faeb66230ce0fa87ea9.json b/.sqlx/query-ff9f1363df5b9dc633767b0d3addedea0baba7447d4557f681f26c666a2c47bc.json similarity index 57% rename from .sqlx/query-8ae08ad03e745e8f3d65707bb8637a9277805ca9c7a22faeb66230ce0fa87ea9.json rename to .sqlx/query-ff9f1363df5b9dc633767b0d3addedea0baba7447d4557f681f26c666a2c47bc.json index 5c37678ae..b4ff83fef 100644 --- a/.sqlx/query-8ae08ad03e745e8f3d65707bb8637a9277805ca9c7a22faeb66230ce0fa87ea9.json +++ b/.sqlx/query-ff9f1363df5b9dc633767b0d3addedea0baba7447d4557f681f26c666a2c47bc.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\" FROM wireguard_network_device WHERE device_id = $1 AND wireguard_network_id = $2", + "query": "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\", preshared_key, is_authorized FROM wireguard_network_device WHERE wireguard_network_id = $1", "describe": { "columns": [ { @@ -17,19 +17,30 @@ "ordinal": 2, "name": "wireguard_ip: IpAddr", "type_info": "Inet" + }, + { + "ordinal": 3, + "name": "preshared_key", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "is_authorized", + "type_info": "Bool" } ], "parameters": { "Left": [ - "Int8", "Int8" ] }, "nullable": [ false, false, + false, + true, false ] }, - "hash": "8ae08ad03e745e8f3d65707bb8637a9277805ca9c7a22faeb66230ce0fa87ea9" + "hash": "ff9f1363df5b9dc633767b0d3addedea0baba7447d4557f681f26c666a2c47bc" } diff --git a/Cargo.lock b/Cargo.lock index 5c004874c..fbcd2fe2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,9 +145,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.76" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355" +checksum = "c9d19de80eff169429ac1e9f48fffb163916b448a44e8e046186232046d9e1f9" [[package]] name = "argon2" @@ -225,7 +225,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -236,7 +236,7 @@ checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -667,7 +667,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -849,21 +849,20 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.16" +version = "0.9.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa" +checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", - "memoffset", ] [[package]] name = "crossbeam-queue" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9bcf5bdbfdd6030fb4a1c497b5d5fc5921aa2f60d359a17e249c0e6df3de153" +checksum = "adc6598521bb5a83d491e8c1fe51db7296019d2ca3cb93cc6c2a20369a4d78a2" dependencies = [ "cfg-if", "crossbeam-utils", @@ -871,9 +870,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.17" +version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" +checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" dependencies = [ "cfg-if", ] @@ -941,7 +940,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -965,7 +964,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -976,7 +975,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -1124,7 +1123,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -1464,9 +1463,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -1479,9 +1478,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -1489,15 +1488,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -1517,38 +1516,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -2400,15 +2399,6 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] - [[package]] name = "mime" version = "0.3.17" @@ -2456,7 +2446,7 @@ name = "model_derive" version = "0.1.2" dependencies = [ "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -2607,7 +2597,7 @@ dependencies = [ "proc-macro-crate 2.0.1", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -2631,9 +2621,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -2718,9 +2708,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.61" +version = "0.10.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45" +checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" dependencies = [ "bitflags 2.4.1", "cfg-if", @@ -2739,7 +2729,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -2750,9 +2740,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.97" +version = "0.9.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b" +checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" dependencies = [ "cc", "libc", @@ -2951,7 +2941,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -3030,7 +3020,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -3109,7 +3099,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -3181,9 +3171,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" dependencies = [ "unicode-ident", ] @@ -3231,7 +3221,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.42", + "syn 2.0.43", "tempfile", "which", ] @@ -3246,7 +3236,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -3690,11 +3680,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3822,7 +3812,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -3904,7 +3894,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -4331,7 +4321,7 @@ checksum = "f14a349c27ebe59faba22f933c9c734d428da7231e88a247e9d8c61eea964ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -4353,7 +4343,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -4375,9 +4365,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.42" +version = "2.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b7d0a2c048d661a1a59fcd7355baa232f7ed34e0ee4df2eef3c1c1c0d3852d8" +checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" dependencies = [ "proc-macro2", "quote", @@ -4466,22 +4456,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.51" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.51" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -4583,7 +4573,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -4701,7 +4691,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -4781,7 +4771,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -5097,7 +5087,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", "wasm-bindgen-shared", ] @@ -5131,7 +5121,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5499,22 +5489,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.31" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.31" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] [[package]] @@ -5534,5 +5524,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.43", ] diff --git a/migrations/20231222094917_support_gateway_disconnect.down.sql b/migrations/20231222094917_support_gateway_disconnect.down.sql new file mode 100644 index 000000000..3381636e3 --- /dev/null +++ b/migrations/20231222094917_support_gateway_disconnect.down.sql @@ -0,0 +1 @@ +ALTER TABLE wireguard_network_device DROP COLUMN is_authorized; diff --git a/migrations/20231222094917_support_gateway_disconnect.up.sql b/migrations/20231222094917_support_gateway_disconnect.up.sql new file mode 100644 index 000000000..45c49cd96 --- /dev/null +++ b/migrations/20231222094917_support_gateway_disconnect.up.sql @@ -0,0 +1 @@ +ALTER TABLE wireguard_network_device ADD COLUMN is_authorized bool NOT NULL DEFAULT false; diff --git a/migrations/20231227091628_fix_preshared_key.down.sql b/migrations/20231227091628_fix_preshared_key.down.sql new file mode 100644 index 000000000..f3574d87d --- /dev/null +++ b/migrations/20231227091628_fix_preshared_key.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE device ADD COLUMN preshared_key text NULL; + +-- remove previous column +ALTER TABLE wireguard_network_device DROP COLUMN preshared_key; diff --git a/migrations/20231227091628_fix_preshared_key.up.sql b/migrations/20231227091628_fix_preshared_key.up.sql new file mode 100644 index 000000000..41e5db5e0 --- /dev/null +++ b/migrations/20231227091628_fix_preshared_key.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE wireguard_network_device ADD COLUMN preshared_key text NULL; + +-- remove previous column +ALTER TABLE device DROP COLUMN preshared_key; diff --git a/src/bin/defguard.rs b/src/bin/defguard.rs index 851d1831a..59beccd03 100644 --- a/src/bin/defguard.rs +++ b/src/bin/defguard.rs @@ -16,6 +16,7 @@ use defguard::{ init_dev_env, init_vpn_location, mail::{run_mail_handler, Mail}, run_web_server, + wireguard_peer_disconnect::run_periodic_peer_disconnect, wireguard_stats_purge::run_periodic_stats_purge, SERVER_CONFIG, }; @@ -107,8 +108,9 @@ async fn main() -> Result<(), anyhow::Error> { // run services tokio::select! { _ = run_grpc_server(&config, Arc::clone(&worker_state), pool.clone(), Arc::clone(&gateway_state), wireguard_tx.clone(), mail_tx.clone(), grpc_cert, grpc_key, user_agent_parser.clone(), failed_logins.clone()) => (), - _ = run_web_server(&config, worker_state, gateway_state, webhook_tx, webhook_rx, wireguard_tx, mail_tx, pool.clone(), user_agent_parser, failed_logins) => (), + _ = run_web_server(&config, worker_state, gateway_state, webhook_tx, webhook_rx, wireguard_tx.clone(), mail_tx, pool.clone(), user_agent_parser, failed_logins) => (), () = run_mail_handler(mail_rx, pool.clone()) => (), + _ = run_periodic_peer_disconnect(pool.clone(), wireguard_tx) => (), _ = run_periodic_stats_purge(pool, config.stats_purge_frequency.into(), config.stats_purge_threshold.into()), if !config.disable_stats_purge => (), } Ok(()) diff --git a/src/db/models/device.rs b/src/db/models/device.rs index a74df7ce7..eb66adc18 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -35,7 +35,6 @@ pub struct Device { pub wireguard_pubkey: String, pub user_id: i64, pub created: NaiveDateTime, - pub preshared_key: Option, } impl Display for Device { @@ -59,6 +58,8 @@ pub struct DeviceInfo { pub struct DeviceNetworkInfo { pub network_id: i64, pub device_wireguard_ip: IpAddr, + #[serde(skip_serializing)] + pub preshared_key: Option, } impl DeviceInfo { @@ -70,7 +71,7 @@ impl DeviceInfo { let device_id = device.get_id()?; let network_info = query_as!( DeviceNetworkInfo, - "SELECT wireguard_network_id as network_id, wireguard_ip as \"device_wireguard_ip: IpAddr\" \ + "SELECT wireguard_network_id as network_id, wireguard_ip as \"device_wireguard_ip: IpAddr\", preshared_key \ FROM wireguard_network_device \ WHERE device_id = $1", device_id @@ -165,6 +166,8 @@ pub struct WireguardNetworkDevice { pub wireguard_network_id: i64, pub wireguard_ip: IpAddr, pub device_id: i64, + pub preshared_key: Option, + pub is_authorized: bool, } #[derive(Serialize, Deserialize, Debug)] @@ -186,6 +189,8 @@ impl WireguardNetworkDevice { wireguard_network_id: network_id, wireguard_ip, device_id, + preshared_key: None, + is_authorized: false, } } @@ -195,13 +200,14 @@ impl WireguardNetworkDevice { { query!( "INSERT INTO wireguard_network_device \ - (device_id, wireguard_network_id, wireguard_ip) \ - VALUES ($1, $2, $3) \ + (device_id, wireguard_network_id, wireguard_ip, is_authorized) \ + VALUES ($1, $2, $3, $4) \ ON CONFLICT ON CONSTRAINT device_network \ - DO UPDATE SET wireguard_ip = $3", + DO UPDATE SET wireguard_ip = $3, is_authorized = $4", self.device_id, self.wireguard_network_id, IpNetwork::from(self.wireguard_ip.clone()), + self.is_authorized, ) .execute(executor) .await?; @@ -214,11 +220,12 @@ impl WireguardNetworkDevice { { query!( "UPDATE wireguard_network_device \ - SET wireguard_ip = $3 \ + SET wireguard_ip = $3, is_authorized = $4 \ WHERE device_id = $1 AND wireguard_network_id = $2", self.device_id, self.wireguard_network_id, - IpNetwork::from(self.wireguard_ip.clone()) + IpNetwork::from(self.wireguard_ip.clone()), + self.is_authorized, ) .execute(executor) .await?; @@ -250,8 +257,8 @@ impl WireguardNetworkDevice { { let res = query_as!( Self, - "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\" FROM \ - wireguard_network_device \ + "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\", preshared_key, is_authorized \ + FROM wireguard_network_device \ WHERE device_id = $1 AND wireguard_network_id = $2", device_id, network_id @@ -267,7 +274,7 @@ impl WireguardNetworkDevice { ) -> Result>, SqlxError> { let result = query_as!( Self, - "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\" \ + "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\", preshared_key, is_authorized \ FROM wireguard_network_device WHERE device_id = $1", device_id ) @@ -288,8 +295,8 @@ impl WireguardNetworkDevice { { let res = query_as!( Self, - "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\" FROM \ - wireguard_network_device \ + "SELECT device_id, wireguard_network_id, wireguard_ip as \"wireguard_ip: IpAddr\", preshared_key, is_authorized \ + FROM wireguard_network_device \ WHERE wireguard_network_id = $1", network_id ) @@ -313,19 +320,13 @@ pub enum DeviceError { impl Device { #[must_use] - pub fn new( - name: String, - wireguard_pubkey: String, - preshared_key: Option, - user_id: i64, - ) -> Self { + pub fn new(name: String, wireguard_pubkey: String, user_id: i64) -> Self { Self { id: None, name, wireguard_pubkey, user_id, created: Utc::now().naive_utc(), - preshared_key, } } @@ -391,7 +392,7 @@ impl Device { { query_as!( Self, - "SELECT d.id \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created, d.preshared_key \ + "SELECT d.id \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created \ FROM device d \ JOIN wireguard_network_device wnd \ ON d.id = wnd.device_id \ @@ -409,7 +410,7 @@ impl Device { { query_as!( Self, - "SELECT id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key \ + "SELECT id \"id?\", name, wireguard_pubkey, user_id, created \ FROM device WHERE wireguard_pubkey = $1", pubkey ) @@ -424,7 +425,7 @@ impl Device { ) -> Result, SqlxError> { query_as!( Self, - "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key \ + "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created \ FROM device JOIN \"user\" ON device.user_id = \"user\".id \ WHERE device.id = $1 AND \"user\".username = $2", id, @@ -441,7 +442,7 @@ impl Device { ) -> Result, SqlxError> { query_as!( Self, - "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key \ + "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created \ FROM device JOIN \"user\" ON device.user_id = \"user\".id \ WHERE device.id = $1 AND \"user\".id = $2", id, @@ -475,7 +476,7 @@ impl Device { pub async fn all_for_username(pool: &DbPool, username: &str) -> Result, SqlxError> { query_as!( Self, - "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key \ + "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created \ FROM device JOIN \"user\" ON device.user_id = \"user\".id \ WHERE \"user\".username = $1", username @@ -535,6 +536,7 @@ impl Device { let device_network_info = DeviceNetworkInfo { network_id, device_wireguard_ip: wireguard_network_device.wireguard_ip, + preshared_key: wireguard_network_device.preshared_key.clone(), }; network_info.push(device_network_info); @@ -630,7 +632,7 @@ mod test { } // Break loop if IP is unassigned and return device if Self::find_by_ip(pool, ip, network_id).await?.is_none() { - let mut device = Self::new(name.clone(), pubkey, None, user_id); + let mut device = Self::new(name.clone(), pubkey, user_id); device.save(pool).await?; info!("Created device: {}", device.name); debug!("For user: {}", device.user_id); diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 0ad9a3cba..c50328434 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -633,7 +633,7 @@ impl User { if let Some(id) = self.id { let devices = query_as!( Device, - "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created, preshared_key \ + "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created \ FROM device WHERE user_id = $1", id ) diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index ab58d69ca..a8107b436 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -311,18 +311,18 @@ impl WireguardNetwork { // devices need to be filtered by allowed group Some(allowed_groups) => { query_as!( - Device, - "SELECT DISTINCT ON (d.id) d.id as \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created, d.preshared_key \ - FROM device d \ - JOIN \"user\" u ON d.user_id = u.id \ - JOIN group_user gu ON u.id = gu.user_id \ - JOIN \"group\" g ON gu.group_id = g.id \ - WHERE g.\"name\" IN (SELECT * FROM UNNEST($1::text[])) - ORDER BY d.id ASC", - &allowed_groups - ) - .fetch_all(&mut *transaction) - .await? + Device, + "SELECT DISTINCT ON (d.id) d.id as \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created \ + FROM device d \ + JOIN \"user\" u ON d.user_id = u.id \ + JOIN group_user gu ON u.id = gu.user_id \ + JOIN \"group\" g ON gu.group_id = g.id \ + WHERE g.\"name\" IN (SELECT * FROM UNNEST($1::text[])) + ORDER BY d.id ASC", + &allowed_groups + ) + .fetch_all(&mut *transaction) + .await? }, // all devices are allowed None => { @@ -429,6 +429,7 @@ impl WireguardNetwork { network_info: vec![DeviceNetworkInfo { network_id, device_wireguard_ip: wireguard_network_device.wireguard_ip, + preshared_key: wireguard_network_device.preshared_key, }], })); } @@ -447,6 +448,7 @@ impl WireguardNetwork { network_info: vec![DeviceNetworkInfo { network_id, device_wireguard_ip: device_network_config.wireguard_ip, + preshared_key: device_network_config.preshared_key, }], })); } else { @@ -467,6 +469,7 @@ impl WireguardNetwork { network_info: vec![DeviceNetworkInfo { network_id, device_wireguard_ip: wireguard_network_device.wireguard_ip, + preshared_key: wireguard_network_device.preshared_key, }], })); } @@ -525,6 +528,7 @@ impl WireguardNetwork { network_info: vec![DeviceNetworkInfo { network_id, device_wireguard_ip: wireguard_network_device.wireguard_ip, + preshared_key: wireguard_network_device.preshared_key, }], })); } @@ -569,7 +573,6 @@ impl WireguardNetwork { let mut device = Device::new( mapped_device.name.clone(), mapped_device.wireguard_pubkey.clone(), - None, mapped_device.user_id, ); device.save(&mut *transaction).await?; @@ -603,6 +606,7 @@ impl WireguardNetwork { network_info.push(DeviceNetworkInfo { network_id, device_wireguard_ip: wireguard_network_device.wireguard_ip, + preshared_key: wireguard_network_device.preshared_key, }); } Some(allowed) => { @@ -618,6 +622,7 @@ impl WireguardNetwork { network_info.push(DeviceNetworkInfo { network_id, device_wireguard_ip: wireguard_network_device.wireguard_ip, + preshared_key: wireguard_network_device.preshared_key, }); } } @@ -783,7 +788,7 @@ impl WireguardNetwork { ORDER BY device_id, latest_handshake DESC \ ) \ SELECT \ - d.id \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created, d.preshared_key \ + d.id \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created \ FROM device d \ JOIN s ON d.id = s.device_id \ WHERE s.latest_handshake >= $1 AND s.network = $2", @@ -1105,7 +1110,7 @@ mod test { None, ); user.save(&pool).await.unwrap(); - let mut device = Device::new(String::new(), String::new(), None, user.id.unwrap()); + let mut device = Device::new(String::new(), String::new(), user.id.unwrap()); device.save(&pool).await.unwrap(); // insert stats @@ -1155,7 +1160,7 @@ mod test { None, ); user.save(&pool).await.unwrap(); - let mut device = Device::new(String::new(), String::new(), None, user.id.unwrap()); + let mut device = Device::new(String::new(), String::new(), user.id.unwrap()); device.save(&pool).await.unwrap(); // insert stats diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index a7fe62836..5b055e2d5 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -320,7 +320,7 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { error!("Invalid pubkey {}", request.pubkey); Status::invalid_argument("invalid pubkey") })?; - let mut device = Device::new(request.name, request.pubkey, None, enrollment.user_id); + let mut device = Device::new(request.name, request.pubkey, enrollment.user_id); let mut transaction = self.pool.begin().await.map_err(|_| { error!("Failed to begin transaction"); diff --git a/src/grpc/gateway.rs b/src/grpc/gateway.rs index 6f5b83bbd..328734daa 100644 --- a/src/grpc/gateway.rs +++ b/src/grpc/gateway.rs @@ -35,7 +35,10 @@ pub struct GatewayServer { } impl WireguardNetwork { - /// Get a list of all peers + /// Get a list of all allowed peers + /// + /// Each device is marked as allowed or not allowed in a given network, + /// which enables enforcing peer disconnect in MFA-protected networks. pub async fn get_peers<'e, E>(&self, executor: E) -> Result, SqlxError> where E: PgExecutor<'e>, @@ -46,9 +49,10 @@ impl WireguardNetwork { array[host(wnd.wireguard_ip)] as \"allowed_ips!: Vec\" \ FROM wireguard_network_device wnd \ JOIN device d ON wnd.device_id = d.id \ - WHERE wireguard_network_id = $1 \ + WHERE wireguard_network_id = $1 AND (is_authorized = true OR NOT $2) \ ORDER BY d.id ASC", self.id, + self.mfa_enabled ) .fetch_all(executor) .await?; @@ -230,7 +234,7 @@ impl GatewayUpdatesHandler { Peer { pubkey: device.device.wireguard_pubkey, allowed_ips: vec![network_info.device_wireguard_ip.to_string()], - preshared_key: device.device.preshared_key, + preshared_key: network_info.preshared_key.clone(), keepalive_interval: Some( self.network.keepalive_interval as u32, ), @@ -254,7 +258,7 @@ impl GatewayUpdatesHandler { Peer { pubkey: device.device.wireguard_pubkey, allowed_ips: vec![network_info.device_wireguard_ip.to_string()], - preshared_key: device.device.preshared_key, + preshared_key: network_info.preshared_key.clone(), keepalive_interval: Some( self.network.keepalive_interval as u32, ), @@ -515,6 +519,7 @@ impl gateway_service_server::GatewayService for GatewayServer { info!("Sending configuration to gateway client, network {network}."); + // store connected gateway in memory { let mut state = self.state.lock().unwrap(); state.add_gateway( diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index 7096e2a88..96cbc6b00 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -488,7 +488,7 @@ pub async fn add_device( let Some(user_id) = user.id else { return Err(WebError::ModelError("User has no id".to_string())); }; - let mut device = Device::new(add_device.name, add_device.wireguard_pubkey, None, user_id); + let mut device = Device::new(add_device.name, add_device.wireguard_pubkey, user_id); let mut transaction = appstate.pool.begin().await?; device.save(&mut *transaction).await?; @@ -599,6 +599,7 @@ pub async fn modify_device( let device_network_info = DeviceNetworkInfo { network_id, device_wireguard_ip: wireguard_network_device.wireguard_ip, + preshared_key: wireguard_network_device.preshared_key, }; network_info.push(device_network_info); } diff --git a/src/lib.rs b/src/lib.rs index ed0fdc81b..51c902410 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,6 +115,7 @@ pub mod secret; pub mod support; pub mod templates; pub mod wg_config; +pub mod wireguard_peer_disconnect; pub mod wireguard_stats_purge; #[macro_use] @@ -441,7 +442,6 @@ pub async fn init_dev_env(config: &DefGuardConfig) { let mut device = Device::new( "TestDevice".to_string(), "gQYL5eMeFDj0R+lpC7oZyIl0/sNVmQDC6ckP7husZjc=".to_string(), - None, 1, ); device diff --git a/src/wireguard_peer_disconnect.rs b/src/wireguard_peer_disconnect.rs new file mode 100644 index 000000000..ea025cb8f --- /dev/null +++ b/src/wireguard_peer_disconnect.rs @@ -0,0 +1,126 @@ +//! This module implements a functionality of disconnecting inactive peers +//! in MFA-protected locations. +//! If a device does not disconnect explicitly and just becomes inactive +//! it should be removed from gateway configuration and marked as "not allowed", +//! which enforces an authentication requirement to connect again. + +use crate::db::{ + models::{ + device::{DeviceInfo, DeviceNetworkInfo, WireguardNetworkDevice}, + error::ModelError, + wireguard::WireguardNetworkError, + }, + DbPool, Device, GatewayEvent, WireguardNetwork, +}; +use sqlx::{query_as, Error as SqlxError}; +use std::time::Duration; +use thiserror::Error; +use tokio::{sync::broadcast::Sender, time::sleep}; + +// How long to sleep between loop iterations +const DISCONNECT_LOOP_SLEEP_SECONDS: u64 = 180; // 3 minutes + +#[derive(Debug, Error)] +pub enum PeerDisconnectError { + #[error(transparent)] + DbError(#[from] SqlxError), + #[error(transparent)] + ModelError(#[from] ModelError), + #[error(transparent)] + WireguardError(#[from] WireguardNetworkError), + #[error("Failed to send gateway event: {0}")] + EventError(String), +} + +/// Run periodic disconnect task +/// +/// Run with a specified frequency and disconnect all inactive peers in MFA-protected locations. +pub async fn run_periodic_peer_disconnect( + pool: DbPool, + wireguard_tx: Sender, +) -> Result<(), PeerDisconnectError> { + info!("Starting periodic disconnect of inactive devices in MFA-protected locations"); + loop { + debug!("Starting periodic inactive device disconnect"); + + // get all MFA-protected locations + let locations = query_as!( + WireguardNetwork, + "SELECT \ + id as \"id?\", name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \ + connected_at, mfa_enabled, keepalive_interval, peer_disconnect_threshold \ + FROM wireguard_network WHERE mfa_enabled = true", + ) + .fetch_all(&pool) + .await?; + + // loop over all locations + for location in locations { + debug!("Fetching inactive devices for location {location}"); + let location_id = location.get_id()?; + let devices = query_as!( + Device, + "WITH stats AS ( \ + SELECT DISTINCT ON (device_id) device_id, endpoint, latest_handshake \ + FROM wireguard_peer_stats \ + WHERE network = $1 \ + ORDER BY device_id, collected_at DESC \ + ) \ + SELECT d.id as \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created \ + FROM device d \ + JOIN wireguard_network_device wnd ON wnd.device_id = d.id \ + LEFT JOIN stats on d.id = stats.device_id \ + WHERE wnd.wireguard_network_id = $1 AND wnd.is_authorized = true AND (NOW() - stats.latest_handshake) > $2 * interval '1 second'", + location_id, + location.peer_disconnect_threshold as f64 + ) + .fetch_all(&pool) + .await?; + + for device in devices { + debug!("Processing inactive device {device}"); + let device_id = device.get_id()?; + + // start transaction + let mut transaction = pool.begin().await?; + + // get network config for device + if let Some(mut device_network_config) = + WireguardNetworkDevice::find(&mut *transaction, device_id, location_id).await? + { + info!("Marking device {device} as not authorized to connect to location {location}"); + // change `is_authorized` value for device + device_network_config.is_authorized = false; + // clear `preshared_key` value + device_network_config.preshared_key = None; + device_network_config.update(&mut *transaction).await?; + + debug!("Sending `peer_delete` message to gateway"); + let device_info = DeviceInfo { + device, + network_info: vec![DeviceNetworkInfo { + network_id: location_id, + device_wireguard_ip: device_network_config.wireguard_ip, + preshared_key: device_network_config.preshared_key, + }], + }; + let event = GatewayEvent::DeviceDeleted(device_info); + wireguard_tx.send(event).map_err(|err| { + error!("Error sending WireGuard event: {err}"); + PeerDisconnectError::EventError(err.to_string()) + })?; + } else { + error!("Network config for device {device} in location {location} not found. Skipping device..."); + continue; + } + + // commit transaction + transaction.commit().await?; + } + } + + // wait till next iteration + debug!("Sleeping until next iteration"); + sleep(Duration::from_secs(DISCONNECT_LOOP_SLEEP_SECONDS)).await; + } +} diff --git a/tests/wireguard_network_allowed_groups.rs b/tests/wireguard_network_allowed_groups.rs index 4b8c53ab9..0822163a6 100644 --- a/tests/wireguard_network_allowed_groups.rs +++ b/tests/wireguard_network_allowed_groups.rs @@ -30,7 +30,6 @@ async fn setup_test_users(pool: &DbPool) -> (Vec, Vec) { let mut admin_device = Device::new( "admin device".into(), "nst4lmZz9kPTq6OdeQq2G2th3n+QneHKmG1wJJ3Jrq0=".into(), - None, admin_user.id.unwrap(), ); admin_device.save(pool).await.unwrap(); @@ -46,7 +45,6 @@ async fn setup_test_users(pool: &DbPool) -> (Vec, Vec) { let mut test_device = Device::new( "test device".into(), "wYOt6ImBaQ3BEMQ3Xf5P5fTnbqwOvjcqYkkSBt+1xOg=".into(), - None, test_user.id.unwrap(), ); test_device.save(pool).await.unwrap(); @@ -70,7 +68,6 @@ async fn setup_test_users(pool: &DbPool) -> (Vec, Vec) { let mut other_device = Device::new( "other device".into(), "v2U14sjNN4tOYD3P15z0WkjriKY9Hl85I3vIEPomrYs=".into(), - None, other_user.id.unwrap(), ); other_device.save(pool).await.unwrap(); @@ -90,7 +87,6 @@ async fn setup_test_users(pool: &DbPool) -> (Vec, Vec) { let mut non_group_device = Device::new( "non group device".into(), "6xmL/jRuxmzQ3J2/kVZnKnh+6dwODcEEczmmkIKU4sM=".into(), - None, non_group_user.id.unwrap(), ); non_group_device.save(pool).await.unwrap(); diff --git a/tests/wireguard_network_import.rs b/tests/wireguard_network_import.rs index aeeb7e4f8..9ddbcc91b 100644 --- a/tests/wireguard_network_import.rs +++ b/tests/wireguard_network_import.rs @@ -65,7 +65,6 @@ async fn test_config_import() { let mut device_1 = Device::new( "test device".into(), "l07+qPWs4jzW3Gp1DKbHgBMRRm4Jg3q2BJxw0ZYl6c4=".into(), - None, 1, ); device_1.save(&mut *transaction).await.unwrap(); @@ -77,7 +76,6 @@ async fn test_config_import() { let mut device_2 = Device::new( "another test device".into(), "v2U14sjNN4tOYD3P15z0WkjriKY9Hl85I3vIEPomrYs=".into(), - None, 1, ); device_2.save(&mut *transaction).await.unwrap(); From e7126378bdb7ace678aca4c7aef269e1af613934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= <102536422+filipslezaklab@users.noreply.github.com> Date: Tue, 2 Jan 2024 11:44:22 +0100 Subject: [PATCH 11/26] fix: allow safe special chars in username and password for users (#493) --- web/src/i18n/en/index.ts | 7 ++++--- web/src/i18n/i18n-types.ts | 20 +++++++++++++------ web/src/i18n/pl/index.ts | 9 +++++---- web/src/pages/network/style.scss | 2 +- .../components/AddUserForm/AddUserForm.tsx | 6 ++---- .../WizardNetworkConfiguration/style.scss | 2 +- web/src/shared/patterns.ts | 5 +++++ web/src/shared/validators/password.ts | 2 ++ 8 files changed, 34 insertions(+), 19 deletions(-) diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index deed5ae3b..b53083ef1 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -711,7 +711,8 @@ const en: BaseTranslation = { username: 'Username', }, error: { - usernameTaken: 'Username is already in use', + forbiddenCharacter: 'Field contain forbidden characters.', + usernameTaken: 'Username is already in use.', invalidKey: 'Key is invalid.', invalid: 'Field is invalid.', required: 'Field is required.', @@ -728,8 +729,8 @@ const en: BaseTranslation = { validPort: 'Enter a valid port.', validCode: 'Code should have 6 digits.', allowedIps: 'Only valid IP or domain is allowed.', - startFromNumber: 'Cannot start from number', - repeat: `Fields don't match`, + startFromNumber: 'Cannot start from number.', + repeat: `Fields don't match.`, }, floatingErrors: { title: 'Please correct the following:', diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index 1d30e200b..8b6df2feb 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -1670,7 +1670,11 @@ type RootTranslation = { } error: { /** - * U​s​e​r​n​a​m​e​ ​i​s​ ​a​l​r​e​a​d​y​ ​i​n​ ​u​s​e + * F​i​e​l​d​ ​c​o​n​t​a​i​n​ ​f​o​r​b​i​d​d​e​n​ ​c​h​a​r​a​c​t​e​r​s​. + */ + forbiddenCharacter: string + /** + * U​s​e​r​n​a​m​e​ ​i​s​ ​a​l​r​e​a​d​y​ ​i​n​ ​u​s​e​. */ usernameTaken: string /** @@ -1738,11 +1742,11 @@ type RootTranslation = { */ allowedIps: string /** - * C​a​n​n​o​t​ ​s​t​a​r​t​ ​f​r​o​m​ ​n​u​m​b​e​r + * C​a​n​n​o​t​ ​s​t​a​r​t​ ​f​r​o​m​ ​n​u​m​b​e​r​. */ startFromNumber: string /** - * F​i​e​l​d​s​ ​d​o​n​'​t​ ​m​a​t​c​h + * F​i​e​l​d​s​ ​d​o​n​'​t​ ​m​a​t​c​h​. */ repeat: string } @@ -5206,7 +5210,11 @@ export type TranslationFunctions = { } error: { /** - * Username is already in use + * Field contain forbidden characters. + */ + forbiddenCharacter: () => LocalizedString + /** + * Username is already in use. */ usernameTaken: () => LocalizedString /** @@ -5274,11 +5282,11 @@ export type TranslationFunctions = { */ allowedIps: () => LocalizedString /** - * Cannot start from number + * Cannot start from number. */ startFromNumber: () => LocalizedString /** - * Fields don't match + * Fields don't match. */ repeat: () => LocalizedString } diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index d4327383c..fe41d274f 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -696,12 +696,13 @@ Uwaga, konfiguracje tutaj podane, nie posiadają twojego klucza prywatnego. Musi username: 'Nazwa użytkownika', }, error: { - usernameTaken: 'Nazwa użytkownika jest już w użyciu', + forbiddenCharacter: 'Pole zawiera niedozwolone znaki.', + usernameTaken: 'Nazwa użytkownika jest już w użyciu.', invalidKey: 'Klucz jest nieprawidłowy.', invalid: 'Pole jest nieprawidłowe.', required: 'Pole jest wymagane.', maximumLength: 'Maksymalna długość przekroczona.', - minimumLength: 'Minimalna długość nie została osiągnięta', + minimumLength: 'Minimalna długość nie została osiągnięta.', noSpecialChars: 'Nie wolno używać znaków specjalnych.', oneDigit: 'Wymagana jedna cyfra.', oneSpecial: 'Wymagany jest znak specjalny.', @@ -713,8 +714,8 @@ Uwaga, konfiguracje tutaj podane, nie posiadają twojego klucza prywatnego. Musi validPort: 'Wprowadź prawidłowy port.', validCode: 'Kod powinien mieć 6 cyfr.', allowedIps: 'Tylko poprawne adresy IP oraz domeny.', - startFromNumber: 'Nie może zaczynać się od liczby', - repeat: 'Wartości się nie pokrywają', + startFromNumber: 'Nie może zaczynać się od liczby.', + repeat: 'Wartości się nie pokrywają.', }, floatingErrors: { title: 'Popraw następujące błędy:', diff --git a/web/src/pages/network/style.scss b/web/src/pages/network/style.scss index cc62fa838..185c669d1 100644 --- a/web/src/pages/network/style.scss +++ b/web/src/pages/network/style.scss @@ -119,7 +119,7 @@ } } - & > .form-checkbox { + & > .form-checkbox { margin-bottom: 25px; } } diff --git a/web/src/pages/users/UsersOverview/modals/AddUserModal/components/AddUserForm/AddUserForm.tsx b/web/src/pages/users/UsersOverview/modals/AddUserModal/components/AddUserForm/AddUserForm.tsx index e1c3fa66b..224e49e2a 100644 --- a/web/src/pages/users/UsersOverview/modals/AddUserModal/components/AddUserForm/AddUserForm.tsx +++ b/web/src/pages/users/UsersOverview/modals/AddUserModal/components/AddUserForm/AddUserForm.tsx @@ -19,8 +19,7 @@ import { import useApi from '../../../../../../../shared/hooks/useApi'; import { useToaster } from '../../../../../../../shared/hooks/useToaster'; import { - patternDigitOrLowercase, - patternNoSpecialChars, + patternSafeUsernameCharacters, patternStartsWithDigit, patternValidEmail, patternValidPhoneNumber, @@ -57,8 +56,7 @@ export const AddUserForm = () => { username: yup .string() .required(LL.form.error.required()) - .matches(patternNoSpecialChars, LL.form.error.noSpecialChars()) - .matches(patternDigitOrLowercase, LL.form.error.invalid()) + .matches(patternSafeUsernameCharacters, LL.form.error.forbiddenCharacter()) .min(3, LL.form.error.minimumLength()) .max(64, LL.form.error.maximumLength()) .test('starts-with-number', LL.form.error.startFromNumber(), (value) => { diff --git a/web/src/pages/wizard/components/WizardNetworkConfiguration/style.scss b/web/src/pages/wizard/components/WizardNetworkConfiguration/style.scss index 5d705890a..269f55821 100644 --- a/web/src/pages/wizard/components/WizardNetworkConfiguration/style.scss +++ b/web/src/pages/wizard/components/WizardNetworkConfiguration/style.scss @@ -21,7 +21,7 @@ margin-bottom: 25px; } - & > .form-checkbox { + & > .form-checkbox { margin-bottom: 25px; } } diff --git a/web/src/shared/patterns.ts b/web/src/shared/patterns.ts index 307b3ec84..9c497c7bb 100644 --- a/web/src/shared/patterns.ts +++ b/web/src/shared/patterns.ts @@ -72,3 +72,8 @@ export const patternValidDomain = export const patternValidIp = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + +export const patternSafeUsernameCharacters = + /^[a-zA-Z0-9.!@#$%^&*()_+\-=\[\]{}|,<>\/?~]+$/; + +export const patternSafePasswordCharacters = patternSafeUsernameCharacters; diff --git a/web/src/shared/validators/password.ts b/web/src/shared/validators/password.ts index 7b1ce2b11..319ae23d3 100644 --- a/web/src/shared/validators/password.ts +++ b/web/src/shared/validators/password.ts @@ -6,6 +6,7 @@ import { patternAtLeastOneLowerCaseChar, patternAtLeastOneSpecialChar, patternAtLeastOneUpperCaseChar, + patternSafePasswordCharacters, } from '../patterns'; export const passwordValidator = (LL: TranslationFunctions) => @@ -17,4 +18,5 @@ export const passwordValidator = (LL: TranslationFunctions) => .matches(patternAtLeastOneSpecialChar, LL.form.error.oneSpecial()) .matches(patternAtLeastOneUpperCaseChar, LL.form.error.oneUppercase()) .matches(patternAtLeastOneLowerCaseChar, LL.form.error.oneLowercase()) + .matches(patternSafePasswordCharacters, LL.form.error.forbiddenCharacter()) .required(LL.form.error.required()); From 3d84b24e329d252d4a52d91743bfd7256d5fdc15 Mon Sep 17 00:00:00 2001 From: Maciek Date: Tue, 2 Jan 2024 12:37:42 +0100 Subject: [PATCH 12/26] feat: update instance & location info in client enrollment (#492) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix typo * include additional instance info in enrollment response * update dependencies * fix username * update protos * update query data * update query data --------- Co-authored-by: Maciej Wójcik --- Cargo.lock | 168 ++++++++++++++++++++-------------------- proto | 2 +- src/db/models/device.rs | 4 + src/grpc/enrollment.rs | 44 +++++++---- 4 files changed, 121 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbcd2fe2f..4506cb661 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,9 +54,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" dependencies = [ "cfg-if", "getrandom", @@ -145,9 +145,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.77" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9d19de80eff169429ac1e9f48fffb163916b448a44e8e046186232046d9e1f9" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "argon2" @@ -225,18 +225,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] name = "async-trait" -version = "0.1.75" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -306,12 +306,12 @@ dependencies = [ [[package]] name = "axum" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202651474fe73c62d9e0a56c6133f7a0ff1dc1c8cf7a5b03381af2a26553ac9d" +checksum = "d09dbe0e490df5da9d69b36dca48a76635288a82f92eca90024883a56202026d" dependencies = [ "async-trait", - "axum-core 0.4.1", + "axum-core 0.4.2", "bytes", "futures-util", "http 1.0.0", @@ -335,6 +335,7 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -343,7 +344,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f5ffe4637708b326c621d5494ab6c91dcf62ee440fa6ee967d289315a9c6f81" dependencies = [ - "axum 0.7.2", + "axum 0.7.3", "forwarded-header-value", "serde", ] @@ -367,9 +368,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77cb22c689c44d4c07b0ab44ebc25d69d8ae601a2f28fb8d672d344178fa17aa" +checksum = "e87c8503f93e6d144ee5690907ba22db7ba79ab001a932ab99034f0fe836b3df" dependencies = [ "async-trait", "bytes", @@ -383,16 +384,17 @@ dependencies = [ "sync_wrapper", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "axum-extra" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523ae92256049a3b02d3bb4df80152386cd97ddba0c8c5077619bdc8c4b1859b" +checksum = "881348a37b079994894b6e5e46edcc4b8a60e1c0333669a65b810abeed780598" dependencies = [ - "axum 0.7.2", - "axum-core 0.4.1", + "axum 0.7.3", + "axum-core 0.4.2", "bytes", "cookie 0.18.0", "futures-util", @@ -520,9 +522,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" +checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" dependencies = [ "memchr", "serde", @@ -587,9 +589,9 @@ dependencies = [ [[package]] name = "chrono-tz" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e23185c0e21df6ed832a12e2bda87c7d1def6842881fb634a8511ced741b0d76" +checksum = "91d7b79e99bfaa0d47da0687c43aa3b7381938a62ad3a6498599039321f660b7" dependencies = [ "chrono", "chrono-tz-build", @@ -638,9 +640,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.11" +version = "4.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" +checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" dependencies = [ "clap_builder", "clap_derive", @@ -648,9 +650,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.11" +version = "4.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" +checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" dependencies = [ "anstream", "anstyle", @@ -667,7 +669,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -940,7 +942,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -964,7 +966,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -975,7 +977,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -990,7 +992,7 @@ version = "0.8.0" dependencies = [ "anyhow", "argon2", - "axum 0.7.2", + "axum 0.7.3", "axum-client-ip", "axum-extra", "base64 0.21.5", @@ -1076,9 +1078,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", "serde", @@ -1123,7 +1125,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -1528,7 +1530,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -1988,9 +1990,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2395,9 +2397,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "mime" @@ -2446,7 +2448,7 @@ name = "model_derive" version = "0.1.2" dependencies = [ "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -2597,7 +2599,7 @@ dependencies = [ "proc-macro-crate 2.0.1", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -2729,7 +2731,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -2941,7 +2943,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -3020,7 +3022,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -3064,9 +3066,9 @@ checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" [[package]] name = "platforms" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0" +checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" [[package]] name = "polyval" @@ -3094,12 +3096,12 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "prettyplease" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -3171,9 +3173,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.71" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" +checksum = "2de98502f212cfcea8d0bb305bd0f49d7ebdd75b64ba0a68f937d888f4e0d6db" dependencies = [ "unicode-ident", ] @@ -3221,7 +3223,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.43", + "syn 2.0.46", "tempfile", "which", ] @@ -3236,7 +3238,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -3287,9 +3289,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -3812,14 +3814,14 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9" dependencies = [ "itoa", "ryu", @@ -3894,7 +3896,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -4321,7 +4323,7 @@ checksum = "f14a349c27ebe59faba22f933c9c734d428da7231e88a247e9d8c61eea964ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -4343,7 +4345,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -4365,9 +4367,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.43" +version = "2.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" +checksum = "89456b690ff72fddcecf231caedbe615c59480c93358a93dfae7fc29e3ebbf0e" dependencies = [ "proc-macro2", "quote", @@ -4421,15 +4423,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", "fastrand", "redox_syscall", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -4456,22 +4458,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.52" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.52" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -4573,7 +4575,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -4691,7 +4693,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -4771,7 +4773,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -5087,7 +5089,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", "wasm-bindgen-shared", ] @@ -5121,7 +5123,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5281,11 +5283,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] @@ -5422,9 +5424,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.30" +version = "0.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b5c3db89721d50d0e2a673f5043fc4722f76dcc352d7b1ab8b8288bed4ed2c5" +checksum = "97a4882e6b134d6c28953a387571f1acdd3496830d5e36c5e3a1075580ea641c" dependencies = [ "memchr", ] @@ -5504,7 +5506,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] [[package]] @@ -5524,5 +5526,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.46", ] diff --git a/proto b/proto index 44b0593cd..9f5c90266 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 44b0593cdd8a0b7cf44386cf6097add59ef79781 +Subproject commit 9f5c90266c9d3449c38197b72e8a9d8a56f12cfd diff --git a/src/db/models/device.rs b/src/db/models/device.rs index eb66adc18..4cdcfa170 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -26,6 +26,8 @@ pub struct DeviceConfig { pub(crate) allowed_ips: Vec, pub(crate) pubkey: String, pub(crate) dns: Option, + pub(crate) mfa_enabled: bool, + pub(crate) keepalive_interval: i32, } #[derive(Clone, Deserialize, Model, Serialize, Debug)] @@ -550,6 +552,8 @@ impl Device { allowed_ips: network.allowed_ips, pubkey: network.pubkey, dns: network.dns, + mfa_enabled: network.mfa_enabled, + keepalive_interval: network.keepalive_interval, }); } } diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 5b055e2d5..075c6e2f9 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -15,6 +15,7 @@ use crate::{ ldap::utils::ldap_add_user, mail::Mail, templates::{self, TemplateLocation}, + SERVER_CONFIG, }; use ipnetwork::IpNetwork; use reqwest::Url; @@ -42,28 +43,35 @@ pub struct EnrollmentServer { ldap_feature_active: bool, } -struct Instance { +struct InstanceInfo { id: uuid::Uuid, name: String, url: Url, + proxy_url: Url, + username: String, } -impl Instance { - pub fn new(settings: Settings, url: Url) -> Self { - Instance { +impl InstanceInfo { + pub fn new>(settings: Settings, username: S) -> Self { + let config = SERVER_CONFIG.get().expect("defguard config not found"); + InstanceInfo { id: settings.uuid, name: settings.instance_name, - url, + url: config.url.clone(), + proxy_url: config.enrollment_url.clone(), + username: username.into(), } } } -impl From for proto::InstanceInfo { - fn from(instance: Instance) -> Self { +impl From for proto::InstanceInfo { + fn from(instance: InstanceInfo) -> Self { Self { name: instance.name, id: instance.id.to_string(), url: instance.url.to_string(), + proxy_url: instance.proxy_url.to_string(), + username: instance.username, } } } @@ -163,6 +171,9 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { Status::internal("unexpected error") })?; + let vpn_setup_optional = settings.enrollment_vpn_step_optional; + let instance_info = InstanceInfo::new(settings, &user.username); + let user_info = InitialUserInfo::from_user(&self.pool, user) .await .map_err(|_| { @@ -179,8 +190,8 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { final_page_content: enrollment .get_welcome_page_content(&mut transaction) .await?, - vpn_setup_optional: settings.enrollment_vpn_step_optional, - instance: Some(Instance::new(settings, self.config.url.clone()).into()), + vpn_setup_optional, + instance: Some(instance_info.into()), }; transaction.commit().await.map_err(|_| { @@ -376,11 +387,11 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { Some(&ip_address), device_info.as_deref(), ) - .map_err(|_| Status::internal("Failed to render new device added tempalte"))?; + .map_err(|_| Status::internal("Failed to render new device added template"))?; let response = DeviceConfigResponse { device: Some(device.into()), configs: configs.into_iter().map(Into::into).collect(), - instance: Some(Instance::new(settings, self.config.url.clone()).into()), + instance: Some(InstanceInfo::new(settings, &user.username).into()), }; Ok(Response::new(response)) @@ -392,7 +403,10 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { &self, request: Request, ) -> Result, Status> { - let _enrollment = self.validate_session(&request).await?; + let enrollment = self.validate_session(&request).await?; + + // get enrollment user + let user = enrollment.fetch_user(&self.pool).await?; let request = request.into_inner(); Device::validate_pubkey(&request.pubkey).map_err(|_| { @@ -446,6 +460,8 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { pubkey: network.pubkey, allowed_ips, dns: network.dns, + mfa_enabled: network.mfa_enabled, + keepalive_interval: network.keepalive_interval, }; configs.push(config); } @@ -454,7 +470,7 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { let response = DeviceConfigResponse { device: Some(device.into()), configs, - instance: Some(Instance::new(settings, self.config.url.clone()).into()), + instance: Some(InstanceInfo::new(settings, &user.username).into()), }; Ok(Response::new(response)) @@ -508,6 +524,8 @@ impl From for ProtoDeviceConfig { pubkey: config.pubkey, allowed_ips, dns: config.dns, + mfa_enabled: config.mfa_enabled, + keepalive_interval: config.keepalive_interval, } } } From 5c4af3fafabea708221fb9a63053858fe7696d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= <102536422+filipslezaklab@users.noreply.github.com> Date: Fri, 5 Jan 2024 11:02:29 +0100 Subject: [PATCH 13/26] docs: e2e readme (#495) --- e2e/README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 e2e/README.md diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 000000000..4b8df1932 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,26 @@ +# Defguard E2E tests powered by Playwright + +## Prerequisites + +- Docker +- Docker compose +- Node +- pnpm + +## How to run +Pull docker images: +```bash +docker-compose --file ../docker-compose.e2e.yaml pull +``` +Install packages: +```bash +pnpm install +``` +Install playwright chromium driver: +```bash +npx playwright install --with-deps chromium +``` +Run tests: +```bash +pnpm test +``` From 90b1210ea483727e53a6060215c66e40620c0a68 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 15 Jan 2024 18:58:59 +0100 Subject: [PATCH 14/26] feat: reverse proxy gRPC service communication (#496) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial gRPC-to-proxy client * Improve gRPC client * Let RPC be reversed * Use new protos; proxy communication is optional * update proto submodule * remove default value * proxy grpc CA setup * use async sleep * implement error responses * rename field * fix log wording * log task results * update proto submodule * bump rust version * update dependencies --------- Co-authored-by: Maciej Wójcik --- .github/workflows/ci.yml | 2 +- .github/workflows/docs.yml | 2 +- Cargo.lock | 341 +++++++++++++++++++----------------- Dockerfile | 2 +- build.rs | 11 +- proto | 2 +- rust-toolchain.toml | 2 +- src/appstate.rs | 2 +- src/auth/failed_login.rs | 9 +- src/bin/defguard.rs | 13 +- src/config.rs | 8 + src/db/models/enrollment.rs | 2 +- src/grpc/auth.rs | 8 +- src/grpc/enrollment.rs | 146 +++++++-------- src/grpc/mod.rs | 213 +++++++++++++++++++--- src/grpc/password_reset.rs | 140 +++++++-------- src/headers.rs | 7 +- 17 files changed, 516 insertions(+), 394 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01732df3f..8fbcfe86b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ env: jobs: test: runs-on: [self-hosted, Linux] - container: rust:1.74 + container: rust:1.75 services: postgres: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f4fed2500..8bec6c503 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,7 +16,7 @@ env: jobs: rustdoc: runs-on: [self-hosted, Linux] - container: rust:1.74 + container: rust:1.75 services: postgres: image: postgres:15-alpine diff --git a/Cargo.lock b/Cargo.lock index 4506cb661..8d24b7a11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,9 +97,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.5" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +checksum = "4cd2405b3ac1faab2990b74d728624cd9fd115651fcecc7c2d8daf01376275ba" dependencies = [ "anstyle", "anstyle-parse", @@ -225,7 +225,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -236,7 +236,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -306,12 +306,12 @@ dependencies = [ [[package]] name = "axum" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09dbe0e490df5da9d69b36dca48a76635288a82f92eca90024883a56202026d" +checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" dependencies = [ "async-trait", - "axum-core 0.4.2", + "axum-core 0.4.3", "bytes", "futures-util", "http 1.0.0", @@ -344,7 +344,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f5ffe4637708b326c621d5494ab6c91dcf62ee440fa6ee967d289315a9c6f81" dependencies = [ - "axum 0.7.3", + "axum 0.7.4", "forwarded-header-value", "serde", ] @@ -368,9 +368,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e87c8503f93e6d144ee5690907ba22db7ba79ab001a932ab99034f0fe836b3df" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" dependencies = [ "async-trait", "bytes", @@ -389,12 +389,12 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "881348a37b079994894b6e5e46edcc4b8a60e1c0333669a65b810abeed780598" +checksum = "895ff42f72016617773af68fb90da2a9677d89c62338ec09162d4909d86fdd8f" dependencies = [ - "axum 0.7.3", - "axum-core 0.4.2", + "axum 0.7.4", + "axum-core 0.4.3", "bytes", "cookie 0.18.0", "futures-util", @@ -445,9 +445,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.5" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64ct" @@ -461,7 +461,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18b3d30abb74120a9d5267463b9e0045fdccc4dd152e7249d966612dc1721384" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "serde", "serde_json", ] @@ -640,9 +640,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.12" +version = "4.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" +checksum = "80932e03c33999b9235edb8655bc9df3204adc9887c2f95b50cb1deb9fd54253" dependencies = [ "clap_builder", "clap_derive", @@ -650,9 +650,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.12" +version = "4.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" +checksum = "d6c0db58c659eef1c73e444d298c27322a1b52f6927d2ad470c0c0f96fa7b8fa" dependencies = [ "anstream", "anstyle", @@ -669,7 +669,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -764,7 +764,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8" dependencies = [ "aes-gcm", - "base64 0.21.5", + "base64 0.21.7", "percent-encoding", "rand", "subtle", @@ -807,9 +807,9 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -840,44 +840,37 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.17" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-queue" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc6598521bb5a83d491e8c1fe51db7296019d2ca3cb93cc6c2a20369a4d78a2" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.18" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crunchy" @@ -942,7 +935,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -966,7 +959,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -977,7 +970,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -992,10 +985,10 @@ version = "0.8.0" dependencies = [ "anyhow", "argon2", - "axum 0.7.3", + "axum 0.7.4", "axum-client-ip", "axum-extra", - "base64 0.21.5", + "base64 0.21.7", "bincode", "bytes", "chrono", @@ -1125,7 +1118,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -1223,7 +1216,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "memchr", ] @@ -1319,9 +1312,9 @@ dependencies = [ [[package]] name = "ethers-core" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f03e0bdc216eeb9e355b90cf610ef6c5bb8aca631f97b5ae9980ce34ea7878d" +checksum = "918b1a9ba585ea61022647def2f27c29ba19f6d2a4a4c8f68a9ae97fd5769737" dependencies = [ "arrayvec", "bytes", @@ -1530,7 +1523,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -1585,9 +1578,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "js-sys", @@ -1649,9 +1642,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +checksum = "b553656127a00601c8ae5590fcfdc118e4083a7924b6cf4ffc1ea4b99dc429d7" dependencies = [ "bytes", "fnv", @@ -1668,9 +1661,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a" +checksum = "991910e35c615d8cab86b5ab04be67e6ad24d2bf5f4f11fdbbed26da999bbeab" dependencies = [ "bytes", "fnv", @@ -1722,7 +1715,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "bytes", "headers-core", "http 1.0.0", @@ -1898,7 +1891,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.22", + "h2 0.3.23", "http 0.2.11", "http-body 0.4.6", "httparse", @@ -1921,7 +1914,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.0", + "h2 0.4.1", "http 1.0.0", "http-body 1.0.0", "httparse", @@ -2050,9 +2043,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" dependencies = [ "crossbeam-deque", "globset", @@ -2183,9 +2176,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] @@ -2196,7 +2189,7 @@ version = "9.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c7ea04a7c5c055c175f189b6dc6ba036fd62306b58c66c9f6389036c503a3f4" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "js-sys", "pem", "ring 0.17.7", @@ -2207,9 +2200,9 @@ dependencies = [ [[package]] name = "k256" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f01b677d82ef7a676aa37e099defd83a28e15687112cafdd112d60236b6115b" +checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" dependencies = [ "cfg-if", "ecdsa", @@ -2220,9 +2213,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ "cpufeatures", ] @@ -2272,12 +2265,12 @@ dependencies = [ [[package]] name = "lettre" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a48c2e9831b370bc2d7233c2620298c45f3a158ed6b4b8d7416b2ada5a268fd8" +checksum = "f5aaf628956b6b0852e12ac3505d20d7a12ecc1e32d5ea921f002af4a74036a5" dependencies = [ "async-trait", - "base64 0.21.5", + "base64 0.21.7", "chumsky", "email-encoding", "email_address", @@ -2290,7 +2283,6 @@ dependencies = [ "mime", "native-tls", "nom", - "once_cell", "quoted_printable", "socket2", "tokio", @@ -2300,9 +2292,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.151" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libm" @@ -2448,7 +2440,7 @@ name = "model_derive" version = "0.1.2" dependencies = [ "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -2583,23 +2575,23 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683751d591e6d81200c39fb0d1032608b77724f34114db54f571ff1317b337c0" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c11e44798ad209ccdd91fc192f0526a369a01234f7373e1b141c96d7cee4f0e" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ - "proc-macro-crate 2.0.1", + "proc-macro-crate 3.0.0", "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -2731,7 +2723,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -2832,7 +2824,7 @@ version = "3.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be30eaf4b0a9fba5336683b38de57bb86d179a35862ba6bfcf57625d006bde5b" dependencies = [ - "proc-macro-crate 2.0.1", + "proc-macro-crate 2.0.0", "proc-macro2", "quote", "syn 1.0.109", @@ -2893,7 +2885,7 @@ version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "serde", ] @@ -2914,9 +2906,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" +checksum = "1f200d8d83c44a45b21764d1916299752ca035d15ecd46faca3e9a2a2bf6ad06" dependencies = [ "memchr", "thiserror", @@ -2925,9 +2917,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" +checksum = "bcd6ab1236bbdb3a49027e920e693192ebfe8913f6d60e294de57463a493cfde" dependencies = [ "pest", "pest_generator", @@ -2935,22 +2927,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" +checksum = "2a31940305ffc96863a735bef7c7994a00b325a7138fdbc5bda0f1a0476d3275" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] name = "pest_meta" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" +checksum = "a7ff62f5259e53b78d1af898941cdcdccfae7385cf7d793a6e55de5d05bb4b7d" dependencies = [ "once_cell", "pest", @@ -3022,7 +3014,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -3101,7 +3093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -3139,12 +3131,20 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml_datetime", - "toml_edit 0.20.2", + "toml_edit 0.20.7", +] + +[[package]] +name = "proc-macro-crate" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b2685dd208a3771337d8d386a89840f0f43cd68be8dae90a5f8c2384effc9cd" +dependencies = [ + "toml_edit 0.21.0", ] [[package]] @@ -3173,9 +3173,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.74" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2de98502f212cfcea8d0bb305bd0f49d7ebdd75b64ba0a68f937d888f4e0d6db" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] @@ -3223,7 +3223,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.46", + "syn 2.0.48", "tempfile", "which", ] @@ -3238,7 +3238,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -3406,14 +3406,14 @@ version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "bytes", "cookie 0.16.2", "cookie_store", "encoding_rs", "futures-core", "futures-util", - "h2 0.3.22", + "h2 0.3.23", "http 0.2.11", "http-body 0.4.6", "hyper 0.14.28", @@ -3581,9 +3581,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.28" +version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ "bitflags 2.4.1", "errno", @@ -3622,7 +3622,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", ] [[package]] @@ -3721,9 +3721,9 @@ dependencies = [ [[package]] name = "secp256k1" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acea373acb8c21ecb5a23741452acd2593ed44ee3d343e72baaa143bc89d0d5" +checksum = "3f622567e3b4b38154fb8190bcf6b160d7a4301d70595a49195b48c116007a27" dependencies = [ "rand", "secp256k1-sys", @@ -3731,9 +3731,9 @@ dependencies = [ [[package]] name = "secp256k1-sys" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd97a086ec737e30053fd5c46f097465d25bb81dd3608825f65298c4c98be83" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" dependencies = [ "cc", ] @@ -3773,15 +3773,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] @@ -3808,20 +3808,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] name = "serde_json" -version = "1.0.109" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", "ryu", @@ -3830,9 +3830,9 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" dependencies = [ "itoa", "serde", @@ -3876,7 +3876,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "chrono", "hex", "indexmap 1.9.3", @@ -3896,7 +3896,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -4012,9 +4012,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.2" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "2593d31f82ead8df961d8bd23a64c2ccf2eb5dd34b0a34bfb4dd54011c72009e" [[package]] name = "socket2" @@ -4166,7 +4166,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" dependencies = [ "atoi", - "base64 0.21.5", + "base64 0.21.7", "bitflags 2.4.1", "byteorder", "bytes", @@ -4210,7 +4210,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" dependencies = [ "atoi", - "base64 0.21.5", + "base64 0.21.7", "bitflags 2.4.1", "byteorder", "chrono", @@ -4323,7 +4323,7 @@ checksum = "f14a349c27ebe59faba22f933c9c734d428da7231e88a247e9d8c61eea964ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -4345,7 +4345,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -4367,9 +4367,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.46" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89456b690ff72fddcecf231caedbe615c59480c93358a93dfae7fc29e3ebbf0e" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -4473,7 +4473,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -4575,7 +4575,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -4625,9 +4625,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" [[package]] name = "toml_edit" @@ -4642,9 +4642,20 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.20.2" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap 2.1.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" dependencies = [ "indexmap 2.1.0", "toml_datetime", @@ -4660,10 +4671,10 @@ dependencies = [ "async-stream", "async-trait", "axum 0.6.20", - "base64 0.21.5", + "base64 0.21.7", "bytes", "flate2", - "h2 0.3.22", + "h2 0.3.23", "http 0.2.11", "http-body 0.4.6", "hyper 0.14.28", @@ -4693,7 +4704,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -4718,9 +4729,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e12e6351354851911bdf8c2b8f2ab15050c567d70a8b9a37ae7b8301a4080d" +checksum = "0da193277a4e2c33e59e09b5861580c33dd0a637c3883d0fa74ba40c0374af2e" dependencies = [ "bitflags 2.4.1", "bytes", @@ -4773,7 +4784,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -5070,9 +5081,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -5080,24 +5091,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.39" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" dependencies = [ "cfg-if", "js-sys", @@ -5107,9 +5118,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5117,22 +5128,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "wasm-streams" @@ -5149,9 +5160,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" dependencies = [ "js-sys", "wasm-bindgen", @@ -5424,9 +5435,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.31" +version = "0.5.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a4882e6b134d6c28953a387571f1acdd3496830d5e36c5e3a1075580ea641c" +checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" dependencies = [ "memchr", ] @@ -5506,7 +5517,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -5526,5 +5537,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] diff --git a/Dockerfile b/Dockerfile index 574a987c0..cf8e819f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.74 as chef +FROM rust:1.75 as chef WORKDIR /build diff --git a/build.rs b/build.rs index 51800307d..1fda0abc2 100644 --- a/build.rs +++ b/build.rs @@ -5,19 +5,12 @@ fn main() -> Result<(), Box> { config, &[ "proto/core/auth.proto", + "proto/core/proxy.proto", "proto/core/vpn.proto", "proto/worker/worker.proto", "proto/wireguard/gateway.proto", - "proto/enrollment/enrollment.proto", - "proto/password_reset/password_reset.proto", - ], - &[ - "proto/core", - "proto/worker", - "proto/wireguard", - "proto/enrollment", - "proto/password_reset", ], + &["proto/core", "proto/worker", "proto/wireguard"], )?; println!("cargo:rerun-if-changed=proto"); println!("cargo:rerun-if-changed=migrations"); diff --git a/proto b/proto index 9f5c90266..3294ebd57 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 9f5c90266c9d3449c38197b72e8a9d8a56f12cfd +Subproject commit 3294ebd5748419ca604afbd6869f305ef7879e1c diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 71326c3d7..6d833ff50 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.74" +channel = "1.75" diff --git a/src/appstate.rs b/src/appstate.rs index feba4d36d..a2632a749 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -84,7 +84,7 @@ impl AppState { /// Sends given `GatewayEvent` to be handled by gateway GRPC server pub fn send_wireguard_event(&self, event: GatewayEvent) { if let Err(err) = self.wireguard_tx.send(event) { - error!("Error sending wireguard event {err}"); + error!("Error sending WireGuard event {err}"); } } diff --git a/src/auth/failed_login.rs b/src/auth/failed_login.rs index ee11ed4ae..c926b815c 100644 --- a/src/auth/failed_login.rs +++ b/src/auth/failed_login.rs @@ -1,7 +1,4 @@ -use std::{ - collections::HashMap, - sync::{Arc, Mutex}, -}; +use std::{collections::HashMap, sync::Mutex}; use chrono::{DateTime, Duration, Local}; use thiserror::Error; @@ -119,7 +116,7 @@ impl FailedLoginMap { // Check if auth request with a given username can proceed pub fn check_username( - failed_logins: &Arc>, + failed_logins: &Mutex, username: &str, ) -> Result<(), FailedLoginError> { let mut failed_logins = failed_logins @@ -129,7 +126,7 @@ pub fn check_username( } // Helper to log failed login attempt -pub fn log_failed_login_attempt(failed_logins: &Arc>, username: &str) { +pub fn log_failed_login_attempt(failed_logins: &Mutex, username: &str) { let mut failed_logins = failed_logins .lock() .expect("Failed to get a lock on failed login map."); diff --git a/src/bin/defguard.rs b/src/bin/defguard.rs index 59beccd03..75e6e5b36 100644 --- a/src/bin/defguard.rs +++ b/src/bin/defguard.rs @@ -11,7 +11,7 @@ use defguard::{ auth::failed_login::FailedLoginMap, config::{Command, DefGuardConfig}, db::{init_db, AppEvent, GatewayEvent, Settings, User}, - grpc::{run_grpc_server, GatewayMap, WorkerState}, + grpc::{run_grpc_bidi_stream, run_grpc_server, GatewayMap, WorkerState}, headers::create_user_agent_parser, init_dev_env, init_vpn_location, mail::{run_mail_handler, Mail}, @@ -107,11 +107,12 @@ async fn main() -> Result<(), anyhow::Error> { // run services tokio::select! { - _ = run_grpc_server(&config, Arc::clone(&worker_state), pool.clone(), Arc::clone(&gateway_state), wireguard_tx.clone(), mail_tx.clone(), grpc_cert, grpc_key, user_agent_parser.clone(), failed_logins.clone()) => (), - _ = run_web_server(&config, worker_state, gateway_state, webhook_tx, webhook_rx, wireguard_tx.clone(), mail_tx, pool.clone(), user_agent_parser, failed_logins) => (), - () = run_mail_handler(mail_rx, pool.clone()) => (), - _ = run_periodic_peer_disconnect(pool.clone(), wireguard_tx) => (), - _ = run_periodic_stats_purge(pool, config.stats_purge_frequency.into(), config.stats_purge_threshold.into()), if !config.disable_stats_purge => (), + res = run_grpc_bidi_stream(pool.clone(), wireguard_tx.clone(), mail_tx.clone(), user_agent_parser.clone()), if config.proxy_url.is_some() => error!("Proxy gRPC stream returned early: {res:#?}"), + res = run_grpc_server(&config, Arc::clone(&worker_state), pool.clone(), Arc::clone(&gateway_state), wireguard_tx.clone(), mail_tx.clone(), grpc_cert, grpc_key, failed_logins.clone()) => error!("gRPC server returned early: {res:#?}"), + res = run_web_server(&config, worker_state, gateway_state, webhook_tx, webhook_rx, wireguard_tx.clone(), mail_tx, pool.clone(), user_agent_parser, failed_logins) => error!("Web server returned early: {res:#?}"), + res = run_mail_handler(mail_rx, pool.clone()) => error!("Mail handler returned early: {res:#?}"), + res = run_periodic_peer_disconnect(pool.clone(), wireguard_tx) => error!("Periodic peer disconnect task returned early: {res:#?}"), + res = run_periodic_stats_purge(pool, config.stats_purge_frequency.into(), config.stats_purge_threshold.into()), if !config.disable_stats_purge => error!("Periodic stats purge task returned early: {res:#?}"), } Ok(()) } diff --git a/src/config.rs b/src/config.rs index 118564fde..d46a416ec 100644 --- a/src/config.rs +++ b/src/config.rs @@ -137,6 +137,14 @@ pub struct DefGuardConfig { #[arg(long, env = "DEFGUARD_COOKIE_INSECURE")] pub cookie_insecure: bool, + // TODO: allow multiple values + #[arg(long, env = "DEFGUARD_PROXY_URL")] + pub proxy_url: Option, + + // path to certificate `.pem` file used if connecting to proxy over HTTPS + #[arg(long, env = "DEFGUARD_PROXY_GRPC_CA")] + pub proxy_grpc_ca: Option, + #[arg( long, env = "DEFGUARD_GATEWAY_DISCONNECTION_NOTIFICATION_TIMEOUT", diff --git a/src/db/models/enrollment.rs b/src/db/models/enrollment.rs index 165b54769..2625a9ff3 100644 --- a/src/db/models/enrollment.rs +++ b/src/db/models/enrollment.rs @@ -52,7 +52,7 @@ pub enum TokenError { impl From for Status { fn from(err: TokenError) -> Self { - error!("{}", err); + error!("{err}"); let (code, msg) = match err { TokenError::DbError(_) | TokenError::AdminNotFound diff --git a/src/grpc/auth.rs b/src/grpc/auth.rs index 977da2a59..992fce4d3 100644 --- a/src/grpc/auth.rs +++ b/src/grpc/auth.rs @@ -1,3 +1,8 @@ +use std::sync::{Arc, Mutex}; + +use jsonwebtoken::errors::Error as JWTError; +use tonic::{Request, Response, Status}; + use crate::{ auth::{ failed_login::{check_username, log_failed_login_attempt, FailedLoginMap}, @@ -5,9 +10,6 @@ use crate::{ }, db::{DbPool, User}, }; -use jsonwebtoken::errors::Error as JWTError; -use std::sync::{Arc, Mutex}; -use tonic::{Request, Response, Status}; tonic::include_proto!("auth"); diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 075c6e2f9..e0a568b96 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -1,7 +1,6 @@ use std::sync::Arc; use crate::{ - config::DefGuardConfig, db::{ models::{ device::{DeviceConfig, DeviceInfo, WireguardNetworkDevice}, @@ -21,25 +20,20 @@ use ipnetwork::IpNetwork; use reqwest::Url; use sqlx::Transaction; use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender}; -use tonic::{Request, Response, Status}; +use tonic::Status; use uaparser::UserAgentParser; -pub mod proto { - tonic::include_proto!("enrollment"); -} - -use self::proto::{ - enrollment_service_server, ActivateUserRequest, AdminInfo, Device as ProtoDevice, - DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse, EnrollmentStartRequest, - EnrollmentStartResponse, ExistingDevice, InitialUserInfo, NewDevice, +use super::proto::{ + ActivateUserRequest, AdminInfo, Device as ProtoDevice, DeviceConfig as ProtoDeviceConfig, + DeviceConfigResponse, EnrollmentStartRequest, EnrollmentStartResponse, ExistingDevice, + InitialUserInfo, NewDevice, }; -pub struct EnrollmentServer { +pub(super) struct EnrollmentServer { pool: DbPool, wireguard_tx: Sender, mail_tx: UnboundedSender, user_agent_parser: Arc, - config: DefGuardConfig, ldap_feature_active: bool, } @@ -64,7 +58,7 @@ impl InstanceInfo { } } -impl From for proto::InstanceInfo { +impl From for super::proto::InstanceInfo { fn from(instance: InstanceInfo) -> Self { Self { name: instance.name, @@ -83,7 +77,6 @@ impl EnrollmentServer { wireguard_tx: Sender, mail_tx: UnboundedSender, user_agent_parser: Arc, - config: DefGuardConfig, ) -> Self { // FIXME: check if LDAP feature is enabled let ldap_feature_active = true; @@ -92,29 +85,22 @@ impl EnrollmentServer { wireguard_tx, mail_tx, user_agent_parser, - config, ldap_feature_active, } } // check if token provided with request corresponds to a valid enrollment session - async fn validate_session( - &self, - request: &Request, - ) -> Result { - debug!("Validating enrollment session token: {request:?}"); - let token = if let Some(token) = request.metadata().get("authorization") { - token - .to_str() - .map_err(|_| Status::unauthenticated("Invalid token"))? - } else { + async fn validate_session(&self, token: Option<&str>) -> Result { + let Some(token) = token else { error!("Missing authorization header in request"); return Err(Status::unauthenticated("Missing authorization header")); }; + debug!("Validating enrollment session token: {token}"); let enrollment = Token::find_by_id(&self.pool, token).await?; + let config = SERVER_CONFIG.get().expect("defguard config not found"); - if enrollment.is_session_valid(self.config.enrollment_session_timeout.as_secs()) { + if enrollment.is_session_valid(config.enrollment_session_timeout.as_secs()) { Ok(enrollment) } else { error!("Enrollment session expired"); @@ -128,20 +114,16 @@ impl EnrollmentServer { error!("Error sending WireGuard event {err}"); } } -} -#[tonic::async_trait] -impl enrollment_service_server::EnrollmentService for EnrollmentServer { - async fn start_enrollment( + pub async fn start_enrollment( &self, - request: Request, - ) -> Result, Status> { - debug!("Starting enrollment session: {request:?}"); - let request = request.into_inner(); + request: EnrollmentStartRequest, + ) -> Result { + let config = SERVER_CONFIG.get().expect("defguard config not found"); // fetch enrollment token let mut enrollment = Token::find_by_id(&self.pool, &request.token).await?; - if let Some(token_type) = enrollment.clone().token_type { + if let Some(token_type) = &enrollment.token_type { if token_type != ENROLLMENT_TOKEN_TYPE { return Err(Status::permission_denied("invalid token")); } @@ -160,7 +142,7 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { let session_deadline = enrollment .start_session( &mut transaction, - self.config.enrollment_session_timeout.as_secs(), + config.enrollment_session_timeout.as_secs(), ) .await?; @@ -183,7 +165,7 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { let admin_info = admin.map(AdminInfo::from); - let response = EnrollmentStartResponse { + let response = super::proto::EnrollmentStartResponse { admin: admin_info, user: Some(user_info), deadline_timestamp: session_deadline.timestamp(), @@ -199,35 +181,32 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { Status::internal("unexpected error") })?; - Ok(Response::new(response)) + Ok(response) } else { - return Err(Status::permission_denied("invalid token")); + Err(Status::permission_denied("invalid token")) } } - async fn activate_user( + pub async fn activate_user( &self, - request: Request, - ) -> Result, Status> { + request: ActivateUserRequest, + req_device_info: Option, + ) -> Result<(), Status> { debug!("Activating user account: {request:?}"); - let enrollment = self.validate_session(&request).await?; - - let ip_address = request - .metadata() - .get("ip_address") - .and_then(|value| value.to_str().map(ToString::to_string).ok()) - .unwrap_or_default(); - - let user_agent = request - .metadata() - .get("user_agent") - .and_then(|value| value.to_str().map(ToString::to_string).ok()) - .unwrap_or_default(); - - let device_info = get_device_info(&self.user_agent_parser, &user_agent); + let enrollment = self.validate_session(request.token.as_deref()).await?; + + let ip_address; + let device_info; + if let Some(info) = req_device_info { + ip_address = info.ip_address.unwrap_or_default(); + let user_agent = info.user_agent.unwrap_or_default(); + device_info = get_device_info(&self.user_agent_parser, &user_agent); + } else { + ip_address = String::new(); + device_info = None; + } // check if password is strong enough - let request = request.into_inner(); if let Err(err) = check_password_strength(&request.password) { error!("Password not strong enough: {err}"); return Err(Status::invalid_argument("password not strong enough")); @@ -296,15 +275,17 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { Status::internal("unexpected error") })?; - Ok(Response::new(())) + Ok(()) } - async fn create_device( + pub async fn create_device( &self, - request: Request, - ) -> Result, Status> { + request: NewDevice, + req_device_info: Option, + ) -> Result { + let config = SERVER_CONFIG.get().expect("defguard config not found"); debug!("Adding new user device: {request:?}"); - let enrollment = self.validate_session(&request).await?; + let enrollment = self.validate_session(request.token.as_deref()).await?; // fetch related users let user = enrollment.fetch_user(&self.pool).await?; @@ -312,21 +293,17 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { // add device info!("Adding new device for user {}", user.username); - let ip_address = request - .metadata() - .get("ip_address") - .and_then(|value| value.to_str().map(ToString::to_string).ok()) - .unwrap_or_default(); - - let user_agent = request - .metadata() - .get("user_agent") - .and_then(|value| value.to_str().map(ToString::to_string).ok()) - .unwrap_or_default(); - - let device_info = get_device_info(&self.user_agent_parser, &user_agent); + let ip_address; + let device_info; + if let Some(info) = req_device_info { + ip_address = info.ip_address.unwrap_or_default(); + let user_agent = info.user_agent.unwrap_or_default(); + device_info = get_device_info(&self.user_agent_parser, &user_agent); + } else { + ip_address = String::new(); + device_info = None; + } - let request = request.into_inner(); Device::validate_pubkey(&request.pubkey).map_err(|_| { error!("Invalid pubkey {}", request.pubkey); Status::invalid_argument("invalid pubkey") @@ -343,7 +320,7 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { })?; let (network_info, configs) = device - .add_to_all_networks(&mut transaction, &self.config.admin_groupname) + .add_to_all_networks(&mut transaction, &config.admin_groupname) .await .map_err(|err| { error!( @@ -394,21 +371,20 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { instance: Some(InstanceInfo::new(settings, &user.username).into()), }; - Ok(Response::new(response)) + Ok(response) } /// Get all information needed /// to update instance information for desktop client - async fn get_network_info( + pub async fn get_network_info( &self, - request: Request, - ) -> Result, Status> { - let enrollment = self.validate_session(&request).await?; + request: ExistingDevice, + ) -> Result { + let enrollment = self.validate_session(request.token.as_deref()).await?; // get enrollment user let user = enrollment.fetch_user(&self.pool).await?; - let request = request.into_inner(); Device::validate_pubkey(&request.pubkey).map_err(|_| { error!("Invalid pubkey {}", request.pubkey); Status::invalid_argument("invalid pubkey") @@ -473,7 +449,7 @@ impl enrollment_service_server::EnrollmentService for EnrollmentServer { instance: Some(InstanceInfo::new(settings, &user.username).into()), }; - Ok(Response::new(response)) + Ok(response) } else { Err(Status::internal("device not found error")) } diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index 2b8114de1..c4006a333 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -1,5 +1,6 @@ use std::{ collections::hash_map::HashMap, + fs::read_to_string, time::{Duration, Instant}, }; #[cfg(any(feature = "wireguard", feature = "worker"))] @@ -11,8 +12,16 @@ use std::{ use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc}; use serde::Serialize; use thiserror::Error; -use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender}; -use tonic::transport::{Identity, Server, ServerTlsConfig}; +use tokio::{ + sync::{ + broadcast::Sender, + mpsc::{self, UnboundedSender}, + }, + time::sleep, +}; +use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; +use tonic::transport::{Certificate, ClientTlsConfig, Endpoint, Identity, Server, ServerTlsConfig}; +use tonic::Status; use uaparser::UserAgentParser; use uuid::Uuid; @@ -20,7 +29,9 @@ use uuid::Uuid; use self::gateway::{gateway_service_server::GatewayServiceServer, GatewayServer}; use self::{ auth::{auth_service_server::AuthServiceServer, AuthServer}, - enrollment::{proto::enrollment_service_server::EnrollmentServiceServer, EnrollmentServer}, + enrollment::EnrollmentServer, + password_reset::PasswordResetServer, + proto::core_response, }; #[cfg(feature = "worker")] use self::{ @@ -28,15 +39,8 @@ use self::{ worker::{worker_service_server::WorkerServiceServer, WorkerServer}, }; use crate::{ - auth::failed_login::FailedLoginMap, - config::DefGuardConfig, - db::AppEvent, - grpc::password_reset::{ - proto::password_reset_service_server::PasswordResetServiceServer, PasswordResetServer, - }, - handlers::mail::send_gateway_disconnected_email, - mail::Mail, - SERVER_CONFIG, + auth::failed_login::FailedLoginMap, config::DefGuardConfig, db::AppEvent, + handlers::mail::send_gateway_disconnected_email, mail::Mail, SERVER_CONFIG, }; #[cfg(feature = "worker")] use crate::{ @@ -54,6 +58,13 @@ pub mod password_reset; #[cfg(feature = "worker")] pub mod worker; +pub(crate) mod proto { + tonic::include_proto!("defguard.proxy"); +} + +use crate::grpc::proto::CoreError; +use proto::{core_request, proxy_client::ProxyClient, CoreResponse}; + // Helper struct used to handle gateway state // gateways are grouped by network type NetworkId = i64; @@ -200,6 +211,7 @@ impl GatewayMap { None => Vec::new(), } } + // return gateway name #[must_use] pub fn get_network_gateway_name(&self, network_id: i64, hostname: &str) -> Option { @@ -305,6 +317,166 @@ impl GatewayState { } } +const TEN_SECS: Duration = Duration::from_secs(10); + +impl From for CoreError { + fn from(status: Status) -> Self { + Self { + status_code: status.code().into(), + message: status.message().into(), + } + } +} + +/// Bi-directional gRPC stream for comminication with Defguard proxy. +pub async fn run_grpc_bidi_stream( + pool: DbPool, + wireguard_tx: Sender, + mail_tx: UnboundedSender, + user_agent_parser: Arc, +) -> Result<(), anyhow::Error> { + let config = SERVER_CONFIG.get().unwrap(); + + // TODO: merge the two + let enrollment_server = EnrollmentServer::new( + pool.clone(), + wireguard_tx.clone(), + mail_tx.clone(), + user_agent_parser, + ); + let password_reset_server = PasswordResetServer::new(pool, mail_tx); + + let endpoint = Endpoint::from_shared(config.proxy_url.as_deref().unwrap())?; + let endpoint = endpoint.http2_keep_alive_interval(TEN_SECS); + let endpoint = endpoint.tcp_keepalive(Some(TEN_SECS)); + let endpoint = if let Some(ca) = &config.proxy_grpc_ca { + let ca = read_to_string(ca)?; + let tls = ClientTlsConfig::new().ca_certificate(Certificate::from_pem(ca)); + endpoint.tls_config(tls)? + } else { + endpoint + }; + + loop { + info!("Connecting to proxy"); + let mut client = ProxyClient::new(endpoint.connect_lazy()); + let (tx, rx) = mpsc::unbounded_channel(); + let Ok(response) = client.bidi(UnboundedReceiverStream::new(rx)).await else { + info!("Failed to connect to proxy, retrying in 10s"); + sleep(TEN_SECS).await; + continue; + }; + let mut resp_stream = response.into_inner(); + while let Some(received) = resp_stream.next().await { + info!("received message"); + match received { + Ok(received) => { + let payload = match received.payload { + // rpc StartEnrollment (EnrollmentStartRequest) returns (EnrollmentStartResponse) + Some(core_request::Payload::EnrollmentStart(request)) => { + match enrollment_server.start_enrollment(request).await { + Ok(response_payload) => { + Some(core_response::Payload::EnrollmentStart(response_payload)) + } + Err(err) => { + error!("start enrollment error {err}"); + Some(core_response::Payload::CoreError(err.into())) + } + } + } + // rpc ActivateUser (ActivateUserRequest) returns (google.protobuf.Empty) + Some(core_request::Payload::ActivateUser(request)) => { + match enrollment_server + .activate_user(request, received.device_info) + .await + { + Ok(()) => Some(core_response::Payload::Empty(())), + Err(err) => { + error!("activate user error {err}"); + Some(core_response::Payload::CoreError(err.into())) + } + } + } + // rpc CreateDevice (NewDevice) returns (DeviceConfigResponse) + Some(core_request::Payload::NewDevice(request)) => { + match enrollment_server + .create_device(request, received.device_info) + .await + { + Ok(response_payload) => { + Some(core_response::Payload::DeviceConfig(response_payload)) + } + Err(err) => { + error!("create device error {err}"); + Some(core_response::Payload::CoreError(err.into())) + } + } + } + // rpc GetNetworkInfo (ExistingDevice) returns (DeviceConfigResponse) + Some(core_request::Payload::ExistingDevice(request)) => { + match enrollment_server.get_network_info(request).await { + Ok(response_payload) => { + Some(core_response::Payload::DeviceConfig(response_payload)) + } + Err(err) => { + error!("get network info error {err}"); + Some(core_response::Payload::CoreError(err.into())) + } + } + } + // rpc RequestPasswordReset (PasswordResetInitializeRequest) returns (google.protobuf.Empty) + Some(core_request::Payload::PasswordResetInit(request)) => { + match password_reset_server + .request_password_reset(request, received.device_info) + .await + { + Ok(()) => Some(core_response::Payload::Empty(())), + Err(err) => { + error!("password reset init error {err}"); + Some(core_response::Payload::CoreError(err.into())) + } + } + } + // rpc StartPasswordReset (PasswordResetStartRequest) returns (PasswordResetStartResponse) + Some(core_request::Payload::PasswordResetStart(request)) => { + match password_reset_server.start_password_reset(request).await { + Ok(response_payload) => Some( + core_response::Payload::PasswordResetStart(response_payload), + ), + Err(err) => { + error!("password reset start error {err}"); + Some(core_response::Payload::CoreError(err.into())) + } + } + } + // rpc ResetPassword (PasswordResetRequest) returns (google.protobuf.Empty) + Some(core_request::Payload::PasswordReset(request)) => { + match password_reset_server + .reset_password(request, received.device_info) + .await + { + Ok(()) => Some(core_response::Payload::Empty(())), + Err(err) => { + error!("password reset error {err}"); + Some(core_response::Payload::CoreError(err.into())) + } + } + } + // Reply without payload. + None => None, + }; + let req = CoreResponse { + id: received.id, + payload, + }; + tx.send(req).unwrap(); + } + Err(err) => error!("stream error {err}"), + } + } + } +} + /// Runs gRPC server with core services. pub async fn run_grpc_server( config: &DefGuardConfig, @@ -315,23 +487,10 @@ pub async fn run_grpc_server( mail_tx: UnboundedSender, grpc_cert: Option, grpc_key: Option, - user_agent_parser: Arc, failed_logins: Arc>, ) -> Result<(), anyhow::Error> { // Build gRPC services let auth_service = AuthServiceServer::new(AuthServer::new(pool.clone(), failed_logins)); - let enrollment_service = EnrollmentServiceServer::new(EnrollmentServer::new( - pool.clone(), - wireguard_tx.clone(), - mail_tx.clone(), - user_agent_parser, - config.clone(), - )); - let password_reset_service = PasswordResetServiceServer::new(PasswordResetServer::new( - pool.clone(), - mail_tx.clone(), - config.clone(), - )); #[cfg(feature = "worker")] let worker_service = WorkerServiceServer::with_interceptor( WorkerServer::new(pool.clone(), worker_state), @@ -344,7 +503,7 @@ pub async fn run_grpc_server( ); // Run gRPC server let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), config.grpc_port); - info!("Started gRPC services"); + info!("Starting gRPC services"); let builder = if let (Some(cert), Some(key)) = (grpc_cert, grpc_key) { let identity = Identity::from_pem(cert, key); Server::builder().tls_config(ServerTlsConfig::new().identity(identity))? @@ -354,8 +513,6 @@ pub async fn run_grpc_server( let builder = builder.http2_keepalive_interval(Some(Duration::from_secs(10))); let mut builder = builder.tcp_keepalive(Some(Duration::from_secs(10))); let router = builder.add_service(auth_service); - let router = router.add_service(enrollment_service); - let router = router.add_service(password_reset_service); #[cfg(feature = "wireguard")] let router = router.add_service(gateway_service); #[cfg(feature = "worker")] diff --git a/src/grpc/password_reset.rs b/src/grpc/password_reset.rs index fcf2e9573..2956d4335 100644 --- a/src/grpc/password_reset.rs +++ b/src/grpc/password_reset.rs @@ -1,8 +1,7 @@ use tokio::sync::mpsc::UnboundedSender; -use tonic::{Request, Response, Status}; +use tonic::Status; use crate::{ - config::DefGuardConfig, db::{ models::enrollment::{Token, PASSWORD_RESET_TOKEN_TYPE}, DbPool, User, @@ -13,84 +12,69 @@ use crate::{ }, ldap::utils::ldap_change_password, mail::Mail, + SERVER_CONFIG, }; -use self::proto::{ - password_reset_service_server, PasswordResetInitializeRequest, PasswordResetRequest, - PasswordResetStartRequest, PasswordResetStartResponse, +use super::proto::{ + PasswordResetInitializeRequest, PasswordResetRequest, PasswordResetStartRequest, + PasswordResetStartResponse, }; -pub mod proto { - tonic::include_proto!("password_reset"); -} - -pub struct PasswordResetServer { +pub(super) struct PasswordResetServer { pool: DbPool, mail_tx: UnboundedSender, - config: DefGuardConfig, - ldap_feature_active: bool, + // ldap_feature_active: bool, } impl PasswordResetServer { #[must_use] - pub fn new(pool: DbPool, mail_tx: UnboundedSender, config: DefGuardConfig) -> Self { + pub fn new(pool: DbPool, mail_tx: UnboundedSender) -> Self { // FIXME: check if LDAP feature is enabled - let ldap_feature_active = true; + // let ldap_feature_active = true; Self { pool, mail_tx, - config, - ldap_feature_active, + // ldap_feature_active, } } // check if token provided with request corresponds to a valid enrollment session - async fn validate_session( - &self, - request: &Request, - ) -> Result { - debug!("Validating enrollment session token: {request:?}"); - let token = if let Some(token) = request.metadata().get("authorization") { - token - .to_str() - .map_err(|_| Status::unauthenticated("Invalid token"))? - } else { + async fn validate_session(&self, token: Option<&str>) -> Result { + let config = SERVER_CONFIG.get().expect("defguard config not found"); + let Some(token) = token else { error!("Missing authorization header in request"); return Err(Status::unauthenticated("Missing authorization header")); }; + debug!("Validating enrollment session token: {token}"); let enrollment = Token::find_by_id(&self.pool, token).await?; - if enrollment.is_session_valid(self.config.enrollment_session_timeout.as_secs()) { + if enrollment.is_session_valid(config.enrollment_session_timeout.as_secs()) { Ok(enrollment) } else { error!("Enrollment session expired"); Err(Status::unauthenticated("Enrollment session expired")) } } -} -#[tonic::async_trait] -impl password_reset_service_server::PasswordResetService for PasswordResetServer { - async fn request_password_reset( + pub async fn request_password_reset( &self, - request: Request, - ) -> Result, Status> { + request: PasswordResetInitializeRequest, + req_device_info: Option, + ) -> Result<(), Status> { + let config = SERVER_CONFIG.get().expect("defguard config not found"); debug!("Starting password reset request"); - let ip_address = request - .metadata() - .get("ip_address") - .and_then(|value| value.to_str().map(ToString::to_string).ok()) - .unwrap_or_default(); - - let user_agent = request - .metadata() - .get("user_agent") - .and_then(|value| value.to_str().map(ToString::to_string).ok()) - .unwrap_or_default(); + let ip_address; + let user_agent; + if let Some(info) = req_device_info { + ip_address = info.ip_address.unwrap_or_default(); + user_agent = info.user_agent.unwrap_or_default(); + } else { + ip_address = String::new(); + user_agent = String::new(); + } - let request = request.into_inner(); let email = request.email; let user = User::find_by_email(&self.pool, email.to_string().as_str()) @@ -100,16 +84,14 @@ impl password_reset_service_server::PasswordResetService for PasswordResetServer Status::internal("unexpected error") })?; - if user.is_none() { + let Some(user) = user else { // Do not return information whether user exists - return Ok(Response::new(())); - } - - let user = user.unwrap(); + return Ok(()); + }; // Do not allow password change if user is not active if !user.has_password() { - return Ok(Response::new(())); + return Ok(()); } let mut transaction = self.pool.begin().await.map_err(|_| { @@ -127,7 +109,7 @@ impl password_reset_service_server::PasswordResetService for PasswordResetServer user.id.expect("Missing user ID"), None, Some(email.clone()), - self.config.password_reset_token_timeout.as_secs(), + config.password_reset_token_timeout.as_secs(), Some(PASSWORD_RESET_TOKEN_TYPE.to_string()), ); enrollment.save(&mut transaction).await?; @@ -140,21 +122,21 @@ impl password_reset_service_server::PasswordResetService for PasswordResetServer send_password_reset_email( &user, &self.mail_tx, - self.config.enrollment_url.clone(), + config.enrollment_url.clone(), &enrollment.id, Some(&ip_address), Some(&user_agent), )?; - Ok(Response::new(())) + Ok(()) } - async fn start_password_reset( + pub async fn start_password_reset( &self, - request: Request, - ) -> Result, Status> { + request: PasswordResetStartRequest, + ) -> Result { + let config = SERVER_CONFIG.get().expect("defguard config not found"); debug!("Starting password reset session: {request:?}"); - let request = request.into_inner(); let mut enrollment = Token::find_by_id(&self.pool, &request.token).await?; @@ -176,7 +158,7 @@ impl password_reset_service_server::PasswordResetService for PasswordResetServer let session_deadline = enrollment .start_session( &mut transaction, - self.config.password_reset_session_timeout.as_secs(), + config.password_reset_session_timeout.as_secs(), ) .await?; @@ -189,29 +171,27 @@ impl password_reset_service_server::PasswordResetService for PasswordResetServer Status::internal("unexpected error") })?; - Ok(Response::new(response)) + Ok(response) } - async fn reset_password( + pub async fn reset_password( &self, - request: Request, - ) -> Result, Status> { + request: PasswordResetRequest, + req_device_info: Option, + ) -> Result<(), Status> { debug!("Starting password reset: {request:?}"); - let enrollment = self.validate_session(&request).await?; - - let ip_address = request - .metadata() - .get("ip_address") - .and_then(|value| value.to_str().map(ToString::to_string).ok()) - .unwrap_or_default(); + let enrollment = self.validate_session(request.token.as_deref()).await?; - let user_agent = request - .metadata() - .get("user_agent") - .and_then(|value| value.to_str().map(ToString::to_string).ok()) - .unwrap_or_default(); + let ip_address; + let user_agent; + if let Some(info) = req_device_info { + ip_address = info.ip_address.unwrap_or_default(); + user_agent = info.user_agent.unwrap_or_default(); + } else { + ip_address = String::new(); + user_agent = String::new(); + } - let request = request.into_inner(); if let Err(err) = check_password_strength(&request.password) { error!("Password not strong enough: {err}"); return Err(Status::invalid_argument("password not strong enough")); @@ -231,9 +211,9 @@ impl password_reset_service_server::PasswordResetService for PasswordResetServer Status::internal("unexpected error") })?; - if self.ldap_feature_active { - let _ = ldap_change_password(&self.pool, &user.username, &request.password).await; - }; + // if self.ldap_feature_active { + let _ = ldap_change_password(&self.pool, &user.username, &request.password).await; + // }; transaction.commit().await.map_err(|_| { error!("Failed to commit transaction"); @@ -247,6 +227,6 @@ impl password_reset_service_server::PasswordResetService for PasswordResetServer Some(&user_agent), )?; - Ok(Response::new(())) + Ok(()) } } diff --git a/src/headers.rs b/src/headers.rs index eb154bab9..8dc6ace92 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -21,7 +21,7 @@ pub fn create_user_agent_parser() -> Arc { #[must_use] pub fn parse_user_agent<'a>( - user_parser: &'a Arc, + user_parser: &UserAgentParser, user_agent: &'a str, ) -> Option> { if user_agent.is_empty() { @@ -32,10 +32,7 @@ pub fn parse_user_agent<'a>( } #[must_use] -pub fn get_device_info( - user_agent_parser: &Arc, - user_agent: &str, -) -> Option { +pub fn get_device_info(user_agent_parser: &UserAgentParser, user_agent: &str) -> Option { let agent = parse_user_agent(user_agent_parser, user_agent); agent.map(|v| get_user_agent_device(&v)) From 044350076fad67ebcaa4a5d96c3a5f27d31b7f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 16 Jan 2024 09:34:10 +0100 Subject: [PATCH 15/26] ci: update e2e compose config --- docker-compose.e2e.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.e2e.yaml b/docker-compose.e2e.yaml index 739cb5069..311194a1a 100644 --- a/docker-compose.e2e.yaml +++ b/docker-compose.e2e.yaml @@ -20,6 +20,7 @@ services: DEFGUARD_DB_PASSWORD: defguard DEFGUARD_DB_NAME: defguard DEFGUARD_URL: http://localhost:8000 + DEFGUARD_PROXY_URL: http://localhost:50051 RUST_BACKTRACE: 1 ports: # rest api @@ -58,4 +59,4 @@ services: ports: - "8080:8080" environment: - DEFGUARD_PROXY_UPSTREAM_GRPC_URL: "http://core:50055/" + DEFGUARD_PROXY_GRPC_PORT: 50051 From 71f3f4dff5fd524b6f84f4e78b9b79a68440123b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 16 Jan 2024 09:58:53 +0100 Subject: [PATCH 16/26] ci: update e2e compose config --- docker-compose.e2e.yaml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docker-compose.e2e.yaml b/docker-compose.e2e.yaml index 311194a1a..b25da2099 100644 --- a/docker-compose.e2e.yaml +++ b/docker-compose.e2e.yaml @@ -20,12 +20,12 @@ services: DEFGUARD_DB_PASSWORD: defguard DEFGUARD_DB_NAME: defguard DEFGUARD_URL: http://localhost:8000 - DEFGUARD_PROXY_URL: http://localhost:50051 + DEFGUARD_PROXY_URL: http://localhost:50052 RUST_BACKTRACE: 1 ports: - # rest api + # REST API - "8000:8000" - # grpc + # gRPC - "50055:50055" depends_on: - db @@ -57,6 +57,9 @@ services: proxy: image: ghcr.io/defguard/defguard-proxy:current ports: + # REST API - "8080:8080" + # gRPC + - "50052:50052" environment: - DEFGUARD_PROXY_GRPC_PORT: 50051 + DEFGUARD_PROXY_GRPC_PORT: 50052 From 79b3f03b6ba41e5cc74a3929359e0e5017a51def Mon Sep 17 00:00:00 2001 From: Maciek Date: Wed, 17 Jan 2024 12:09:22 +0100 Subject: [PATCH 17/26] feat: desktop client MFA (#502) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * setup mfa service endpoints * add mfa start response * implement login start endpoint logic * store client login sessions * implement login finish handler * handle devices which never connected * fix tests * review fixes * use a JWT for auth * validate TOTP code * update protos --------- Co-authored-by: Maciej Wójcik --- ...073400544f1ce454d059ac13b24b32cbdbf4.json} | 4 +- proto | 2 +- src/auth/mod.rs | 2 + src/db/models/device.rs | 3 +- src/db/models/group.rs | 10 +- src/db/models/wireguard.rs | 33 +-- src/grpc/desktop_client_mfa.rs | 271 ++++++++++++++++++ src/grpc/enrollment.rs | 22 +- src/grpc/mod.rs | 32 ++- src/handlers/auth.rs | 2 +- src/handlers/mail.rs | 2 +- src/handlers/user.rs | 4 +- src/handlers/wireguard.rs | 30 +- src/templates.rs | 4 +- src/wireguard_peer_disconnect.rs | 3 +- tests/wireguard_network_import.rs | 4 +- 16 files changed, 348 insertions(+), 80 deletions(-) rename .sqlx/{query-580741c18880eb98a7073dbb8e1cd907893fedd70f7d752521d515230397f3ee.json => query-92c38d7487d3aa5342fbe4d059cb073400544f1ce454d059ac13b24b32cbdbf4.json} (82%) create mode 100644 src/grpc/desktop_client_mfa.rs diff --git a/.sqlx/query-580741c18880eb98a7073dbb8e1cd907893fedd70f7d752521d515230397f3ee.json b/.sqlx/query-92c38d7487d3aa5342fbe4d059cb073400544f1ce454d059ac13b24b32cbdbf4.json similarity index 82% rename from .sqlx/query-580741c18880eb98a7073dbb8e1cd907893fedd70f7d752521d515230397f3ee.json rename to .sqlx/query-92c38d7487d3aa5342fbe4d059cb073400544f1ce454d059ac13b24b32cbdbf4.json index 8323d4d07..afa7cceb4 100644 --- a/.sqlx/query-580741c18880eb98a7073dbb8e1cd907893fedd70f7d752521d515230397f3ee.json +++ b/.sqlx/query-92c38d7487d3aa5342fbe4d059cb073400544f1ce454d059ac13b24b32cbdbf4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "WITH stats AS ( SELECT DISTINCT ON (device_id) device_id, endpoint, latest_handshake FROM wireguard_peer_stats WHERE network = $1 ORDER BY device_id, collected_at DESC ) SELECT d.id as \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created FROM device d JOIN wireguard_network_device wnd ON wnd.device_id = d.id LEFT JOIN stats on d.id = stats.device_id WHERE wnd.wireguard_network_id = $1 AND wnd.is_authorized = true AND (NOW() - stats.latest_handshake) > $2 * interval '1 second'", + "query": "WITH stats AS ( SELECT DISTINCT ON (device_id) device_id, endpoint, latest_handshake FROM wireguard_peer_stats WHERE network = $1 ORDER BY device_id, collected_at DESC ) SELECT d.id as \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created FROM device d JOIN wireguard_network_device wnd ON wnd.device_id = d.id LEFT JOIN stats on d.id = stats.device_id WHERE wnd.wireguard_network_id = $1 AND wnd.is_authorized = true AND (stats.latest_handshake IS NULL OR (NOW() - stats.latest_handshake) > $2 * interval '1 second')", "describe": { "columns": [ { @@ -43,5 +43,5 @@ false ] }, - "hash": "580741c18880eb98a7073dbb8e1cd907893fedd70f7d752521d515230397f3ee" + "hash": "92c38d7487d3aa5342fbe4d059cb073400544f1ce454d059ac13b24b32cbdbf4" } diff --git a/proto b/proto index 3294ebd57..a5776f47e 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 3294ebd5748419ca604afbd6869f305ef7879e1c +Subproject commit a5776f47e0a9ffb7e408401662f3e5a4ced205d9 diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 107a26799..6fc07679f 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -37,6 +37,7 @@ pub enum ClaimsType { Auth, Gateway, YubiBridge, + DesktopClient, } /// Standard claims: https://www.iana.org/assignments/jwt/jwt.xhtml @@ -85,6 +86,7 @@ impl Claims { ClaimsType::Auth => AUTH_SECRET_ENV, ClaimsType::Gateway => GATEWAY_SECRET_ENV, ClaimsType::YubiBridge => YUBIBRIDGE_SECRET_ENV, + ClaimsType::DesktopClient => AUTH_SECRET_ENV, }; env::var(env_var).unwrap_or_default() } diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 4cdcfa170..69f24228c 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -491,7 +491,6 @@ impl Device { pub async fn add_to_all_networks( &self, transaction: &mut PgConnection, - admin_group_name: &str, ) -> Result<(Vec, Vec), DeviceError> { info!("Adding device {} to all existing networks", self.name); let networks = WireguardNetwork::all(&mut *transaction).await?; @@ -528,7 +527,7 @@ impl Device { } if let Ok(wireguard_network_device) = network - .add_device_to_network(&mut *transaction, self, admin_group_name, None) + .add_device_to_network(&mut *transaction, self, None) .await { debug!( diff --git a/src/db/models/group.rs b/src/db/models/group.rs index 2ee5f91ff..2df043620 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -1,7 +1,10 @@ use model_derive::Model; use sqlx::{query, query_as, query_scalar, Error as SqlxError, PgConnection, PgExecutor}; -use crate::db::{models::error::ModelError, User, WireguardNetwork}; +use crate::{ + db::{models::error::ModelError, User, WireguardNetwork}, + SERVER_CONFIG, +}; #[derive(Model)] pub struct Group { @@ -97,9 +100,12 @@ impl WireguardNetwork { pub async fn get_allowed_groups( &self, transaction: &mut PgConnection, - admin_group_name: &str, ) -> Result>, ModelError> { debug!("Returning a list of allowed groups for network {self}"); + let admin_group_name = &SERVER_CONFIG + .get() + .expect("defguard config not found") + .admin_groupname; // get allowed groups from DB let mut groups = self.fetch_allowed_groups(&mut *transaction).await?; diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index a8107b436..0f24d8262 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -302,11 +302,10 @@ impl WireguardNetwork { async fn get_allowed_devices( &self, transaction: &mut PgConnection, - admin_group_name: &str, ) -> Result, ModelError> { debug!("Fetching all allowed devices for network {}", self); let devices = match self - .get_allowed_groups(&mut *transaction, admin_group_name) + .get_allowed_groups(&mut *transaction) .await? { // devices need to be filtered by allowed group Some(allowed_groups) => { @@ -338,15 +337,12 @@ impl WireguardNetwork { pub async fn add_all_allowed_devices( &self, transaction: &mut PgConnection, - admin_group_name: &str, ) -> Result<(), ModelError> { info!( "Assigning IPs in network {} for all existing devices ", self ); - let devices = self - .get_allowed_devices(&mut *transaction, admin_group_name) - .await?; + let devices = self.get_allowed_devices(&mut *transaction).await?; for device in devices { device .assign_network_ip(&mut *transaction, self, None) @@ -360,13 +356,10 @@ impl WireguardNetwork { &self, transaction: &mut PgConnection, device: &Device, - admin_group_name: &str, reserved_ips: Option<&[IpAddr]>, ) -> Result { info!("Assigning IP in network {self} for {device}"); - let allowed_devices = self - .get_allowed_devices(&mut *transaction, admin_group_name) - .await?; + let allowed_devices = self.get_allowed_devices(&mut *transaction).await?; let allowed_device_ids: Vec = allowed_devices.iter().filter_map(|dev| dev.id).collect(); if allowed_device_ids.contains(&device.get_id()?) { @@ -389,14 +382,11 @@ impl WireguardNetwork { pub async fn sync_allowed_devices( &self, transaction: &mut PgConnection, - admin_group_name: &str, reserved_ips: Option<&[IpAddr]>, ) -> Result, WireguardNetworkError> { info!("Synchronizing IPs in network {self} for all allowed devices "); // list all allowed devices - let allowed_devices = self - .get_allowed_devices(&mut *transaction, admin_group_name) - .await?; + let allowed_devices = self.get_allowed_devices(&mut *transaction).await?; // convert to a map for easier processing let mut allowed_devices: HashMap = allowed_devices .into_iter() @@ -485,12 +475,9 @@ impl WireguardNetwork { &self, transaction: &mut PgConnection, imported_devices: Vec, - admin_group_name: &str, ) -> Result<(Vec, Vec), WireguardNetworkError> { let network_id = self.get_id()?; - let allowed_devices = self - .get_allowed_devices(&mut *transaction, admin_group_name) - .await?; + let allowed_devices = self.get_allowed_devices(&mut *transaction).await?; // convert to a map for easier processing let allowed_devices: HashMap = allowed_devices .into_iter() @@ -551,14 +538,11 @@ impl WireguardNetwork { &self, transaction: &mut PgConnection, mapped_devices: Vec, - admin_group_name: &str, ) -> Result, WireguardNetworkError> { info!("Mapping user devices for network {}", self); let network_id = self.get_id()?; // get allowed groups for network - let allowed_groups = self - .get_allowed_groups(&mut *transaction, admin_group_name) - .await?; + let allowed_groups = self.get_allowed_groups(&mut *transaction).await?; let mut events = Vec::new(); // use a helper hashmap to avoid repeated queries @@ -629,9 +613,8 @@ impl WireguardNetwork { } // assign IPs in other networks - let (mut all_network_info, _configs) = device - .add_to_all_networks(&mut *transaction, admin_group_name) - .await?; + let (mut all_network_info, _configs) = + device.add_to_all_networks(&mut *transaction).await?; network_info.append(&mut all_network_info); diff --git a/src/grpc/desktop_client_mfa.rs b/src/grpc/desktop_client_mfa.rs new file mode 100644 index 000000000..93556e41b --- /dev/null +++ b/src/grpc/desktop_client_mfa.rs @@ -0,0 +1,271 @@ +use super::proto::{ + ClientMfaFinishRequest, ClientMfaFinishResponse, ClientMfaStartRequest, ClientMfaStartResponse, + MfaMethod, +}; +use crate::{ + auth::{Claims, ClaimsType}, + db::{ + models::device::{DeviceInfo, DeviceNetworkInfo, WireguardNetworkDevice}, + DbPool, Device, GatewayEvent, User, UserInfo, WireguardNetwork, + }, + handlers::mail::send_email_mfa_code_email, + mail::Mail, +}; +use std::collections::HashMap; +use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender}; +use tonic::Status; + +const SESSION_TIMEOUT: u64 = 60 * 5; // 10 minutes + +struct ClientLoginSession { + method: MfaMethod, + location: WireguardNetwork, + device: Device, + user: User, +} + +pub(super) struct ClientMfaServer { + pool: DbPool, + mail_tx: UnboundedSender, + wireguard_tx: Sender, + sessions: HashMap, +} + +impl ClientMfaServer { + #[must_use] + pub fn new( + pool: DbPool, + mail_tx: UnboundedSender, + wireguard_tx: Sender, + ) -> Self { + Self { + pool, + mail_tx, + wireguard_tx, + sessions: HashMap::new(), + } + } + fn generate_token(&self, pubkey: &str) -> Result { + Claims::new( + ClaimsType::DesktopClient, + String::new(), + pubkey.into(), + SESSION_TIMEOUT, + ) + .to_jwt() + .map_err(|err| { + error!("Failed to generate JWT token: {err:?}"); + Status::internal("unexpected error") + }) + } + + /// Validate JWT and extract client pubkey + fn parse_token(&self, token: &str) -> Result { + let claims = Claims::from_jwt(ClaimsType::DesktopClient, token).map_err(|err| { + error!("Failed to parse JWT token: {err:?}"); + Status::invalid_argument("invalid token") + })?; + Ok(claims.client_id) + } + + pub async fn start_client_mfa_login( + &mut self, + request: ClientMfaStartRequest, + ) -> Result { + info!("Starting desktop client login: {request:?}"); + // fetch location + let Ok(Some(location)) = + WireguardNetwork::find_by_id(&self.pool, request.location_id).await + else { + error!("Failed to find location with ID {}", request.location_id); + return Err(Status::invalid_argument("location not found")); + }; + + // fetch device + let Ok(Some(device)) = Device::find_by_pubkey(&self.pool, &request.pubkey).await else { + error!("Failed to find device with pubkey {}", request.pubkey); + return Err(Status::invalid_argument("device not found")); + }; + + // fetch user + let Ok(Some(user)) = User::find_by_id(&self.pool, device.user_id).await else { + error!("Failed to find user with ID {}", device.user_id); + return Err(Status::invalid_argument("user not found")); + }; + let user_info = UserInfo::from_user(&self.pool, &user).await.map_err(|_| { + error!("Failed to fetch user info for {}", user.username); + Status::internal("unexpected error") + })?; + + // validate user is allowed to connect to a given location + let mut transaction = self.pool.begin().await.map_err(|_| { + error!("Failed to begin transaction"); + Status::internal("unexpected error") + })?; + let allowed_groups = location + .get_allowed_groups(&mut transaction) + .await + .map_err(|err| { + error!("Failed to fetch allowed groups for location {location}: {err:?}"); + Status::internal("unexpected error") + })?; + if let Some(groups) = allowed_groups { + // check if user belongs to one of allowed groups + if !groups + .iter() + .any(|allowed_group| user_info.groups.contains(allowed_group)) + { + error!( + "User {} not allowed to connect to location {location}", + user.username + ); + return Err(Status::unauthenticated("unauthorized")); + } + } + + // check if selected method is enabled + let method = MfaMethod::try_from(request.method).map_err(|err| { + error!("Invalid MFA method selected: {err}"); + Status::invalid_argument("invalid MFA method selected") + })?; + match method { + MfaMethod::Totp => { + if !user.totp_enabled { + error!("TOTP not enabled for user {}", user.username); + return Err(Status::invalid_argument( + "selected MFA method not available", + )); + } + } + MfaMethod::Email => { + if !user.email_mfa_enabled { + error!("Email MFA not enabled for user {}", user.username); + return Err(Status::invalid_argument( + "selected MFA method not available", + )); + } + // send email code + send_email_mfa_code_email(&user, &self.mail_tx, None).map_err(|err| { + error!( + "Failed to send email MFA code for user {}: {err:?}", + user.username + ); + Status::internal("unexpected error") + })?; + } + }; + + // generate auth token + let token = self.generate_token(&request.pubkey)?; + + // store login session + self.sessions.insert( + request.pubkey, + ClientLoginSession { + method, + location, + device, + user, + }, + ); + + Ok(ClientMfaStartResponse { token }) + } + + pub async fn finish_client_mfa_login( + &mut self, + request: ClientMfaFinishRequest, + ) -> Result { + info!("Finishing desktop client login: {request:?}"); + // get pubkey from token + let pubkey = self.parse_token(&request.token)?; + + // fetch login session + let Some(session) = self.sessions.remove(&pubkey) else { + error!("Client login session not found"); + return Err(Status::invalid_argument("login session not found")); + }; + let ClientLoginSession { + method, + device, + location, + user, + } = session; + + // validate code + match method { + MfaMethod::Totp => { + if !user.verify_totp_code(request.code) { + error!("Provided TOTP code is not valid"); + return Err(Status::unauthenticated("unauthorized")); + } + } + MfaMethod::Email => { + if !user.verify_email_mfa_code(request.code) { + error!("Provided email code is not valid"); + return Err(Status::unauthenticated("unauthorized")); + } + } + }; + + // begin transaction + let mut transaction = self.pool.begin().await.map_err(|_| { + error!("Failed to begin transaction"); + Status::internal("unexpected error") + })?; + + // fetch device config for the location + let Ok(Some(mut network_device)) = WireguardNetworkDevice::find( + &mut *transaction, + device.id.expect("Missing device ID"), + location.id.expect("Missing location ID"), + ) + .await + else { + error!("Failed to fetch network config for device {device} and location {location}"); + return Err(Status::internal("unexpected error")); + }; + + // generate PSK + let key = WireguardNetwork::genkey(); + network_device.preshared_key = Some(key.public.clone()); + + // authorize device for given location + network_device.is_authorized = true; + + // save updated network config + network_device + .update(&mut *transaction) + .await + .map_err(|err| { + error!("Failed to update device network config {network_device:?}: {err:?}"); + Status::internal("unexpected error") + })?; + + // send gateway event + debug!("Sending `peer_create` message to gateway"); + let device_info = DeviceInfo { + device, + network_info: vec![DeviceNetworkInfo { + network_id: location.id.expect("Missing location ID"), + device_wireguard_ip: network_device.wireguard_ip, + preshared_key: network_device.preshared_key, + }], + }; + let event = GatewayEvent::DeviceCreated(device_info); + self.wireguard_tx.send(event).map_err(|err| { + error!("Error sending WireGuard event: {err}"); + Status::internal("unexpected error") + })?; + + // commit transaction + transaction.commit().await.map_err(|_| { + error!("Failed to commit transaction"); + Status::internal("unexpected error") + })?; + + Ok(ClientMfaFinishResponse { + preshared_key: key.public, + }) + } +} diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index e0a568b96..c81e8db86 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -283,7 +283,6 @@ impl EnrollmentServer { request: NewDevice, req_device_info: Option, ) -> Result { - let config = SERVER_CONFIG.get().expect("defguard config not found"); debug!("Adding new user device: {request:?}"); let enrollment = self.validate_session(request.token.as_deref()).await?; @@ -319,16 +318,17 @@ impl EnrollmentServer { Status::internal("unexpected error") })?; - let (network_info, configs) = device - .add_to_all_networks(&mut transaction, &config.admin_groupname) - .await - .map_err(|err| { - error!( - "Failed to add device {} to existing networks: {err}", - device.name - ); - Status::internal("unexpected error") - })?; + let (network_info, configs) = + device + .add_to_all_networks(&mut transaction) + .await + .map_err(|err| { + error!( + "Failed to add device {} to existing networks: {err}", + device.name + ); + Status::internal("unexpected error") + })?; self.send_wireguard_event(GatewayEvent::DeviceCreated(DeviceInfo { device: device.clone(), diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index c4006a333..43a2cf589 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -29,6 +29,7 @@ use uuid::Uuid; use self::gateway::{gateway_service_server::GatewayServiceServer, GatewayServer}; use self::{ auth::{auth_service_server::AuthServiceServer, AuthServer}, + desktop_client_mfa::ClientMfaServer, enrollment::EnrollmentServer, password_reset::PasswordResetServer, proto::core_response, @@ -49,6 +50,7 @@ use crate::{ }; mod auth; +mod desktop_client_mfa; pub mod enrollment; #[cfg(feature = "wireguard")] pub(crate) mod gateway; @@ -62,8 +64,7 @@ pub(crate) mod proto { tonic::include_proto!("defguard.proxy"); } -use crate::grpc::proto::CoreError; -use proto::{core_request, proxy_client::ProxyClient, CoreResponse}; +use proto::{core_request, proxy_client::ProxyClient, CoreError, CoreResponse}; // Helper struct used to handle gateway state // gateways are grouped by network @@ -344,7 +345,8 @@ pub async fn run_grpc_bidi_stream( mail_tx.clone(), user_agent_parser, ); - let password_reset_server = PasswordResetServer::new(pool, mail_tx); + let password_reset_server = PasswordResetServer::new(pool.clone(), mail_tx.clone()); + let mut client_mfa_server = ClientMfaServer::new(pool, mail_tx, wireguard_tx); let endpoint = Endpoint::from_shared(config.proxy_url.as_deref().unwrap())?; let endpoint = endpoint.http2_keep_alive_interval(TEN_SECS); @@ -462,6 +464,30 @@ pub async fn run_grpc_bidi_stream( } } } + // rpc ClientMfaStart (ClientMfaStartRequest) returns (ClientMfaStartResponse) + Some(core_request::Payload::ClientMfaStart(request)) => { + match client_mfa_server.start_client_mfa_login(request).await { + Ok(response_payload) => { + Some(core_response::Payload::ClientMfaStart(response_payload)) + } + Err(err) => { + error!("client MFA start error {err}"); + Some(core_response::Payload::CoreError(err.into())) + } + } + } + // rpc ClientMfaFinish (ClientMfaFinishRequest) returns (ClientMfaFinishResponse) + Some(core_request::Payload::ClientMfaFinish(request)) => { + match client_mfa_server.finish_client_mfa_login(request).await { + Ok(response_payload) => { + Some(core_response::Payload::ClientMfaFinish(response_payload)) + } + Err(err) => { + error!("client MFA start error {err}"); + Some(core_response::Payload::CoreError(err.into())) + } + } + } // Reply without payload. None => None, }; diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index be0643ea5..fdeba9461 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -598,7 +598,7 @@ pub async fn request_email_mfa_code( if let Some(user) = User::find_by_id(&appstate.pool, session.user_id).await? { debug!("Sending email MFA code for user {}", user.username); if user.email_mfa_enabled { - send_email_mfa_code_email(&user, &appstate.mail_tx, &session)?; + send_email_mfa_code_email(&user, &appstate.mail_tx, Some(&session))?; info!("Sent email MFA code for user {}", user.username); Ok(ApiResponse::default()) } else { diff --git a/src/handlers/mail.rs b/src/handlers/mail.rs index 0a273a309..73c8e1cc9 100644 --- a/src/handlers/mail.rs +++ b/src/handlers/mail.rs @@ -385,7 +385,7 @@ pub fn send_email_mfa_activation_email( pub fn send_email_mfa_code_email( user: &User, mail_tx: &UnboundedSender, - session: &Session, + session: Option<&Session>, ) -> Result<(), TemplateError> { debug!("Sending email MFA code mail to {}", user.email); diff --git a/src/handlers/user.rs b/src/handlers/user.rs index c3b650632..e96301905 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -314,9 +314,7 @@ pub async fn modify_user( { let networks = WireguardNetwork::all(&mut *transaction).await?; for network in networks { - let gateway_events = network - .sync_allowed_devices(&mut transaction, &appstate.config.admin_groupname, None) - .await?; + let gateway_events = network.sync_allowed_devices(&mut transaction, None).await?; appstate.send_multiple_wireguard_events(gateway_events); } }; diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index 96cbc6b00..0a853df7e 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -114,9 +114,7 @@ pub async fn create_network( .await?; // generate IP addresses for existing devices - network - .add_all_allowed_devices(&mut transaction, &appstate.config.admin_groupname) - .await?; + network.add_all_allowed_devices(&mut transaction).await?; info!("Assigning IPs for existing devices in network {network}"); match &network.id { @@ -181,9 +179,7 @@ pub async fn modify_network( network .set_allowed_groups(&mut transaction, data.allowed_groups) .await?; - let _events = network - .sync_allowed_devices(&mut transaction, &appstate.config.admin_groupname, None) - .await?; + let _events = network.sync_allowed_devices(&mut transaction, None).await?; match &network.id { Some(network_id) => { @@ -377,22 +373,14 @@ pub async fn import_network( .map(|dev| dev.wireguard_ip) .collect(); let (devices, gateway_events) = network - .handle_imported_devices( - &mut transaction, - imported_devices, - &appstate.config.admin_groupname, - ) + .handle_imported_devices(&mut transaction, imported_devices) .await?; appstate.send_multiple_wireguard_events(gateway_events); // assign IPs for other existing devices info!("Assigning IPs in imported network for remaining existing devices"); let gateway_events = network - .sync_allowed_devices( - &mut transaction, - &appstate.config.admin_groupname, - Some(&reserved_ips), - ) + .sync_allowed_devices(&mut transaction, Some(&reserved_ips)) .await?; appstate.send_multiple_wireguard_events(gateway_events); @@ -434,11 +422,7 @@ pub async fn add_user_devices( // wrap loop in transaction to abort if a device is invalid let mut transaction = appstate.pool.begin().await?; let events = network - .handle_mapped_devices( - &mut transaction, - mapped_devices, - &appstate.config.admin_groupname, - ) + .handle_mapped_devices(&mut transaction, mapped_devices) .await?; appstate.send_multiple_wireguard_events(events); transaction.commit().await?; @@ -500,9 +484,7 @@ pub async fn add_device( device: Device, } - let (network_info, configs) = device - .add_to_all_networks(&mut transaction, &appstate.config.admin_groupname) - .await?; + let (network_info, configs) = device.add_to_all_networks(&mut transaction).await?; let mut network_ips: Vec = Vec::new(); for network_info_item in network_info.clone() { diff --git a/src/templates.rs b/src/templates.rs index 0f9cbb1d3..52a712563 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -261,8 +261,8 @@ pub fn email_mfa_activation_mail(code: u32, session: &Session) -> Result Result { - let (mut tera, mut context) = get_base_tera(None, Some(session), None, None)?; +pub fn email_mfa_code_mail(code: u32, session: Option<&Session>) -> Result { + let (mut tera, mut context) = get_base_tera(None, session, None, None)?; // zero-pad code to make sure it's always 6 digits long context.insert("code", &format!("{code:0>6}")); tera.add_raw_template("mail_email_mfa_code", MAIL_EMAIL_MFA_CODE)?; diff --git a/src/wireguard_peer_disconnect.rs b/src/wireguard_peer_disconnect.rs index ea025cb8f..0e1a09c44 100644 --- a/src/wireguard_peer_disconnect.rs +++ b/src/wireguard_peer_disconnect.rs @@ -70,7 +70,8 @@ pub async fn run_periodic_peer_disconnect( FROM device d \ JOIN wireguard_network_device wnd ON wnd.device_id = d.id \ LEFT JOIN stats on d.id = stats.device_id \ - WHERE wnd.wireguard_network_id = $1 AND wnd.is_authorized = true AND (NOW() - stats.latest_handshake) > $2 * interval '1 second'", + WHERE wnd.wireguard_network_id = $1 AND wnd.is_authorized = true AND \ + (stats.latest_handshake IS NULL OR (NOW() - stats.latest_handshake) > $2 * interval '1 second')", location_id, location.peer_disconnect_threshold as f64 ) diff --git a/tests/wireguard_network_import.rs b/tests/wireguard_network_import.rs index 9ddbcc91b..1ac5c5f3a 100644 --- a/tests/wireguard_network_import.rs +++ b/tests/wireguard_network_import.rs @@ -69,7 +69,7 @@ async fn test_config_import() { ); device_1.save(&mut *transaction).await.unwrap(); device_1 - .add_to_all_networks(&mut transaction, &client_state.config.admin_groupname) + .add_to_all_networks(&mut transaction) .await .unwrap(); @@ -80,7 +80,7 @@ async fn test_config_import() { ); device_2.save(&mut *transaction).await.unwrap(); device_2 - .add_to_all_networks(&mut transaction, &client_state.config.admin_groupname) + .add_to_all_networks(&mut transaction) .await .unwrap(); From dd86c5b8994087dbcd90a952598644a6ac841571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 17 Jan 2024 12:41:43 +0100 Subject: [PATCH 18/26] ci: fix proxy url --- docker-compose.e2e.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.e2e.yaml b/docker-compose.e2e.yaml index b25da2099..49129382d 100644 --- a/docker-compose.e2e.yaml +++ b/docker-compose.e2e.yaml @@ -20,7 +20,7 @@ services: DEFGUARD_DB_PASSWORD: defguard DEFGUARD_DB_NAME: defguard DEFGUARD_URL: http://localhost:8000 - DEFGUARD_PROXY_URL: http://localhost:50052 + DEFGUARD_PROXY_URL: http://proxy:50052 RUST_BACKTRACE: 1 ports: # REST API From 60437463db6dcd4bf9535bed5adea70f1d93b48c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 17 Jan 2024 15:42:49 +0100 Subject: [PATCH 19/26] refactor: update startup log messages --- src/bin/defguard.rs | 5 +++-- src/db/mod.rs | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/bin/defguard.rs b/src/bin/defguard.rs index 75e6e5b36..4b9e397f1 100644 --- a/src/bin/defguard.rs +++ b/src/bin/defguard.rs @@ -45,6 +45,9 @@ async fn main() -> Result<(), anyhow::Error> { .init(); SERVER_CONFIG.set(config.clone())?; + info!("Starting defguard"); + debug!("Using config: {config:?}"); + let pool = init_db( &config.database_host, config.database_port, @@ -70,8 +73,6 @@ async fn main() -> Result<(), anyhow::Error> { return Ok(()); } - debug!("Starting defguard server with config: {config:?}"); - if config.openid_signing_key.is_some() { info!("Using RSA OpenID signing key"); } else { diff --git a/src/db/mod.rs b/src/db/mod.rs index 7559c1cb2..cbad0a295 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -6,6 +6,7 @@ pub type DbPool = sqlx::postgres::PgPool; /// Initializes and migrates postgres database. Returns DB pool object. pub async fn init_db(host: &str, port: u16, name: &str, user: &str, password: &str) -> DbPool { + info!("Initializing DB pool"); let opts = PgConnectOptions::new() .host(host) .port(port) From 0c80c6435110c676518e64b8f460e7a619abe2df Mon Sep 17 00:00:00 2001 From: Maciek Date: Thu, 18 Jan 2024 13:31:00 +0100 Subject: [PATCH 20/26] fix: allow username special chars (#505) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix username validation on backend * firx frontend validation * fix typo * formatting * formatting * remove regex * update crate metadata * remove bincode crate * fix tests --------- Co-authored-by: Maciej Wójcik --- Cargo.lock | 10 ---- Cargo.toml | 6 ++- src/handlers/user.rs | 49 +++++++++++++++++-- tests/auth.rs | 3 +- tests/user.rs | 8 +-- web/src/i18n/en/index.ts | 2 +- web/src/i18n/i18n-types.ts | 4 +- .../ProfileDetailsForm/ProfileDetailsForm.tsx | 13 ++--- .../components/AddUserForm/AddUserForm.tsx | 7 --- web/src/shared/patterns.ts | 8 ++- 10 files changed, 64 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8d24b7a11..2fc3b589f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -466,15 +466,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -989,7 +980,6 @@ dependencies = [ "axum-client-ip", "axum-extra", "base64 0.21.7", - "bincode", "bytes", "chrono", "claims", diff --git a/Cargo.toml b/Cargo.toml index 2678ab831..112b97668 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,9 @@ name = "defguard" version = "0.8.0" edition = "2021" +license = "Apache-2.0" +homepage = "https://defguard.net/" +repository = "https://github.com/DefGuard/defguard" [workspace] @@ -17,7 +20,6 @@ axum-extra = { version = "0.9", features = [ "typed-header", ] } base64 = "0.21" -bincode = "1.3" chrono = { version = "0.4", default-features = false, features = [ "clock", "serde", @@ -63,6 +65,7 @@ sqlx = { version = "0.7", features = [ "postgres", "uuid", ] } +struct-patch = "0.4" tera = "1.19" thiserror = "1.0" # match axum-extra -> cookies @@ -89,7 +92,6 @@ webauthn-rs = { version = "0.4", features = [ ] } webauthn-rs-proto = "0.4" x25519-dalek = { version = "2.0", features = ["static_secrets"] } -struct-patch = "0.4" [dev-dependencies] bytes = "1.5" diff --git a/src/handlers/user.rs b/src/handlers/user.rs index e96301905..25a51c939 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -25,7 +25,17 @@ use crate::{ }; /// Verify the given username +/// +/// To enable LDAP sync usernames need to avoid reserved characters. +/// Username requirements: +/// - 3 - 64 characters long +/// - lowercase or uppercase latin alphabet letters (A-Z, a-z) +/// - digits (0-9) +/// - starts with non-special character +/// - special characters: . - _ +/// - no whitespaces fn check_username(username: &str) -> Result<(), WebError> { + // check length let length = username.len(); if !(3..64).contains(&length) { return Err(WebError::Serialization(format!( @@ -33,22 +43,25 @@ fn check_username(username: &str) -> Result<(), WebError> { ))); } + // check first character is a letter or digit if let Some(first_char) = username.chars().next() { - if first_char.is_ascii_digit() { + if !first_char.is_ascii_alphanumeric() { return Err(WebError::Serialization( - "Username must not start with a digit".into(), + "Username must not start with a special character".into(), )); } } + // check if username contains only valid characters if !username .chars() - .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()) + .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_') { return Err(WebError::Serialization( - "Username is not in lowercase".into(), + "Username contains invalid characters".into(), )); } + Ok(()) } @@ -783,3 +796,31 @@ pub async fn delete_authorized_app( Err(WebError::ObjectNotFound("Authorized app not found".into())) } } + +#[cfg(test)] +mod test { + use super::*; + use claims::{assert_err, assert_ok}; + + #[test] + fn test_username_validation() { + // valid usernames + assert_ok!(check_username("zenek34")); + assert_ok!(check_username("zenekXXX__")); + assert_ok!(check_username("first.last")); + assert_ok!(check_username("First_Last")); + assert_ok!(check_username("32zenek")); + assert_ok!(check_username("32-zenek")); + + // invalid usernames + assert_err!(check_username("a")); + assert_err!(check_username("32")); + assert_err!(check_username("a4")); + assert_err!(check_username("__zenek")); + assert_err!(check_username("zenek?")); + assert_err!(check_username("MeMeMe!")); + assert_err!(check_username( + "averylongnameeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + )); + } +} diff --git a/tests/auth.rs b/tests/auth.rs index 05088fe62..fe82f16ef 100644 --- a/tests/auth.rs +++ b/tests/auth.rs @@ -262,8 +262,9 @@ async fn test_totp() { assert_eq!(response.status(), StatusCode::OK); } +static EMAIL_CODE_REGEX: &str = r"(?\d{6})"; fn extract_email_code(content: &str) -> u32 { - let re = regex::Regex::new(r"(?\d{6})").unwrap(); + let re = regex::Regex::new(EMAIL_CODE_REGEX).unwrap(); let code = re.captures(content).unwrap().name("code").unwrap().as_str(); code.parse().unwrap() } diff --git a/tests/user.rs b/tests/user.rs index b1ecd55f8..07238c70f 100644 --- a/tests/user.rs +++ b/tests/user.rs @@ -224,7 +224,7 @@ async fn test_username_available() { assert_eq!(response.status(), StatusCode::OK); let avail = Username { - username: "CrashTestDummy".into(), + username: "_CrashTestDummy".into(), }; let response = client .post("/api/v1/user/available") @@ -234,7 +234,7 @@ async fn test_username_available() { assert_eq!(response.status(), StatusCode::BAD_REQUEST); let avail = Username { - username: "crashtestdummy".into(), + username: "crashtestdummy42".into(), }; let response = client .post("/api/v1/user/available") @@ -431,8 +431,8 @@ async fn test_check_username() { let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); - let invalid_usernames = ["ADumbledore", "1user"]; - let valid_usernames = ["user1", "use2r3", "notwrong"]; + let invalid_usernames = ["ADumble dore", ".1user"]; + let valid_usernames = ["user1", "use2r3", "not_wrong"]; for username in invalid_usernames { let new_user = AddUserData { diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index b53083ef1..6d6acb4e8 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -711,7 +711,7 @@ const en: BaseTranslation = { username: 'Username', }, error: { - forbiddenCharacter: 'Field contain forbidden characters.', + forbiddenCharacter: 'Field contains forbidden characters.', usernameTaken: 'Username is already in use.', invalidKey: 'Key is invalid.', invalid: 'Field is invalid.', diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index 8b6df2feb..c86f4eb14 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -1670,7 +1670,7 @@ type RootTranslation = { } error: { /** - * F​i​e​l​d​ ​c​o​n​t​a​i​n​ ​f​o​r​b​i​d​d​e​n​ ​c​h​a​r​a​c​t​e​r​s​. + * F​i​e​l​d​ ​c​o​n​t​a​i​n​s​ ​f​o​r​b​i​d​d​e​n​ ​c​h​a​r​a​c​t​e​r​s​. */ forbiddenCharacter: string /** @@ -5210,7 +5210,7 @@ export type TranslationFunctions = { } error: { /** - * Field contain forbidden characters. + * Field contains forbidden characters. */ forbiddenCharacter: () => LocalizedString /** diff --git a/web/src/pages/users/UserProfile/ProfileDetails/ProfileDetailsForm/ProfileDetailsForm.tsx b/web/src/pages/users/UserProfile/ProfileDetails/ProfileDetailsForm/ProfileDetailsForm.tsx index afd7a40f4..97c6a67d6 100644 --- a/web/src/pages/users/UserProfile/ProfileDetails/ProfileDetailsForm/ProfileDetailsForm.tsx +++ b/web/src/pages/users/UserProfile/ProfileDetails/ProfileDetailsForm/ProfileDetailsForm.tsx @@ -15,8 +15,7 @@ import useApi from '../../../../../shared/hooks/useApi'; import { useToaster } from '../../../../../shared/hooks/useToaster'; import { MutationKeys } from '../../../../../shared/mutations'; import { - patternNoSpecialChars, - patternStartsWithDigit, + patternSafeUsernameCharacters, patternValidEmail, patternValidPhoneNumber, } from '../../../../../shared/patterns'; @@ -68,15 +67,9 @@ export const ProfileDetailsForm = () => { username: yup .string() .required(LL.form.error.required()) - .matches(patternNoSpecialChars, LL.form.error.noSpecialChars()) + .matches(patternSafeUsernameCharacters, LL.form.error.forbiddenCharacter()) .min(3, LL.form.error.minimumLength()) - .max(64, LL.form.error.maximumLength()) - .test('starts-with-number', LL.form.error.startFromNumber(), (value) => { - if (value && value.length) { - return !patternStartsWithDigit.test(value); - } - return false; - }), + .max(64, LL.form.error.maximumLength()), first_name: yup.string().required(LL.form.error.required()), last_name: yup.string().required(LL.form.error.required()), phone: yup diff --git a/web/src/pages/users/UsersOverview/modals/AddUserModal/components/AddUserForm/AddUserForm.tsx b/web/src/pages/users/UsersOverview/modals/AddUserModal/components/AddUserForm/AddUserForm.tsx index 224e49e2a..812369dbc 100644 --- a/web/src/pages/users/UsersOverview/modals/AddUserModal/components/AddUserForm/AddUserForm.tsx +++ b/web/src/pages/users/UsersOverview/modals/AddUserModal/components/AddUserForm/AddUserForm.tsx @@ -20,7 +20,6 @@ import useApi from '../../../../../../../shared/hooks/useApi'; import { useToaster } from '../../../../../../../shared/hooks/useToaster'; import { patternSafeUsernameCharacters, - patternStartsWithDigit, patternValidEmail, patternValidPhoneNumber, } from '../../../../../../../shared/patterns'; @@ -59,12 +58,6 @@ export const AddUserForm = () => { .matches(patternSafeUsernameCharacters, LL.form.error.forbiddenCharacter()) .min(3, LL.form.error.minimumLength()) .max(64, LL.form.error.maximumLength()) - .test('starts-with-number', LL.form.error.startFromNumber(), (value) => { - if (value && value.length) { - return !patternStartsWithDigit.test(value); - } - return false; - }) .test( 'username-available', LL.form.error.usernameTaken(), diff --git a/web/src/shared/patterns.ts b/web/src/shared/patterns.ts index 9c497c7bb..1cb06ea8c 100644 --- a/web/src/shared/patterns.ts +++ b/web/src/shared/patterns.ts @@ -14,8 +14,6 @@ export const patternAtLeastOneLowerCaseChar = /(?=.*?[a-z])/g; export const patternAtLeastOneDigit = /(?=.*?[0-9])/g; -export const patternStartsWithDigit = /^\d/; - export const patternAtLeastOneSpecialChar = /(?=.*?[#?!@$%^&*-])/g; export const patternValidPhoneNumber = @@ -73,7 +71,7 @@ export const patternValidDomain = export const patternValidIp = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; -export const patternSafeUsernameCharacters = - /^[a-zA-Z0-9.!@#$%^&*()_+\-=\[\]{}|,<>\/?~]+$/; +export const patternSafeUsernameCharacters = /^[a-zA-Z0-9]+[a-zA-Z0-9.\-_]+$/; -export const patternSafePasswordCharacters = patternSafeUsernameCharacters; +export const patternSafePasswordCharacters = + /^[a-zA-Z0-9.!@#$%^&*()_+\-=\[\]{}|,<>\/?~]+$/; From 696ff0279985887a9464e01992a950f3c99761cc Mon Sep 17 00:00:00 2001 From: Maciek Date: Fri, 19 Jan 2024 13:29:14 +0100 Subject: [PATCH 21/26] fix: invalid new MFA peer (#509) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * don't add peer on new device creation * update query data --------- Co-authored-by: Maciej Wójcik --- ...d378b0653a7b6b98d71caaf55c6651a44bbd57df017.json} | 12 +++++++++--- src/db/models/device.rs | 4 +++- src/db/models/wireguard.rs | 6 ++++++ src/grpc/desktop_client_mfa.rs | 1 + src/grpc/gateway.rs | 9 ++++++++- src/handlers/wireguard.rs | 1 + src/wireguard_peer_disconnect.rs | 1 + 7 files changed, 29 insertions(+), 5 deletions(-) rename .sqlx/{query-6eff81e0ddc89652014c10c9b5c1561dfa68bc80db59bc000dc217ffa639b53b.json => query-63dd22326d77a452d5624d378b0653a7b6b98d71caaf55c6651a44bbd57df017.json} (63%) diff --git a/.sqlx/query-6eff81e0ddc89652014c10c9b5c1561dfa68bc80db59bc000dc217ffa639b53b.json b/.sqlx/query-63dd22326d77a452d5624d378b0653a7b6b98d71caaf55c6651a44bbd57df017.json similarity index 63% rename from .sqlx/query-6eff81e0ddc89652014c10c9b5c1561dfa68bc80db59bc000dc217ffa639b53b.json rename to .sqlx/query-63dd22326d77a452d5624d378b0653a7b6b98d71caaf55c6651a44bbd57df017.json index 15d12798f..758eb4113 100644 --- a/.sqlx/query-6eff81e0ddc89652014c10c9b5c1561dfa68bc80db59bc000dc217ffa639b53b.json +++ b/.sqlx/query-63dd22326d77a452d5624d378b0653a7b6b98d71caaf55c6651a44bbd57df017.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT wireguard_network_id as network_id, wireguard_ip as \"device_wireguard_ip: IpAddr\", preshared_key FROM wireguard_network_device WHERE device_id = $1", + "query": "SELECT wireguard_network_id as network_id, wireguard_ip as \"device_wireguard_ip: IpAddr\", preshared_key, is_authorized FROM wireguard_network_device WHERE device_id = $1", "describe": { "columns": [ { @@ -17,6 +17,11 @@ "ordinal": 2, "name": "preshared_key", "type_info": "Text" + }, + { + "ordinal": 3, + "name": "is_authorized", + "type_info": "Bool" } ], "parameters": { @@ -27,8 +32,9 @@ "nullable": [ false, false, - true + true, + false ] }, - "hash": "6eff81e0ddc89652014c10c9b5c1561dfa68bc80db59bc000dc217ffa639b53b" + "hash": "63dd22326d77a452d5624d378b0653a7b6b98d71caaf55c6651a44bbd57df017" } diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 69f24228c..e4d4dcd59 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -62,6 +62,7 @@ pub struct DeviceNetworkInfo { pub device_wireguard_ip: IpAddr, #[serde(skip_serializing)] pub preshared_key: Option, + pub is_authorized: bool, } impl DeviceInfo { @@ -73,7 +74,7 @@ impl DeviceInfo { let device_id = device.get_id()?; let network_info = query_as!( DeviceNetworkInfo, - "SELECT wireguard_network_id as network_id, wireguard_ip as \"device_wireguard_ip: IpAddr\", preshared_key \ + "SELECT wireguard_network_id as network_id, wireguard_ip as \"device_wireguard_ip: IpAddr\", preshared_key, is_authorized \ FROM wireguard_network_device \ WHERE device_id = $1", device_id @@ -538,6 +539,7 @@ impl Device { network_id, device_wireguard_ip: wireguard_network_device.wireguard_ip, preshared_key: wireguard_network_device.preshared_key.clone(), + is_authorized: wireguard_network_device.is_authorized, }; network_info.push(device_network_info); diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 0f24d8262..cb79057a6 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -420,6 +420,7 @@ impl WireguardNetwork { network_id, device_wireguard_ip: wireguard_network_device.wireguard_ip, preshared_key: wireguard_network_device.preshared_key, + is_authorized: wireguard_network_device.is_authorized, }], })); } @@ -439,6 +440,7 @@ impl WireguardNetwork { network_id, device_wireguard_ip: device_network_config.wireguard_ip, preshared_key: device_network_config.preshared_key, + is_authorized: device_network_config.is_authorized, }], })); } else { @@ -460,6 +462,7 @@ impl WireguardNetwork { network_id, device_wireguard_ip: wireguard_network_device.wireguard_ip, preshared_key: wireguard_network_device.preshared_key, + is_authorized: wireguard_network_device.is_authorized, }], })); } @@ -516,6 +519,7 @@ impl WireguardNetwork { network_id, device_wireguard_ip: wireguard_network_device.wireguard_ip, preshared_key: wireguard_network_device.preshared_key, + is_authorized: wireguard_network_device.is_authorized, }], })); } @@ -591,6 +595,7 @@ impl WireguardNetwork { network_id, device_wireguard_ip: wireguard_network_device.wireguard_ip, preshared_key: wireguard_network_device.preshared_key, + is_authorized: wireguard_network_device.is_authorized, }); } Some(allowed) => { @@ -607,6 +612,7 @@ impl WireguardNetwork { network_id, device_wireguard_ip: wireguard_network_device.wireguard_ip, preshared_key: wireguard_network_device.preshared_key, + is_authorized: wireguard_network_device.is_authorized, }); } } diff --git a/src/grpc/desktop_client_mfa.rs b/src/grpc/desktop_client_mfa.rs index 93556e41b..60292da27 100644 --- a/src/grpc/desktop_client_mfa.rs +++ b/src/grpc/desktop_client_mfa.rs @@ -250,6 +250,7 @@ impl ClientMfaServer { network_id: location.id.expect("Missing location ID"), device_wireguard_ip: network_device.wireguard_ip, preshared_key: network_device.preshared_key, + is_authorized: network_device.is_authorized, }], }; let event = GatewayEvent::DeviceCreated(device_info); diff --git a/src/grpc/gateway.rs b/src/grpc/gateway.rs index 328734daa..a347290ac 100644 --- a/src/grpc/gateway.rs +++ b/src/grpc/gateway.rs @@ -210,7 +210,10 @@ impl GatewayUpdatesHandler { } GatewayEvent::NetworkModified(network_id, network, peers) => { if network_id == self.network_id { - self.send_network_update(&network, peers, 1).await + let result = self.send_network_update(&network, peers, 1).await; + // update stored network data + self.network = network; + result } else { Ok(()) } @@ -230,6 +233,10 @@ impl GatewayUpdatesHandler { .find(|info| info.network_id == self.network_id) { Some(network_info) => { + if self.network.mfa_enabled && !network_info.is_authorized { + debug!("Created WireGuard device is not authorized to connect to MFA enabled location"); + continue; + }; self.send_peer_update( Peer { pubkey: device.device.wireguard_pubkey, diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index 0a853df7e..489ba1fae 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -582,6 +582,7 @@ pub async fn modify_device( network_id, device_wireguard_ip: wireguard_network_device.wireguard_ip, preshared_key: wireguard_network_device.preshared_key, + is_authorized: wireguard_network_device.is_authorized, }; network_info.push(device_network_info); } diff --git a/src/wireguard_peer_disconnect.rs b/src/wireguard_peer_disconnect.rs index 0e1a09c44..34d1757b6 100644 --- a/src/wireguard_peer_disconnect.rs +++ b/src/wireguard_peer_disconnect.rs @@ -103,6 +103,7 @@ pub async fn run_periodic_peer_disconnect( network_id: location_id, device_wireguard_ip: device_network_config.wireguard_ip, preshared_key: device_network_config.preshared_key, + is_authorized: device_network_config.is_authorized, }], }; let event = GatewayEvent::DeviceDeleted(device_info); From ccbe16ff60408a7cc1b33776f8322cc1cbc64309 Mon Sep 17 00:00:00 2001 From: Maciek Date: Mon, 22 Jan 2024 11:31:41 +0100 Subject: [PATCH 22/26] fix: update username validation in login form (#510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix user form validation * don't cast username to lowercase * redirect admin on username change * fix typos * fix typos * fix typos --------- Co-authored-by: Maciej Wójcik --- README.md | 6 ++--- docker-compose.ldap.yaml | 2 +- docker-compose.yaml | 2 +- src/db/models/wireguard.rs | 6 ++--- src/handlers/auth.rs | 26 +++++++++---------- src/wg_config.rs | 4 +-- web/src/i18n/pl/index.ts | 2 +- web/src/pages/auth/Login/Login.tsx | 6 +++-- .../ProfileDetailsForm/ProfileDetailsForm.tsx | 10 ++++++- 9 files changed, 36 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 69619d9a9..3d35f8d24 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ defguard

-defguard is an **SSO & VPN Server** based on **OpenID and Wireguard VPN** with unique secure&private architecture for **building secure and privacy-aware organizations**. +defguard is an **SSO & VPN Server** based on **OpenID and WireGuard VPN** with unique secure&private architecture for **building secure and privacy-aware organizations**. By design **defguard core is meant to be deployed in your secure network segments** (available only from an internal network or by VPN) and operations that require **public access** (like user onboarding, enrollment, password reset, etc.) are done using a **secure proxy**. @@ -16,8 +16,8 @@ Read more about this in [our documentation](https://defguard.gitbook.io/defguard - LDAP (tested on [OpenLDAP](https://www.openldap.org/)) synchronization - [forward auth](https://defguard.gitbook.io/defguard/features/forward-auth) for reverse proxies (tested with Traefik and Caddy) - nice UI to manage users - - Users **self-service** (besides typical data management, users can revoke access to granted apps, MFA, Wireguard, etc.) -* [Wireguard:tm:](https://www.wireguard.com/) VPN management with: + - Users **self-service** (besides typical data management, users can revoke access to granted apps, MFA, WireGuard, etc.) +* [WireGuard:tm:](https://www.wireguard.com/) VPN management with: - multiple VPN Locations (networks/sites) - with defined access (all users or only Admin group) - multiple [Gateways](https://github.com/DefGuard/gateway) for each VPN Location (**high availability/failover**) - supported on a cluster of routers/firewalls for Linux, FreeBSD/PFSense/OPNSense - **import your current WireGuard server configuration (with a wizard!)** diff --git a/docker-compose.ldap.yaml b/docker-compose.ldap.yaml index e89682e39..d078bfb2c 100644 --- a/docker-compose.ldap.yaml +++ b/docker-compose.ldap.yaml @@ -38,7 +38,7 @@ services: DEFGUARD_TOKEN: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJEZWZHdWFyZCIsInN1YiI6IlRlc3ROZXQiLCJjbGllbnRfaWQiOiIiLCJleHAiOjU5NjE3NDcwNzYsIm5iZiI6MTY2Njc3OTc4MSwicm9sZXMiOltdfQ.uEUMnw_gO23W0K2q3N1lToeP0D2zAY1swr8N-84sRHA RUST_LOG: debug ports: - # wireguard endpoint + # WireGuard endpoint - "50051:50051/udp" depends_on: - core diff --git a/docker-compose.yaml b/docker-compose.yaml index 233a98ac3..80f18a65a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -35,7 +35,7 @@ services: DEFGUARD_TOKEN: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJEZWZHdWFyZCIsInN1YiI6IlRlc3ROZXQiLCJjbGllbnRfaWQiOiIiLCJleHAiOjU5NjE3NDcwNzYsIm5iZiI6MTY2Njc3OTc4MSwicm9sZXMiOltdfQ.uEUMnw_gO23W0K2q3N1lToeP0D2zAY1swr8N-84sRHA RUST_LOG: debug ports: - # wireguard endpoint + # WireGuard endpoint - "50051:50051/udp" depends_on: - core diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index cb79057a6..03417c114 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -65,7 +65,7 @@ pub enum GatewayEvent { DeviceDeleted(DeviceInfo), } -/// Stores configuration required to setup a wireguard network +/// Stores configuration required to setup a WireGuard network #[derive(Clone, Debug, Model, Deserialize, Serialize, PartialEq)] #[table(wireguard_network)] pub struct WireguardNetwork { @@ -215,7 +215,7 @@ impl WireguardNetwork { Ok(()) } - /// Utility method to create wireguard keypair + /// Utility method to create WireGuard keypair #[must_use] pub fn genkey() -> WireguardKey { let private = StaticSecret::random_from_rng(OsRng); @@ -473,7 +473,7 @@ impl WireguardNetwork { /// Check if devices found in an imported config file exist already, /// if they do assign a specified IP. /// Return a list of imported devices which need to be manually mapped to a user - /// and a list of wireguard events to be sent out. + /// and a list of WireGuard events to be sent out. pub async fn handle_imported_devices( &self, transaction: &mut PgConnection, diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index fdeba9461..7d6520664 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -50,35 +50,33 @@ pub async fn authenticate( State(appstate): State, Json(data): Json, ) -> Result<(CookieJar, PrivateCookieJar, ApiResponse), WebError> { - let lowercase_username = data.username.to_lowercase(); - debug!("Authenticating user {lowercase_username}"); + let username = data.username; + debug!("Authenticating user {username}"); // check if user can proceed with login - check_username(&appstate.failed_logins, &lowercase_username)?; + check_username(&appstate.failed_logins, &username)?; - let user = match User::find_by_username(&appstate.pool, &lowercase_username).await { + let user = match User::find_by_username(&appstate.pool, &username).await { Ok(Some(user)) => match user.verify_password(&data.password) { Ok(()) => user, Err(err) => { - info!("Failed to authenticate user {lowercase_username}: {err}"); - log_failed_login_attempt(&appstate.failed_logins, &lowercase_username); + info!("Failed to authenticate user {username}: {err}"); + log_failed_login_attempt(&appstate.failed_logins, &username); return Err(WebError::Authorization(err.to_string())); } }, Ok(None) => { // create user from LDAP - debug!("User not found in DB, authenticating user {lowercase_username} with LDAP"); - if let Ok(user) = - user_from_ldap(&appstate.pool, &lowercase_username, &data.password).await - { + debug!("User not found in DB, authenticating user {username} with LDAP"); + if let Ok(user) = user_from_ldap(&appstate.pool, &username, &data.password).await { user } else { - info!("Failed to authenticate user {lowercase_username} with LDAP"); - log_failed_login_attempt(&appstate.failed_logins, &lowercase_username); + info!("Failed to authenticate user {username} with LDAP"); + log_failed_login_attempt(&appstate.failed_logins, &username); return Err(WebError::Authorization("user not found".into())); } } Err(err) => { - error!("DB error when authenticating user {lowercase_username}: {err}"); + error!("DB error when authenticating user {username}: {err}"); return Err(WebError::DbError(err.to_string())); } }; @@ -122,7 +120,7 @@ pub async fn authenticate( let login_event_type = "AUTHENTICATION".to_string(); - info!("Authenticated user {lowercase_username}"); + info!("Authenticated user {username}"); if user.mfa_enabled { if let Some(mfa_info) = MFAInfo::for_user(&appstate.pool, &user).await? { check_new_device_login( diff --git a/src/wg_config.rs b/src/wg_config.rs index c4560ef06..3a8307498 100644 --- a/src/wg_config.rs +++ b/src/wg_config.rs @@ -35,7 +35,7 @@ pub enum WireguardConfigParseError { InvalidKey(String), #[error("Invalid port: {0}")] InvalidPort(String), - #[error("Wireguard network error")] + #[error("WireGuard network error")] NetworkError(#[from] WireguardNetworkError), } @@ -55,7 +55,7 @@ pub fn parse_wireguard_config( config: &str, ) -> Result<(WireguardNetwork, Vec), WireguardConfigParseError> { let config = ini::Ini::load_from_str(config)?; - // Parse WireguardNetwork + // Parse WireGuardNetwork let interface_section = config .section(Some("Interface")) .ok_or_else(|| WireguardConfigParseError::SectionNotFound("Interface".to_string()))?; diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index fe41d274f..e3c3d3a55 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -443,7 +443,7 @@ const pl: Translation = { qrInfo: 'Użyj poniższych konfiguracji aby połączyć się z wybranymi lokalizacjami.', helpers: { - qrHelper: `

Możesz skonfigurować Wireguard na telefonie skanując QR kod przez aplikację Wireguard.

`, + qrHelper: `

Możesz skonfigurować WireGuard na telefonie skanując QR kod przez aplikację Wireguard.

`, warningAutoMode: `

Uwaga, Defguard nie przechowuje twojego klucza prywatnego. Gdy opuścisz obecną stronę nie będziesz mógł pobrać ponownie konfiguracji z kluczem prywatnym.

`, diff --git a/web/src/pages/auth/Login/Login.tsx b/web/src/pages/auth/Login/Login.tsx index 6639f854e..baa46c258 100644 --- a/web/src/pages/auth/Login/Login.tsx +++ b/web/src/pages/auth/Login/Login.tsx @@ -17,7 +17,7 @@ import { import { useAuthStore } from '../../../shared/hooks/store/useAuthStore'; import useApi from '../../../shared/hooks/useApi'; import { MutationKeys } from '../../../shared/mutations'; -import { patternNoSpecialChars } from '../../../shared/patterns'; +import { patternSafeUsernameCharacters } from '../../../shared/patterns'; import { LoginData } from '../../../shared/types'; type Inputs = { @@ -34,7 +34,9 @@ export const Login = () => { username: yup .string() .required(LL.form.error.required()) - .matches(patternNoSpecialChars, LL.form.error.noSpecialChars()), + .matches(patternSafeUsernameCharacters, LL.form.error.forbiddenCharacter()) + .min(3, LL.form.error.minimumLength()) + .max(64, LL.form.error.maximumLength()), password: yup .string() .required(LL.form.error.required()) diff --git a/web/src/pages/users/UserProfile/ProfileDetails/ProfileDetailsForm/ProfileDetailsForm.tsx b/web/src/pages/users/UserProfile/ProfileDetails/ProfileDetailsForm/ProfileDetailsForm.tsx index 97c6a67d6..1ce400ef1 100644 --- a/web/src/pages/users/UserProfile/ProfileDetails/ProfileDetailsForm/ProfileDetailsForm.tsx +++ b/web/src/pages/users/UserProfile/ProfileDetails/ProfileDetailsForm/ProfileDetailsForm.tsx @@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { pick } from 'lodash-es'; import { useEffect, useMemo, useRef, useState } from 'react'; import { Controller, SubmitErrorHandler, SubmitHandler, useForm } from 'react-hook-form'; +import { useNavigate, useParams } from 'react-router'; import * as yup from 'yup'; import { useI18nContext } from '../../../../../i18n/i18n-react'; @@ -59,6 +60,8 @@ export const ProfileDetailsForm = () => { user: { editUser }, groups: { getGroups }, } = useApi(); + const { username: paramsUsername } = useParams(); + const navigate = useNavigate(); const schema = useMemo( () => @@ -123,11 +126,16 @@ export const ProfileDetailsForm = () => { [MutationKeys.EDIT_USER], editUser, { - onSuccess: () => { + onSuccess: (_data, variables) => { queryClient.invalidateQueries([QueryKeys.FETCH_USERS_LIST]); queryClient.invalidateQueries([QueryKeys.FETCH_USER_PROFILE]); toaster.success(LL.userPage.messages.editSuccess()); setUserProfile({ editMode: false, loading: false }); + // if username was changed redirect to new profile page + const newUsername = variables.data.username; + if (paramsUsername !== newUsername) { + navigate(`/admin/users/${variables.data.username}`, { replace: true }); + } }, onError: (err) => { toaster.error(LL.messages.error()); From 6a0c22237a38f776cd45c4d365b291495c852eb3 Mon Sep 17 00:00:00 2001 From: Maciek Date: Mon, 22 Jan 2024 12:14:43 +0100 Subject: [PATCH 23/26] consume session token only if login was successful (#512) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Maciej Wójcik --- src/grpc/desktop_client_mfa.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/grpc/desktop_client_mfa.rs b/src/grpc/desktop_client_mfa.rs index 60292da27..c360d9435 100644 --- a/src/grpc/desktop_client_mfa.rs +++ b/src/grpc/desktop_client_mfa.rs @@ -181,7 +181,7 @@ impl ClientMfaServer { let pubkey = self.parse_token(&request.token)?; // fetch login session - let Some(session) = self.sessions.remove(&pubkey) else { + let Some(session) = self.sessions.get(&pubkey) else { error!("Client login session not found"); return Err(Status::invalid_argument("login session not found")); }; @@ -245,7 +245,7 @@ impl ClientMfaServer { // send gateway event debug!("Sending `peer_create` message to gateway"); let device_info = DeviceInfo { - device, + device: device.clone(), network_info: vec![DeviceNetworkInfo { network_id: location.id.expect("Missing location ID"), device_wireguard_ip: network_device.wireguard_ip, @@ -259,6 +259,9 @@ impl ClientMfaServer { Status::internal("unexpected error") })?; + // remove login session from map + self.sessions.remove(&pubkey); + // commit transaction transaction.commit().await.map_err(|_| { error!("Failed to commit transaction"); From f94197732ff30f976c8e2dfc57f7e45b461f847e Mon Sep 17 00:00:00 2001 From: Maciek Date: Tue, 23 Jan 2024 13:21:21 +0100 Subject: [PATCH 24/26] fix: add missing port to location endpoint in desktop client instance update (#514) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add missing port to location endpoint * update env file --------- Co-authored-by: Maciej Wójcik --- .env | 4 ++++ src/grpc/enrollment.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.env b/.env index db64c9e25..344ede6fc 100644 --- a/.env +++ b/.env @@ -11,6 +11,10 @@ DEFGUARD_AUTH_SESSION_LIFETIME=604800 DEFGUARD_ADMIN_GROUPNAME=admin DEFGUARD_DEFAULT_ADMIN_PASSWORD=pass123 +### Proxy configuration ### +# Optional. URL of proxy gRPC server +# DEFGUARD_PROXY_URL: http://localhost:50051 + ### LDAP configuration ### DEFGUARD_LDAP_URL=ldap://localhost:389 DEFGUARD_LDAP_SERVICE_PASSWORD=adminpassword diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index c81e8db86..c5ceee113 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -432,7 +432,7 @@ impl EnrollmentServer { network_id, network_name: network.name, assigned_ip: wireguard_network_device.wireguard_ip.to_string(), - endpoint: network.endpoint, + endpoint: format!("{}:{}", network.endpoint, network.port), pubkey: network.pubkey, allowed_ips, dns: network.dns, From e23b1883b0def7a27ede186cbb37126409ad5894 Mon Sep 17 00:00:00 2001 From: Maciek Date: Tue, 23 Jan 2024 15:33:20 +0100 Subject: [PATCH 25/26] bump version (#515) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Maciej Wójcik --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2fc3b589f..6f23b1d33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -972,7 +972,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "defguard" -version = "0.8.0" +version = "0.9.0" dependencies = [ "anyhow", "argon2", diff --git a/Cargo.toml b/Cargo.toml index 112b97668..295615098 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "defguard" -version = "0.8.0" +version = "0.9.0" edition = "2021" license = "Apache-2.0" homepage = "https://defguard.net/" From 0c15f1c79fbdea40e6d230e83fd666dbda903358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 23 Jan 2024 15:53:03 +0100 Subject: [PATCH 26/26] chore: format e2e tests --- e2e/tests/passwordReset.spec.ts | 16 ++++++++++------ e2e/utils/controllers/passwordReset.ts | 3 +-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/e2e/tests/passwordReset.spec.ts b/e2e/tests/passwordReset.spec.ts index d84a6ec36..e0aaa117d 100644 --- a/e2e/tests/passwordReset.spec.ts +++ b/e2e/tests/passwordReset.spec.ts @@ -1,22 +1,26 @@ import { test } from '@playwright/test'; import { testsConfig, testUserTemplate } from '../config'; +import { User } from '../types'; +import { createUser } from '../utils/controllers/createUser'; import { loginBasic } from '../utils/controllers/login'; +import { logout } from '../utils/controllers/logout'; +import { + selectPasswordReset, + setEmail, + setPassword, +} from '../utils/controllers/passwordReset'; +import { getPasswordResetToken } from '../utils/db/getPasswordResetToken'; import { dockerDown, dockerRestart } from '../utils/docker'; import { waitForBase } from '../utils/waitForBase'; import { waitForPromise } from '../utils/waitForPromise'; -import { selectPasswordReset, setEmail, setPassword } from '../utils/controllers/passwordReset'; -import { getPasswordResetToken } from '../utils/db/getPasswordResetToken'; -import { createUser } from '../utils/controllers/createUser'; -import { logout } from '../utils/controllers/logout'; -import { User } from '../types'; const newPassword = '!7(8o3aN8RoF'; test.describe('Reset password', () => { const user: User = { ...testUserTemplate, username: 'test' }; - test.beforeEach(async ({ browser, page }) => { + test.beforeEach(async ({ browser }) => { dockerRestart(); await createUser(browser, user); }); diff --git a/e2e/utils/controllers/passwordReset.ts b/e2e/utils/controllers/passwordReset.ts index c8b0b7761..6271eb4a3 100644 --- a/e2e/utils/controllers/passwordReset.ts +++ b/e2e/utils/controllers/passwordReset.ts @@ -1,5 +1,4 @@ -import { Page } from "playwright"; - +import { Page } from 'playwright'; export const selectPasswordReset = async (page: Page) => { const selectButton = page.getByTestId('select-password-reset');