From c7adbae03f72250d0fd0ab2ea6ab7911b4369467 Mon Sep 17 00:00:00 2001 From: RatCornu Date: Sat, 9 Aug 2025 15:06:48 +0200 Subject: [PATCH] feat: ldap login --- Cargo.lock | 413 +++++++++++++++++++----- Cargo.toml | 5 + conduwuit-example.toml | 91 ++++++ src/admin/user/commands.rs | 4 +- src/api/Cargo.toml | 3 + src/api/client/account.rs | 5 +- src/api/client/profile.rs | 6 +- src/api/client/session.rs | 211 ++++++++---- src/api/client/unstable.rs | 4 +- src/core/config/mod.rs | 108 +++++++ src/core/error/mod.rs | 2 + src/database/maps.rs | 4 + src/service/Cargo.toml | 5 + src/service/admin/create.rs | 2 +- src/service/emergency/mod.rs | 8 +- src/service/rooms/state_cache/update.rs | 2 +- src/service/users/mod.rs | 203 +++++++++++- 17 files changed, 921 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d7192b6..2b044a1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,7 +126,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -183,7 +183,7 @@ dependencies = [ "rustc-hash 2.1.1", "serde", "serde_derive", - "syn", + "syn 2.0.104", ] [[package]] @@ -198,6 +198,45 @@ dependencies = [ "winnow", ] +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure 0.12.6", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "assign" version = "1.1.1" @@ -250,7 +289,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -261,7 +300,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -433,11 +472,11 @@ dependencies = [ "hyper", "hyper-util", "pin-project-lite", - "rustls", - "rustls-pemfile", + "rustls 0.23.29", + "rustls-pemfile 2.2.0", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tower-service", ] @@ -452,9 +491,9 @@ dependencies = [ "http", "http-body-util", "pin-project", - "rustls", + "rustls 0.23.29", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-util", "tower-layer", "tower-service", @@ -521,7 +560,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn", + "syn 2.0.104", "which", ] @@ -540,7 +579,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn", + "syn 2.0.104", ] [[package]] @@ -794,7 +833,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -972,7 +1011,7 @@ dependencies = [ "rand 0.8.5", "regex", "reqwest", - "ring", + "ring 0.17.14", "ruma", "sanitize-filename", "serde", @@ -1019,7 +1058,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1044,7 +1083,7 @@ dependencies = [ "hyper-util", "log", "ruma", - "rustls", + "rustls 0.23.29", "sd-notify", "sentry", "sentry-tower", @@ -1074,6 +1113,7 @@ dependencies = [ "image", "ipaddress", "itertools 0.14.0", + "ldap3", "log", "loole", "lru-cache", @@ -1183,6 +1223,16 @@ dependencies = [ "crossterm", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -1256,7 +1306,7 @@ dependencies = [ "proc-macro2", "quote", "strict", - "syn", + "syn 2.0.104", ] [[package]] @@ -1366,7 +1416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1393,7 +1443,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1434,6 +1484,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.4.0" @@ -1461,7 +1525,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1483,7 +1547,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1544,7 +1608,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1564,7 +1628,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1734,6 +1798,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1781,7 +1846,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -2030,7 +2095,7 @@ dependencies = [ "ipnet", "once_cell", "rand 0.9.2", - "ring", + "ring 0.17.14", "serde", "thiserror 2.0.12", "tinyvec", @@ -2122,7 +2187,7 @@ dependencies = [ "markup5ever", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -2216,11 +2281,11 @@ dependencies = [ "http", "hyper", "hyper-util", - "rustls", - "rustls-native-certs", + "rustls 0.23.29", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tower-service", "webpki-roots 1.0.2", ] @@ -2444,7 +2509,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -2613,7 +2678,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn", + "syn 2.0.104", ] [[package]] @@ -2628,6 +2693,43 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lber" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2df7f9fd9f64cf8f59e1a4a0753fe7d575a5b38d3d7ac5758dcee9357d83ef0a" +dependencies = [ + "bytes", + "nom", +] + +[[package]] +name = "ldap3" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "166199a8207874a275144c8a94ff6eed5fcbf5c52303e4d9b4d53a0c7ac76554" +dependencies = [ + "async-trait", + "bytes", + "futures", + "futures-util", + "lazy_static", + "lber", + "log", + "nom", + "percent-encoding", + "ring 0.16.20", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "thiserror 1.0.69", + "tokio", + "tokio-rustls 0.24.1", + "tokio-stream", + "tokio-util", + "url", + "x509-parser", +] + [[package]] name = "lebe" version = "0.5.2" @@ -2866,7 +2968,7 @@ checksum = "a9882ef5c56df184b8ffc107fc6c61e33ee3a654b021961d790a78571bb9d67a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3032,7 +3134,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3094,6 +3196,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -3284,7 +3395,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3358,7 +3469,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3458,7 +3569,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.104", ] [[package]] @@ -3487,7 +3598,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", "version_check", "yansi", ] @@ -3508,7 +3619,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3531,7 +3642,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3597,7 +3708,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls", + "rustls 0.23.29", "socket2", "thiserror 2.0.12", "tokio", @@ -3615,9 +3726,9 @@ dependencies = [ "getrandom 0.3.3", "lru-slab", "rand 0.9.2", - "ring", + "ring 0.17.14", "rustc-hash 2.1.1", - "rustls", + "rustls 0.23.29", "rustls-pki-types", "slab", "thiserror 2.0.12", @@ -3876,16 +3987,16 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls", - "rustls-native-certs", - "rustls-pemfile", + "rustls 0.23.29", + "rustls-native-certs 0.8.1", + "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-socks", "tokio-util", "tower 0.5.2", @@ -3909,6 +4020,21 @@ version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + [[package]] name = "ring" version = "0.17.14" @@ -3919,7 +4045,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.16", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -4093,7 +4219,7 @@ dependencies = [ "quote", "ruma-identifiers-validation", "serde", - "syn", + "syn 2.0.104", "toml", ] @@ -4178,6 +4304,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.38.44" @@ -4204,6 +4339,18 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.14", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.23.29" @@ -4213,13 +4360,25 @@ dependencies = [ "aws-lc-rs", "log", "once_cell", - "ring", + "ring 0.17.14", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.4", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework 2.11.1", +] + [[package]] name = "rustls-native-certs" version = "0.8.1" @@ -4229,7 +4388,16 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.2.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", ] [[package]] @@ -4251,6 +4419,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + [[package]] name = "rustls-webpki" version = "0.103.4" @@ -4258,9 +4436,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "aws-lc-rs", - "ring", + "ring 0.17.14", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -4319,6 +4497,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + [[package]] name = "sd-notify" version = "0.4.5" @@ -4328,6 +4516,19 @@ dependencies = [ "libc", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.2.0" @@ -4335,7 +4536,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ "bitflags 2.9.1", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -4365,7 +4566,7 @@ checksum = "255914a8e53822abd946e2ce8baa41d4cded6b8e938913b7f7b9da5b7ab44335" dependencies = [ "httpdate", "reqwest", - "rustls", + "rustls 0.23.29", "sentry-backtrace", "sentry-contexts", "sentry-core", @@ -4509,7 +4710,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -4723,6 +4924,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spki" version = "0.7.3" @@ -4791,6 +4998,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.104" @@ -4811,6 +5029,18 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -4819,7 +5049,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -4910,7 +5140,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -4921,7 +5151,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -5088,7 +5318,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -5103,13 +5333,23 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls", + "rustls 0.23.29", "tokio", ] @@ -5307,7 +5547,7 @@ source = "git+https://forgejo.ellis.link/continuwuation/tracing?rev=1e64095a8051 dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -5461,12 +5701,24 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -5482,7 +5734,7 @@ dependencies = [ "base64 0.22.1", "log", "once_cell", - "rustls", + "rustls 0.23.29", "rustls-pki-types", "url", "webpki-roots 0.26.11", @@ -5617,7 +5869,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -5652,7 +5904,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5832,7 +6084,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -5843,7 +6095,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -6164,6 +6416,23 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "x509-parser" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "xml5ever" version = "0.18.1" @@ -6221,8 +6490,8 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn", - "synstructure", + "syn 2.0.104", + "synstructure 0.13.2", ] [[package]] @@ -6242,7 +6511,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -6262,8 +6531,8 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", - "synstructure", + "syn 2.0.104", + "synstructure 0.13.2", ] [[package]] @@ -6302,7 +6571,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 04ff4bb7..9452066c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -546,6 +546,11 @@ features = ["std"] [workspace.dependencies.maplit] version = "1.0.2" +[workspace.dependencies.ldap3] +version = "0.11.5" +default-features = false +features = ["sync", "tls-rustls"] + # # Patches # diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 541050b1..47a9da19 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -1696,6 +1696,10 @@ # #config_reload_signal = true +# This item is undocumented. Please contribute documentation for it. +# +#ldap = false + [global.tls] # Path to a valid TLS certificate file. @@ -1774,3 +1778,90 @@ # is 33.55MB. Setting it to 0 disables blurhashing. # #blurhash_max_raw_size = 33554432 + +[global.ldap] + +# Whether to enable LDAP login. +# +# example: "true" +# +#enable = false + +# URI of the LDAP server. +# +# example: "ldap://ldap.example.com:389" +# +#uri = + +# Root of the searches. +# +# example: "ou=users,dc=example,dc=org" +# +#base_dn = false + +# Bind DN if anonymous search is not enabled. +# +# You can use the variable `{username}` that will be replaced by the +# entered username. In such case, the password used to bind will be the +# one provided for the login and not the one given by +# `bind_password_file`. Beware: automatically granting admin rights will +# not work if you use this direct bind instead of a LDAP search. +# +# example: "cn=ldap-reader,dc=example,dc=org" or +# "cn={username},ou=users,dc=example,dc=org" +# +#bind_dn = false + +# Path to a file on the system that contains the password for the +# `bind_dn`. +# +# The server must be able to access the file, and it must not be empty. +# +#bind_password_file = false + +# Search filter to limit user searches. +# +# You can use the variable `{username}` that will be replaced by the +# entered username for more complex filters. +# +# example: "(&(objectClass=person)(memberOf=matrix))" +# +#filter = "(objectClass=*)" + +# Attribute to use to uniquely identify the user. +# +# example: "uid" or "cn" +# +#uid_attribute = "uid" + +# Attribute containing the mail of the user. +# +# example: "mail" +# +#mail_attribute = "mail" + +# Attribute containing the distinguished name of the user. +# +# example: "givenName" or "sn" +# +#name_attribute = "givenName" + +# Root of the searches for admin users. +# +# Defaults to `base_dn` if empty. +# +# example: "ou=admins,dc=example,dc=org" +# +#admin_base_dn = false + +# The LDAP search filter to find administrative users for conduwuit. +# +# If left blank, administrative state must be configured manually for each +# user. +# +# You can use the variable `{username}` that will be replaced by the +# entered username for more complex filters. +# +# example: "(objectClass=conduwuitAdmin)" or "(uid={username})" +# +#admin_filter = false diff --git a/src/admin/user/commands.rs b/src/admin/user/commands.rs index 86206c2b..56864a32 100644 --- a/src/admin/user/commands.rs +++ b/src/admin/user/commands.rs @@ -68,7 +68,8 @@ pub(super) async fn create_user(&self, username: String, password: Option return Err!("Couldn't reset the password for user {user_id}: {e}"), | Ok(()) => { diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index 15ada812..9b4ea460 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -49,6 +49,9 @@ jemalloc_stats = [ "conduwuit-core/jemalloc_stats", "conduwuit-service/jemalloc_stats", ] +ldap = [ + "conduwuit-service/ldap" +] release_max_log_level = [ "conduwuit-core/release_max_log_level", "conduwuit-service/release_max_log_level", diff --git a/src/api/client/account.rs b/src/api/client/account.rs index 0cea7bd9..67268c9f 100644 --- a/src/api/client/account.rs +++ b/src/api/client/account.rs @@ -373,7 +373,7 @@ pub(crate) async fn register_route( let password = if is_guest { None } else { body.password.as_deref() }; // Create user - services.users.create(&user_id, password)?; + services.users.create(&user_id, password, None).await?; // Default to pretty displayname let mut displayname = user_id.localpart().to_owned(); @@ -659,7 +659,8 @@ pub(crate) async fn change_password_route( services .users - .set_password(sender_user, Some(&body.new_password))?; + .set_password(sender_user, Some(&body.new_password)) + .await?; if body.logout_devices { // Logout all devices except the current one diff --git a/src/api/client/profile.rs b/src/api/client/profile.rs index 1882495c..eaa66e70 100644 --- a/src/api/client/profile.rs +++ b/src/api/client/profile.rs @@ -90,7 +90,7 @@ pub(crate) async fn get_displayname_route( .await { if !services.users.exists(&body.user_id).await { - services.users.create(&body.user_id, None)?; + services.users.create(&body.user_id, None, None).await?; } services @@ -189,7 +189,7 @@ pub(crate) async fn get_avatar_url_route( .await { if !services.users.exists(&body.user_id).await { - services.users.create(&body.user_id, None)?; + services.users.create(&body.user_id, None, None).await?; } services @@ -248,7 +248,7 @@ pub(crate) async fn get_profile_route( .await { if !services.users.exists(&body.user_id).await { - services.users.create(&body.user_id, None)?; + services.users.create(&body.user_id, None, None).await?; } services diff --git a/src/api/client/session.rs b/src/api/client/session.rs index fe07e41d..2f066d58 100644 --- a/src/api/client/session.rs +++ b/src/api/client/session.rs @@ -6,10 +6,11 @@ use conduwuit::{ Err, Error, Result, debug, err, info, utils, utils::{ReadyExt, hash}, }; -use conduwuit_service::uiaa::SESSION_ID_LENGTH; +use conduwuit_core::debug_error; +use conduwuit_service::{Services, uiaa::SESSION_ID_LENGTH}; use futures::StreamExt; use ruma::{ - UserId, + OwnedUserId, UserId, api::client::{ session::{ get_login_token, @@ -49,6 +50,147 @@ pub(crate) async fn get_login_types_route( ])) } +/// Authenticates the given user by its ID and its password. +/// +/// Returns the user ID if successful, and an error otherwise. +#[tracing::instrument(skip_all, fields(%user_id), name = "password")] +pub(crate) async fn password_login( + services: &Services, + user_id: &UserId, + lowercased_user_id: &UserId, + password: &str, +) -> Result { + // Restrict login to accounts only of type 'password', including untyped + // legacy accounts which are equivalent to 'password'. + if services + .users + .origin(user_id) + .await + .is_ok_and(|origin| origin != "password") + { + return Err!(Request(Forbidden("Account does not permit password login."))); + } + + let (hash, user_id) = match services.users.password_hash(user_id).await { + | Ok(hash) => (hash, user_id), + | Err(_) => services + .users + .password_hash(lowercased_user_id) + .await + .map(|hash| (hash, lowercased_user_id)) + .map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?, + }; + + if hash.is_empty() { + return Err!(Request(UserDeactivated("The user has been deactivated"))); + } + + hash::verify_password(password, &hash) + .inspect_err(|e| debug_error!("{e}")) + .map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?; + + Ok(user_id.to_owned()) +} + +/// Authenticates the given user through the configured LDAP server. +/// +/// Creates the user if the user is found in the LDAP and do not already have an +/// account. +#[tracing::instrument(skip_all, fields(%user_id), name = "ldap")] +pub(super) async fn ldap_login( + services: &Services, + user_id: &UserId, + lowercased_user_id: &UserId, + password: &str, +) -> Result { + let (user_dn, is_ldap_admin) = match services.config.ldap.bind_dn.as_ref() { + | Some(bind_dn) if bind_dn.contains("{username}") => + (bind_dn.replace("{username}", lowercased_user_id.localpart()), false), + | _ => { + debug!("Searching user in LDAP"); + + let dns = services.users.search_ldap(user_id).await?; + if dns.len() >= 2 { + return Err!(Ldap("LDAP search returned two or more results")); + } + + let Some((user_dn, is_admin)) = dns.first() else { + return password_login(services, user_id, lowercased_user_id, password).await; + }; + + (user_dn.clone(), *is_admin) + }, + }; + + let user_id = services + .users + .auth_ldap(&user_dn, password) + .await + .map(|()| lowercased_user_id.to_owned())?; + + // LDAP users are automatically created on first login attempt. This is a very + // common feature that can be seen on many services using a LDAP provider for + // their users (synapse, Nextcloud, Jellyfin, ...). + // + // LDAP users are crated with a dummy password but non empty because an empty + // password is reserved for deactivated accounts. The conduwuit password field + // will never be read to login a LDAP user so it's not an issue. + if !services.users.exists(lowercased_user_id).await { + services + .users + .create(lowercased_user_id, Some("*"), Some("ldap")) + .await?; + } + + let is_conduwuit_admin = services.admin.user_is_admin(lowercased_user_id).await; + + if is_ldap_admin && !is_conduwuit_admin { + services.admin.make_user_admin(lowercased_user_id).await?; + } else if !is_ldap_admin && is_conduwuit_admin { + services.admin.revoke_admin(lowercased_user_id).await?; + } + + Ok(user_id) +} + +pub(crate) async fn handle_login( + services: &Services, + body: &Ruma, + identifier: &Option, + password: &str, + user: &Option, +) -> Result { + debug!("Got password login type"); + let user_id = + if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier { + UserId::parse_with_server_name(user_id, &services.config.server_name) + } else if let Some(user) = user { + UserId::parse_with_server_name(user, &services.config.server_name) + } else { + return Err!(Request(Unknown( + debug_warn!(?body.login_info, "Valid identifier or username was not provided (invalid or unsupported login type?)") + ))); + } + .map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?; + + let lowercased_user_id = UserId::parse_with_server_name( + user_id.localpart().to_lowercase(), + &services.config.server_name, + )?; + + if !services.globals.user_is_local(&user_id) + || !services.globals.user_is_local(&lowercased_user_id) + { + return Err!(Request(Unknown("User ID does not belong to this homeserver"))); + } + + if cfg!(feature = "ldap") && services.config.ldap.enable { + ldap_login(services, &user_id, &lowercased_user_id, password).await + } else { + password_login(services, &user_id, &lowercased_user_id, password).await + } +} + /// # `POST /_matrix/client/v3/login` /// /// Authenticates the user and returns an access token it can use in subsequent @@ -80,70 +222,7 @@ pub(crate) async fn login_route( password, user, .. - }) => { - debug!("Got password login type"); - let user_id = - if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier { - UserId::parse_with_server_name(user_id, &services.config.server_name) - } else if let Some(user) = user { - UserId::parse_with_server_name(user, &services.config.server_name) - } else { - return Err!(Request(Unknown( - debug_warn!(?body.login_info, "Valid identifier or username was not provided (invalid or unsupported login type?)") - ))); - } - .map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?; - - let lowercased_user_id = UserId::parse_with_server_name( - user_id.localpart().to_lowercase(), - &services.config.server_name, - )?; - - if !services.globals.user_is_local(&user_id) - || !services.globals.user_is_local(&lowercased_user_id) - { - return Err!(Request(Unknown("User ID does not belong to this homeserver"))); - } - - // first try the username as-is - let hash = services - .users - .password_hash(&user_id) - .await - .inspect_err(|e| debug!("{e}")); - - match hash { - | Ok(hash) => { - if hash.is_empty() { - return Err!(Request(UserDeactivated("The user has been deactivated"))); - } - - hash::verify_password(password, &hash) - .inspect_err(|e| debug!("{e}")) - .map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?; - - user_id - }, - | Err(_e) => { - let hash_lowercased_user_id = services - .users - .password_hash(&lowercased_user_id) - .await - .inspect_err(|e| debug!("{e}")) - .map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?; - - if hash_lowercased_user_id.is_empty() { - return Err!(Request(UserDeactivated("The user has been deactivated"))); - } - - hash::verify_password(password, &hash_lowercased_user_id) - .inspect_err(|e| debug!("{e}")) - .map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?; - - lowercased_user_id - }, - } - }, + }) => handle_login(&services, &body, identifier, password, user).await?, | login::v3::LoginInfo::Token(login::v3::Token { token }) => { debug!("Got token login type"); if !services.server.config.login_via_existing_session { diff --git a/src/api/client/unstable.rs b/src/api/client/unstable.rs index 08f70975..f8703ff3 100644 --- a/src/api/client/unstable.rs +++ b/src/api/client/unstable.rs @@ -292,7 +292,7 @@ pub(crate) async fn get_timezone_key_route( .await { if !services.users.exists(&body.user_id).await { - services.users.create(&body.user_id, None)?; + services.users.create(&body.user_id, None, None).await?; } services @@ -352,7 +352,7 @@ pub(crate) async fn get_profile_key_route( .await { if !services.users.exists(&body.user_id).await { - services.users.create(&body.user_id, None)?; + services.users.create(&body.user_id, None, None).await?; } services diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 0708196d..a7fbc7dc 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -1948,6 +1948,10 @@ pub struct Config { pub allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure: bool, + // external structure; separate section + #[serde(default)] + pub ldap: LdapConfig, + // external structure; separate section #[serde(default)] pub blurhashing: BlurhashConfig, @@ -2042,6 +2046,102 @@ pub struct BlurhashConfig { pub blurhash_max_raw_size: u64, } +#[derive(Clone, Debug, Default, Deserialize)] +#[config_example_generator(filename = "conduwuit-example.toml", section = "global.ldap")] +pub struct LdapConfig { + /// Whether to enable LDAP login. + /// + /// example: "true" + #[serde(default)] + pub enable: bool, + + /// URI of the LDAP server. + /// + /// example: "ldap://ldap.example.com:389" + pub uri: Option, + + /// Root of the searches. + /// + /// example: "ou=users,dc=example,dc=org" + #[serde(default)] + pub base_dn: String, + + /// Bind DN if anonymous search is not enabled. + /// + /// You can use the variable `{username}` that will be replaced by the + /// entered username. In such case, the password used to bind will be the + /// one provided for the login and not the one given by + /// `bind_password_file`. Beware: automatically granting admin rights will + /// not work if you use this direct bind instead of a LDAP search. + /// + /// example: "cn=ldap-reader,dc=example,dc=org" or + /// "cn={username},ou=users,dc=example,dc=org" + #[serde(default)] + pub bind_dn: Option, + + /// Path to a file on the system that contains the password for the + /// `bind_dn`. + /// + /// The server must be able to access the file, and it must not be empty. + #[serde(default)] + pub bind_password_file: Option, + + /// Search filter to limit user searches. + /// + /// You can use the variable `{username}` that will be replaced by the + /// entered username for more complex filters. + /// + /// example: "(&(objectClass=person)(memberOf=matrix))" + /// + /// default: "(objectClass=*)" + #[serde(default = "default_ldap_search_filter")] + pub filter: String, + + /// Attribute to use to uniquely identify the user. + /// + /// example: "uid" or "cn" + /// + /// default: "uid" + #[serde(default = "default_ldap_uid_attribute")] + pub uid_attribute: String, + + /// Attribute containing the mail of the user. + /// + /// example: "mail" + /// + /// default: "mail" + #[serde(default = "default_ldap_mail_attribute")] + pub mail_attribute: String, + + /// Attribute containing the distinguished name of the user. + /// + /// example: "givenName" or "sn" + /// + /// default: "givenName" + #[serde(default = "default_ldap_name_attribute")] + pub name_attribute: String, + + /// Root of the searches for admin users. + /// + /// Defaults to `base_dn` if empty. + /// + /// example: "ou=admins,dc=example,dc=org" + #[serde(default)] + pub admin_base_dn: String, + + /// The LDAP search filter to find administrative users for conduwuit. + /// + /// If left blank, administrative state must be configured manually for each + /// user. + /// + /// You can use the variable `{username}` that will be replaced by the + /// entered username for more complex filters. + /// + /// example: "(objectClass=conduwuitAdmin)" or "(uid={username})" + #[serde(default)] + pub admin_filter: String, +} + #[derive(Deserialize, Clone, Debug)] #[serde(transparent)] struct ListeningPort { @@ -2431,3 +2531,11 @@ pub(super) fn default_blurhash_x_component() -> u32 { 4 } pub(super) fn default_blurhash_y_component() -> u32 { 3 } // end recommended & blurhashing defaults + +fn default_ldap_search_filter() -> String { "(objectClass=*)".to_owned() } + +fn default_ldap_uid_attribute() -> String { String::from("uid") } + +fn default_ldap_mail_attribute() -> String { String::from("mail") } + +fn default_ldap_name_attribute() -> String { String::from("givenName") } diff --git a/src/core/error/mod.rs b/src/core/error/mod.rs index e46edf09..541af793 100644 --- a/src/core/error/mod.rs +++ b/src/core/error/mod.rs @@ -110,6 +110,8 @@ pub enum Error { InconsistentRoomState(&'static str, ruma::OwnedRoomId), #[error(transparent)] IntoHttp(#[from] ruma::api::error::IntoHttpError), + #[error("{0}")] + Ldap(Cow<'static, str>), #[error(transparent)] Mxc(#[from] ruma::MxcUriError), #[error(transparent)] diff --git a/src/database/maps.rs b/src/database/maps.rs index 214dbf34..da97ef45 100644 --- a/src/database/maps.rs +++ b/src/database/maps.rs @@ -374,6 +374,10 @@ pub(super) static MAPS: &[Descriptor] = &[ name: "userid_masterkeyid", ..descriptor::RANDOM_SMALL }, + Descriptor { + name: "userid_origin", + ..descriptor::RANDOM + }, Descriptor { name: "userid_password", ..descriptor::RANDOM diff --git a/src/service/Cargo.toml b/src/service/Cargo.toml index fdebd1d7..6e538f40 100644 --- a/src/service/Cargo.toml +++ b/src/service/Cargo.toml @@ -53,6 +53,9 @@ jemalloc_stats = [ "conduwuit-core/jemalloc_stats", "conduwuit-database/jemalloc_stats", ] +ldap = [ + "dep:ldap3" +] media_thumbnail = [ "dep:image", ] @@ -89,6 +92,8 @@ image.workspace = true image.optional = true ipaddress.workspace = true itertools.workspace = true +ldap3.workspace = true +ldap3.optional = true log.workspace = true loole.workspace = true lru-cache.workspace = true diff --git a/src/service/admin/create.rs b/src/service/admin/create.rs index 157b4d65..755673fe 100644 --- a/src/service/admin/create.rs +++ b/src/service/admin/create.rs @@ -38,7 +38,7 @@ pub async fn create_admin_room(services: &Services) -> Result { // Create a user for the server let server_user = services.globals.server_user.as_ref(); - services.users.create(server_user, None)?; + services.users.create(server_user, None, None).await?; let create_content = { use RoomVersionId::*; diff --git a/src/service/emergency/mod.rs b/src/service/emergency/mod.rs index 3a61f710..f8ecbb3e 100644 --- a/src/service/emergency/mod.rs +++ b/src/service/emergency/mod.rs @@ -41,6 +41,11 @@ impl crate::Service for Service { return Ok(()); } + if self.services.config.ldap.enable { + warn!("emergency password feature not available with LDAP enabled."); + return Ok(()); + } + self.set_emergency_access().await.inspect_err(|e| { error!("Could not set the configured emergency password for the server user: {e}"); }) @@ -57,7 +62,8 @@ impl Service { self.services .users - .set_password(server_user, self.services.config.emergency_password.as_deref())?; + .set_password(server_user, self.services.config.emergency_password.as_deref()) + .await?; let (ruleset, pwd_set) = match self.services.config.emergency_password { | Some(_) => (Ruleset::server_default(server_user), true), diff --git a/src/service/rooms/state_cache/update.rs b/src/service/rooms/state_cache/update.rs index 32c67947..86c1afe7 100644 --- a/src/service/rooms/state_cache/update.rs +++ b/src/service/rooms/state_cache/update.rs @@ -49,7 +49,7 @@ pub async fn update_membership( #[allow(clippy::collapsible_if)] if !self.services.globals.user_is_local(user_id) { if !self.services.users.exists(user_id).await { - self.services.users.create(user_id, None)?; + self.services.users.create(user_id, None, None).await?; } } diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index eb54660e..35202ec7 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -1,11 +1,19 @@ -use std::{collections::BTreeMap, mem, sync::Arc}; +use std::{ + collections::{BTreeMap, HashMap}, + mem, + sync::Arc, +}; use conduwuit::{ - Err, Error, Result, Server, at, debug_warn, err, trace, + Err, Error, Result, Server, at, debug_warn, err, is_equal_to, + result::LogErr, + trace, utils::{self, ReadyExt, stream::TryIgnore, string::Unquoted}, }; +use conduwuit_core::{debug, error}; use database::{Deserialized, Ignore, Interfix, Json, Map}; use futures::{Stream, StreamExt, TryFutureExt}; +use ldap3::{LdapConnAsync, Scope, SearchEntry}; use ruma::{ DeviceId, KeyId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId, OneTimeKeyName, OwnedDeviceId, OwnedKeyId, OwnedMxcUri, OwnedUserId, RoomId, UInt, UserId, @@ -63,6 +71,7 @@ struct Data { userid_displayname: Arc, userid_lastonetimekeyupdate: Arc, userid_masterkeyid: Arc, + userid_origin: Arc, userid_password: Arc, userid_suspension: Arc, userid_selfsigningkeyid: Arc, @@ -100,6 +109,7 @@ impl crate::Service for Service { userid_displayname: args.db["userid_displayname"].clone(), userid_lastonetimekeyupdate: args.db["userid_lastonetimekeyupdate"].clone(), userid_masterkeyid: args.db["userid_masterkeyid"].clone(), + userid_origin: args.db["userid_origin"].clone(), userid_password: args.db["userid_password"].clone(), userid_suspension: args.db["userid_suspension"].clone(), userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(), @@ -136,9 +146,21 @@ impl Service { } /// Create a new user account on this homeserver. + /// + /// User origin is by default "password" (meaning that it will login using + /// its user_id/password). Users with other origins (currently only "ldap" + /// is available) have special login processes. #[inline] - pub fn create(&self, user_id: &UserId, password: Option<&str>) -> Result<()> { - self.set_password(user_id, password) + pub async fn create( + &self, + user_id: &UserId, + password: Option<&str>, + origin: Option<&str>, + ) -> Result<()> { + self.db + .userid_origin + .insert(user_id, origin.unwrap_or("password")); + self.set_password(user_id, password).await } /// Deactivate account @@ -152,7 +174,7 @@ impl Service { // result in an empty string, so the user will not be able to log in again. // Systems like changing the password without logging in should check if the // account is deactivated. - self.set_password(user_id, None)?; + self.set_password(user_id, None).await?; // TODO: Unhook 3PID Ok(()) @@ -253,13 +275,34 @@ impl Service { .ready_filter_map(|(u, p): (&UserId, &[u8])| (!p.is_empty()).then_some(u)) } + /// Returns the origin of the user (password/LDAP/...). + pub async fn origin(&self, user_id: &UserId) -> Result { + self.db.userid_origin.get(user_id).await.deserialized() + } + /// Returns the password hash for the given user. pub async fn password_hash(&self, user_id: &UserId) -> Result { self.db.userid_password.get(user_id).await.deserialized() } /// Hash and set the user's password to the Argon2 hash - pub fn set_password(&self, user_id: &UserId, password: Option<&str>) -> Result<()> { + pub async fn set_password(&self, user_id: &UserId, password: Option<&str>) -> Result<()> { + // Cannot change the password of a LDAP user. There are two special cases : + // - a `None` password can be used to deactivate a LDAP user + // - a "*" password is used as the default password of an active LDAP user + if cfg!(feature = "ldap") + && password.is_some_and(|pwd| pwd != "*") + && self + .db + .userid_origin + .get(user_id) + .await + .deserialized::() + .is_ok_and(is_equal_to!("ldap")) + { + return Err!(Request(InvalidParam("Cannot change password of a LDAP user"))); + } + password .map(utils::hash::password) .transpose() @@ -1132,6 +1175,154 @@ impl Service { self.db.useridprofilekey_value.del(key); } } + + #[cfg(not(feature = "ldap"))] + pub async fn search_ldap(&self, _user_id: &UserId) -> Result> { + Err!(FeatureDisabled("ldap")) + } + + #[cfg(feature = "ldap")] + pub async fn search_ldap(&self, user_id: &UserId) -> Result> { + let localpart = user_id.localpart().to_owned(); + let lowercased_localpart = localpart.to_lowercase(); + + let config = &self.services.server.config.ldap; + let uri = config + .uri + .as_ref() + .ok_or_else(|| err!(Ldap(error!("LDAP URI is not configured."))))?; + + debug!(?uri, "LDAP creating connection..."); + let (conn, mut ldap) = LdapConnAsync::new(uri.as_str()) + .await + .map_err(|e| err!(Ldap(error!(?user_id, "LDAP connection setup error: {e}"))))?; + + let driver = self.services.server.runtime().spawn(async move { + match conn.drive().await { + | Err(e) => error!("LDAP connection error: {e}"), + | Ok(()) => debug!("LDAP connection completed."), + } + }); + + match (&config.bind_dn, &config.bind_password_file) { + | (Some(bind_dn), Some(bind_password_file)) => { + let bind_pw = String::from_utf8(std::fs::read(bind_password_file)?)?; + ldap.simple_bind(bind_dn, bind_pw.trim()) + .await + .and_then(ldap3::LdapResult::success) + .map_err(|e| err!(Ldap(error!("LDAP bind error: {e}"))))?; + }, + | (..) => {}, + } + + let attr = [&config.uid_attribute, &config.name_attribute]; + + let user_filter = &config.filter.replace("{username}", &lowercased_localpart); + + let (entries, _result) = ldap + .search(&config.base_dn, Scope::Subtree, user_filter, &attr) + .await + .and_then(ldap3::SearchResult::success) + .inspect(|(entries, result)| trace!(?entries, ?result, "LDAP Search")) + .map_err(|e| err!(Ldap(error!(?attr, ?user_filter, "LDAP search error: {e}"))))?; + + let mut dns: HashMap = entries + .into_iter() + .filter_map(|entry| { + let search_entry = SearchEntry::construct(entry); + debug!(?search_entry, "LDAP search entry"); + search_entry + .attrs + .get(&config.uid_attribute) + .into_iter() + .chain(search_entry.attrs.get(&config.name_attribute)) + .any(|ids| ids.contains(&localpart) || ids.contains(&lowercased_localpart)) + .then_some((search_entry.dn, false)) + }) + .collect(); + + if !config.admin_filter.is_empty() { + let admin_base_dn = if config.admin_base_dn.is_empty() { + &config.base_dn + } else { + &config.admin_base_dn + }; + + let admin_filter = &config + .admin_filter + .replace("{username}", &lowercased_localpart); + + let (admin_entries, _result) = ldap + .search(admin_base_dn, Scope::Subtree, admin_filter, &attr) + .await + .and_then(ldap3::SearchResult::success) + .inspect(|(entries, result)| trace!(?entries, ?result, "LDAP Admin Search")) + .map_err(|e| { + err!(Ldap(error!(?attr, ?admin_filter, "Ldap admin search error: {e}"))) + })?; + + dns.extend(admin_entries.into_iter().filter_map(|entry| { + let search_entry = SearchEntry::construct(entry); + debug!(?search_entry, "LDAP search entry"); + search_entry + .attrs + .get(&config.uid_attribute) + .into_iter() + .chain(search_entry.attrs.get(&config.name_attribute)) + .any(|ids| ids.contains(&localpart) || ids.contains(&lowercased_localpart)) + .then_some((search_entry.dn, true)) + })); + } + + ldap.unbind() + .await + .map_err(|e| err!(Ldap(error!("LDAP unbind error: {e}"))))?; + + driver.await.log_err().ok(); + + Ok(dns.drain().collect()) + } + + #[cfg(not(feature = "ldap"))] + pub async fn auth_ldap(&self, _user_dn: &str, _password: &str) -> Result { + Err!(FeatureDisabled("ldap")) + } + + #[cfg(feature = "ldap")] + pub async fn auth_ldap(&self, user_dn: &str, password: &str) -> Result { + let config = &self.services.server.config.ldap; + let uri = config + .uri + .as_ref() + .ok_or_else(|| err!(Ldap(error!("LDAP URI is not configured."))))?; + + debug!(?uri, "LDAP creating connection..."); + let (conn, mut ldap) = LdapConnAsync::new(uri.as_str()) + .await + .map_err(|e| err!(Ldap(error!(?user_dn, "LDAP connection setup error: {e}"))))?; + + let driver = self.services.server.runtime().spawn(async move { + match conn.drive().await { + | Err(e) => error!("LDAP connection error: {e}"), + | Ok(()) => debug!("LDAP connection completed."), + } + }); + + ldap.simple_bind(user_dn, password) + .await + .and_then(ldap3::LdapResult::success) + .map_err(|e| { + err!(Request(Forbidden(debug_error!("LDAP authentication error: {e}")))) + })?; + + ldap.unbind() + .await + .map_err(|e| err!(Ldap(error!("LDAP unbind error: {e}"))))?; + + driver.await.log_err().ok(); + + Ok(()) + } } pub fn parse_master_key(