From 52d1ed24a9acac569e083718be8540996ee2e244 Mon Sep 17 00:00:00 2001 From: Ginger Date: Mon, 4 May 2026 11:27:47 -0400 Subject: [PATCH] refactor: Remove LDAP support --- Cargo.lock | 124 ------------- Cargo.toml | 5 - src/admin/user/commands.rs | 2 +- src/api/Cargo.toml | 3 - src/api/client/account/register.rs | 2 +- src/api/client/session.rs | 91 +--------- src/core/config/mod.rs | 130 -------------- src/core/error/mod.rs | 2 - src/main/Cargo.toml | 4 - src/service/Cargo.toml | 5 - src/service/admin/create.rs | 2 +- src/service/appservice/mod.rs | 2 +- src/service/emergency/mod.rs | 5 - src/service/password_reset/mod.rs | 11 -- src/service/rooms/state_cache/update.rs | 2 +- src/service/uiaa/mod.rs | 17 -- src/service/users/mod.rs | 224 +----------------------- 17 files changed, 11 insertions(+), 620 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b86d6d33c..03180af91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,45 +240,6 @@ dependencies = [ "winnow 1.0.2", ] -[[package]] -name = "asn1-rs" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" -dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", - "displaydoc", - "nom 7.1.3", - "num-traits", - "rusticata-macros", - "thiserror", - "time", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "assign" version = "1.1.1" @@ -1222,7 +1183,6 @@ dependencies = [ "image", "ipaddress", "itertools 0.14.0", - "ldap3", "lettre", "log", "loole", @@ -1677,20 +1637,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "der-parser" -version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" -dependencies = [ - "asn1-rs", - "displaydoc", - "nom 7.1.3", - "num-bigint", - "num-traits", - "rusticata-macros", -] - [[package]] name = "deranged" version = "0.5.8" @@ -3000,41 +2946,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "lber" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbcf559624bfd9fe8d488329a8959766335a43a9b8b2cdd6a2c379fca02909a5" -dependencies = [ - "bytes", - "nom 7.1.3", -] - -[[package]] -name = "ldap3" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01fe89f5e7cfb7e4701e3a38ff9f00358e026a9aee940355d88ee9d81e5c7503" -dependencies = [ - "async-trait", - "bytes", - "futures", - "futures-util", - "lber", - "log", - "nom 7.1.3", - "percent-encoding", - "rustls", - "rustls-native-certs", - "thiserror", - "tokio", - "tokio-rustls", - "tokio-stream", - "tokio-util", - "url", - "x509-parser", -] - [[package]] name = "leb128fmt" version = "0.1.0" @@ -3753,15 +3664,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "oid-registry" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" -dependencies = [ - "asn1-rs", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -4880,15 +4782,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rusticata-macros" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" -dependencies = [ - "nom 7.1.3", -] - [[package]] name = "rustix" version = "1.1.4" @@ -6922,23 +6815,6 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" -[[package]] -name = "x509-parser" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" -dependencies = [ - "asn1-rs", - "data-encoding", - "der-parser", - "lazy_static", - "nom 7.1.3", - "oid-registry", - "rusticata-macros", - "thiserror", - "time", -] - [[package]] name = "xml5ever" version = "0.18.1" diff --git a/Cargo.toml b/Cargo.toml index d6c579af1..80149b6c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -549,11 +549,6 @@ features = ["std"] [workspace.dependencies.maplit] version = "1.0.2" -[workspace.dependencies.ldap3] -version = "0.12.0" -default-features = false -features = ["sync", "tls-rustls", "rustls-provider"] - [workspace.dependencies.yansi] version = "1.0.1" diff --git a/src/admin/user/commands.rs b/src/admin/user/commands.rs index 411ff5249..ce621dad1 100644 --- a/src/admin/user/commands.rs +++ b/src/admin/user/commands.rs @@ -70,7 +70,7 @@ pub(super) async fn create_user(&self, username: String, password: Option 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 @@ -96,71 +85,6 @@ pub(crate) async fn password_login( 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", level = "debug")] -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()), None), - | _ => { - 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?; - } - - // Only sync admin status if LDAP can actually determine it. - // None means LDAP cannot determine admin status (manual config required). - if let Some(is_ldap_admin) = is_ldap_admin { - let is_conduwuit_admin = services.admin.user_is_admin(lowercased_user_id).await; - - if is_ldap_admin && !is_conduwuit_admin { - Box::pin(services.admin.make_user_admin(lowercased_user_id)).await?; - } else if !is_ldap_admin && is_conduwuit_admin { - Box::pin(services.admin.revoke_admin(lowercased_user_id)).await?; - } - } - - Ok(user_id) -} - pub(crate) async fn handle_login( services: &Services, identifier: Option<&UserIdentifier>, @@ -212,18 +136,7 @@ pub(crate) async fn handle_login( return Err!(Request(Forbidden("This account is not permitted to log in."))); } - if cfg!(feature = "ldap") && services.config.ldap.enable { - match Box::pin(ldap_login(services, &user_id, &lowercased_user_id, password)).await { - | Ok(user_id) => Ok(user_id), - | Err(err) if services.config.ldap.ldap_only => Err(err), - | Err(err) => { - debug_warn!("{err}"); - password_login(services, &user_id, &lowercased_user_id, password).await - }, - } - } else { - password_login(services, &user_id, &lowercased_user_id, password).await - } + password_login(services, &user_id, &lowercased_user_id, password).await } /// # `POST /_matrix/client/v3/login` diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 1943ae022..05fdb001b 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -2130,10 +2130,6 @@ pub struct Config { #[serde(default)] pub allow_web_indexing: bool, - /// display: nested - #[serde(default)] - pub ldap: LdapConfig, - /// Configuration for antispam support /// display: nested #[serde(default)] @@ -2295,126 +2291,6 @@ impl MatrixRtcConfig { } } -#[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, - - /// Whether to force LDAP authentication or authorize classical password - /// login. - /// - /// example: "true" - #[serde(default)] - pub ldap_only: bool, - - /// URI of the LDAP server. - /// - /// example: "ldap://ldap.example.com:389" - /// - /// default: "" - #[serde(default)] - pub uri: Option, - - /// StartTLS for LDAP connections. - /// - /// default: false - #[serde(default)] - pub use_starttls: bool, - - /// Skip TLS certificate verification, possibly dangerous. - /// - /// default: false - #[serde(default)] - pub disable_tls_verification: bool, - - /// Root of the searches. - /// - /// example: "ou=users,dc=example,dc=org" - /// - /// default: "" - #[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" - /// - /// default: "" - #[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. - /// - /// default: "" - #[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 display 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" - /// - /// default: "" - #[serde(default)] - pub admin_base_dn: String, - - /// The LDAP search filter to find administrative users for continuwuity. - /// - /// 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})" - /// - /// default: "" - #[serde(default)] - pub admin_filter: String, -} - #[derive(Deserialize, Clone, Debug)] #[serde(transparent)] struct ListeningPort { @@ -2935,9 +2811,3 @@ 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_name_attribute() -> String { String::from("givenName") } diff --git a/src/core/error/mod.rs b/src/core/error/mod.rs index 54fa18394..63f820254 100644 --- a/src/core/error/mod.rs +++ b/src/core/error/mod.rs @@ -110,8 +110,6 @@ 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/main/Cargo.toml b/src/main/Cargo.toml index d3ae79c30..3a98c9eb5 100644 --- a/src/main/Cargo.toml +++ b/src/main/Cargo.toml @@ -55,7 +55,6 @@ standard = [ "jemalloc", "jemalloc_conf", "journald", - "ldap", "media_thumbnail", "systemd", "url_preview", @@ -126,9 +125,6 @@ jemalloc_stats = [ jemalloc_conf = [ "conduwuit-core/jemalloc_conf", ] -ldap = [ - "conduwuit-api/ldap", -] media_thumbnail = [ "conduwuit-service/media_thumbnail", ] diff --git a/src/service/Cargo.toml b/src/service/Cargo.toml index 0a886d469..5d5148363 100644 --- a/src/service/Cargo.toml +++ b/src/service/Cargo.toml @@ -52,9 +52,6 @@ jemalloc_stats = [ "conduwuit-core/jemalloc_stats", "conduwuit-database/jemalloc_stats", ] -ldap = [ - "dep:ldap3" -] media_thumbnail = [ "dep:image", ] @@ -99,8 +96,6 @@ 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 6e41321ea..15c25c6b5 100644 --- a/src/service/admin/create.rs +++ b/src/service/admin/create.rs @@ -37,7 +37,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, None).await?; + services.users.create(server_user, None).await?; let mut create_content = { use RoomVersionId::*; diff --git a/src/service/appservice/mod.rs b/src/service/appservice/mod.rs index dc2c263fd..c21b6fa05 100644 --- a/src/service/appservice/mod.rs +++ b/src/service/appservice/mod.rs @@ -111,7 +111,7 @@ impl Service { if !self.services.users.exists(&appservice_user_id).await { self.services .users - .create(&appservice_user_id, None, None) + .create(&appservice_user_id, None) .await?; } else if self .services diff --git a/src/service/emergency/mod.rs b/src/service/emergency/mod.rs index 4f99ee9c3..5617ac821 100644 --- a/src/service/emergency/mod.rs +++ b/src/service/emergency/mod.rs @@ -37,11 +37,6 @@ impl crate::Service for Service { } async fn worker(self: Arc) -> Result { - 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}"); }) diff --git a/src/service/password_reset/mod.rs b/src/service/password_reset/mod.rs index d19826dd3..a4a3f44ef 100644 --- a/src/service/password_reset/mod.rs +++ b/src/service/password_reset/mod.rs @@ -58,17 +58,6 @@ impl Service { return Err!("Cannot issue a password reset token for the server user"); } - if self - .services - .users - .origin(&user_id) - .await - .unwrap_or_else(|_| "password".to_owned()) - != "password" - { - return Err!("Cannot issue a password reset token for non-internal user {user_id}"); - } - if self.services.users.is_deactivated(&user_id).await? { return Err!("Cannot issue a password reset token for deactivated user {user_id}"); } diff --git a/src/service/rooms/state_cache/update.rs b/src/service/rooms/state_cache/update.rs index 1e3589811..0103713f4 100644 --- a/src/service/rooms/state_cache/update.rs +++ b/src/service/rooms/state_cache/update.rs @@ -47,7 +47,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, None).await?; + self.services.users.create(user_id, None).await?; } } diff --git a/src/service/uiaa/mod.rs b/src/service/uiaa/mod.rs index b17a84e4d..7ce165dac 100644 --- a/src/service/uiaa/mod.rs +++ b/src/service/uiaa/mod.rs @@ -385,23 +385,6 @@ impl Service { password_verified = hash::verify_password(password, &hash).is_ok(); } - // If local password verification failed, try LDAP authentication - #[cfg(feature = "ldap")] - if !password_verified && self.services.config.ldap.enable { - // Search for user in LDAP to get their DN - if let Ok(dns) = self.services.users.search_ldap(&user_id).await { - if let Some((user_dn, _is_admin)) = dns.first() { - // Try to authenticate with LDAP - password_verified = self - .services - .users - .auth_ldap(user_dn, password) - .await - .is_ok(); - } - } - } - if password_verified { identity.try_set_localpart(user_id.localpart().to_owned())?; diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index 3100f0590..0a95d6108 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -1,21 +1,13 @@ pub(super) mod dehydrated_device; -#[cfg(feature = "ldap")] -use std::collections::HashMap; use std::{collections::BTreeMap, mem, net::IpAddr, sync::Arc}; -#[cfg(feature = "ldap")] -use conduwuit::result::LogErr; use conduwuit::{ - Err, Error, Result, Server, debug_warn, err, is_equal_to, trace, + Err, Error, Result, Server, debug_warn, err, trace, utils::{self, ReadyExt, stream::TryIgnore, string::Unquoted}, }; -#[cfg(feature = "ldap")] -use conduwuit_core::{debug, error}; use database::{Deserialized, Ignore, Interfix, Json, Map}; use futures::{Stream, StreamExt, TryFutureExt}; -#[cfg(feature = "ldap")] -use ldap3::{LdapConnAsync, LdapConnSettings, Scope, SearchEntry}; use ruma::{ DeviceId, KeyId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId, OneTimeKeyName, OwnedDeviceId, OwnedKeyId, OwnedMxcUri, OwnedUserId, RoomId, UInt, UserId, @@ -79,7 +71,6 @@ struct Data { userid_displayname: Arc, userid_lastonetimekeyupdate: Arc, userid_masterkeyid: Arc, - userid_origin: Arc, userid_password: Arc, userid_suspension: Arc, userid_lock: Arc, @@ -120,7 +111,6 @@ 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_lock: args.db["userid_lock"].clone(), @@ -178,26 +168,12 @@ 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 async fn create( - &self, - user_id: &UserId, - password: Option<&str>, - origin: Option<&str>, - ) -> Result<()> { - if !self.services.globals.user_is_local(user_id) - && (password.is_some() || origin.is_some()) - { - return Err!("Cannot create a nonlocal user with a set password or origin"); + pub async fn create(&self, user_id: &UserId, password: Option<&str>) -> Result<()> { + if !self.services.globals.user_is_local(user_id) && password.is_some() { + return Err!("Cannot create a nonlocal user with a set password"); } - self.db - .userid_origin - .insert(user_id, origin.unwrap_or("password")); self.set_password(user_id, password).await?; Ok(()) @@ -360,11 +336,6 @@ impl Service { .ready_filter_map(|(u, p): (OwnedUserId, &[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() @@ -372,22 +343,6 @@ impl Service { /// Hash and set the user's password to the Argon2 hash 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() @@ -1292,177 +1247,6 @@ impl Service { .ready_for_each(|(key, _)| self.set_profile_key(user_id, &key, None)) .await; } - - #[cfg(feature = "ldap")] - async fn create_ldap_connection( - config: &conduwuit_core::config::LdapConfig, - uri: &str, - ) -> Result<(LdapConnAsync, ldap3::Ldap), ldap3::LdapError> { - let mut settings = LdapConnSettings::new(); - - if config.use_starttls { - settings = settings.set_starttls(true); - } - - if config.disable_tls_verification { - settings = settings.set_no_tls_verify(true); - } - - LdapConnAsync::with_settings(settings, uri).await - } - - #[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) = Self::create_ldap_connection(config, 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, None)) - }) - .collect(); - - if !config.admin_filter.is_empty() { - // Update all existing entries to Some(false) since we can now determine admin - // status - for admin_status in dns.values_mut() { - *admin_status = Some(false); - } - 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, Some(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) = Self::create_ldap_connection(config, 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(