diff --git a/Cargo.lock b/Cargo.lock index 64698c3..66fbf54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -301,6 +301,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -387,6 +393,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -719,10 +735,10 @@ dependencies = [ "hex", "http 1.1.0", "indoc", - "ldap-poller", + "itertools 0.14.0", "ldap3", "native-tls", - "reqwest 0.11.27", + "reqwest 0.12.8", "serde", "serde_json", "serde_yaml", @@ -1224,6 +1240,7 @@ dependencies = [ "hyper 1.4.1", "hyper-util", "rustls 0.23.14", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", @@ -1242,19 +1259,6 @@ dependencies = [ "tokio-io-timeout", ] -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper 0.14.30", - "native-tls", - "tokio", - "tokio-native-tls", -] - [[package]] name = "hyper-tls" version = "0.6.0" @@ -1396,6 +1400,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -1456,22 +1469,6 @@ dependencies = [ "nom", ] -[[package]] -name = "ldap-poller" -version = "0.1.0" -source = "git+https://github.com/famedly/ldap-poller#96dfa724a618b1f0f6caec83f4e261366d2e84e1" -dependencies = [ - "ldap3", - "native-tls", - "rustls 0.21.12", - "serde", - "thiserror", - "time", - "tokio", - "tracing", - "url", -] - [[package]] name = "ldap3" version = "0.11.5" @@ -1520,16 +1517,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - [[package]] name = "log" version = "0.4.22" @@ -1608,7 +1595,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -1875,29 +1862,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.6", -] - [[package]] name = "pathdiff" version = "0.2.2" @@ -2186,6 +2150,55 @@ dependencies = [ "ipnet", ] +[[package]] +name = "quinn" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.14", + "socket2", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +dependencies = [ + "bytes", + "rand", + "ring 0.17.8", + "rustc-hash", + "rustls 0.23.14", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.37" @@ -2225,15 +2238,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "redox_syscall" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" -dependencies = [ - "bitflags 2.6.0", -] - [[package]] name = "regex" version = "1.11.0" @@ -2294,17 +2298,14 @@ dependencies = [ "http-body 0.4.6", "hyper 0.14.30", "hyper-rustls 0.24.2", - "hyper-tls 0.5.0", "ipnet", "js-sys", "log", "mime", - "native-tls", "once_cell", "percent-encoding", "pin-project-lite", "rustls 0.21.12", - "rustls-native-certs 0.6.3", "rustls-pemfile 1.0.4", "serde", "serde_json", @@ -2312,7 +2313,6 @@ dependencies = [ "sync_wrapper 0.1.2", "system-configuration 0.5.1", "tokio", - "tokio-native-tls", "tokio-rustls 0.24.1", "tower-service", "url", @@ -2340,7 +2340,7 @@ dependencies = [ "http-body-util", "hyper 1.4.1", "hyper-rustls 0.27.3", - "hyper-tls 0.6.0", + "hyper-tls", "hyper-util", "ipnet", "js-sys", @@ -2350,7 +2350,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls 0.23.14", + "rustls-native-certs 0.8.1", "rustls-pemfile 2.2.0", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", @@ -2358,6 +2362,7 @@ dependencies = [ "system-configuration 0.6.1", "tokio", "tokio-native-tls", + "tokio-rustls 0.26.0", "tower-service", "url", "wasm-bindgen", @@ -2454,6 +2459,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2522,6 +2533,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" dependencies = [ "once_cell", + "ring 0.17.8", "rustls-pki-types", "rustls-webpki 0.102.8", "subtle", @@ -2537,32 +2549,32 @@ dependencies = [ "openssl-probe", "rustls 0.19.1", "schannel", - "security-framework", + "security-framework 2.11.1", ] [[package]] name = "rustls-native-certs" -version = "0.6.3" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ "openssl-probe", - "rustls-pemfile 1.0.4", + "rustls-pemfile 2.2.0", + "rustls-pki-types", "schannel", - "security-framework", + "security-framework 2.11.1", ] [[package]] name = "rustls-native-certs" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", - "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.0.1", ] [[package]] @@ -2631,12 +2643,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "sct" version = "0.6.1" @@ -2678,7 +2684,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.6.0", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1415a607e92bec364ea2cf9264646dcce0f91e6d65281bd6f2819cca3bf39c8" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", @@ -2868,15 +2887,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - [[package]] name = "signature" version = "2.2.0" @@ -2991,7 +3001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys 0.5.0", ] @@ -3002,7 +3012,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.6.0", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -3158,9 +3168,7 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", diff --git a/Cargo.toml b/Cargo.toml index 2c3ccac..21d4eaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,6 @@ base64 = "0.22.1" chrono = "0.4.19" config = { version = "0.14.0" } http = "1.1.0" -# error-stack = "0.4.1" -ldap-poller = { git = "https://github.com/famedly/ldap-poller", version = "0.1.0" } serde = { version = "1.0.203", features = ["derive"] } serde_json = "1.0.127" tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "sync", "time", "fs", "rt"] } @@ -34,6 +32,7 @@ futures = "0.3.31" ldap3 = { version = "0.11.1", default-features = false, features = ["tls-native"] } native-tls = "0.2.12" hex = "0.4.3" +itertools = "0.14.0" [dependencies.tonic] version = "*" diff --git a/src/sources/ldap.rs b/src/sources/ldap.rs index e1f732f..f793351 100644 --- a/src/sources/ldap.rs +++ b/src/sources/ldap.rs @@ -1,16 +1,13 @@ //! LDAP source for syncing with Famedly's Zitadel. -use std::{fmt::Display, path::PathBuf}; +use std::{fmt::Display, path::PathBuf, time::Duration}; use anyhow::{anyhow, bail, Context, Result}; use async_trait::async_trait; -use ldap_poller::{ - config::TLSConfig, ldap::EntryStatus, ldap3::SearchEntry, AttributeConfig, CacheMethod, - ConnectionConfig, Ldap, SearchEntryExt, Searches, -}; +use itertools::Itertools; +use ldap3::{LdapConnAsync, LdapConnSettings, Scope, SearchEntry}; +use native_tls::{Certificate, Identity, TlsConnector}; use serde::Deserialize; -use tokio::sync::mpsc::Receiver; -use tokio_stream::{wrappers::ReceiverStream, StreamExt}; use url::Url; use super::Source; @@ -29,21 +26,59 @@ impl Source for LdapSource { } async fn get_sorted_users(&self) -> Result> { - let (mut ldap_client, ldap_receiver) = Ldap::new(self.ldap_config.clone().into(), None); - - let sync_handle: tokio::task::JoinHandle> = tokio::spawn(async move { - ldap_client.sync_once(None).await.context("failed to sync/fetch data from LDAP")?; - tracing::info!("Finished syncing LDAP data"); - Ok(()) - }); - - let mut added = self.get_user_changes(ldap_receiver).await?; - sync_handle.await??; - + let (conn, mut ldap) = LdapConnAsync::from_url_with_settings( + self.ldap_config.clone().try_into()?, + &self.ldap_config.url, + ) + .await?; + + let connection_result = ldap3::drive!(conn); + + ldap.with_timeout(Duration::from_secs(self.ldap_config.timeout)) + .simple_bind(&self.ldap_config.bind_dn, &self.ldap_config.bind_password) + .await? + .non_error()?; + + // We *could* use the streaming search instead, as that + // *could* let up on memory pressure, however we end up + // sorting the list in-memory later anyway. + // + // TODO: Use streaming search when we have a way to receive + // pre-sorted results. + let (search_results, _stats) = ldap + .search( + &self.ldap_config.base_dn, + Scope::Subtree, + &self.ldap_config.user_filter, + self.ldap_config.clone().get_attribute_list(), + ) + .await? + .non_error()?; + + let mut users: Vec = search_results + .into_iter() + .map(SearchEntry::construct) + .map(|entry| self.parse_user(entry)) + .try_collect()?; + + // Check if there were any connection errors before proceeding + // with an expensive sort + ldap.unbind().await?; + connection_result.await.context("Connection to ldap server failed")?; + + // There are LDAP extensions that permit sorting, however they + // seem to be largely best-effort, and the server may just + // return unsorted results if it doesn't feel like it or the + // user is not permitted to sort (yeah...). + // + // Since having sorted lists is *really* important to the sync + // algorithm, we shouldn't try to rely on this without a good + // amount of testing. + // // TODO: Find out if we can use the AD extension for receiving sorted data - added.sort_by(|a, b| a.external_user_id.cmp(&b.external_user_id)); + users.sort_by(|a, b| a.external_user_id.cmp(&b.external_user_id)); - Ok(added) + Ok(users) } } @@ -53,23 +88,6 @@ impl LdapSource { Self { ldap_config } } - /// Get user changes from an ldap receiver - pub async fn get_user_changes( - &self, - ldap_receiver: Receiver, - ) -> Result> { - ReceiverStream::new(ldap_receiver) - .fold(Ok(vec![]), |acc, entry_status| { - let mut added = acc?; - if let EntryStatus::New(entry) = entry_status { - tracing::debug!("New entry: {:?}", entry); - added.push(self.parse_user(entry)?); - }; - Ok(added) - }) - .await - } - /// Construct a user from an LDAP SearchEntry pub(crate) fn parse_user(&self, entry: SearchEntry) -> Result { let disable_bitmask = { @@ -153,19 +171,30 @@ fn read_string_entry( fn read_search_entry(entry: &SearchEntry, attribute: &AttributeMapping) -> Result { match attribute { AttributeMapping::OptionalBinary { name, is_binary: false } - | AttributeMapping::NoBinaryOption(name) => { - entry.attr_first(name).map(|entry| StringOrBytes::String(entry.to_owned())) - } + | AttributeMapping::NoBinaryOption(name) => entry + .attrs + .get(name) + .and_then(|entry| entry.first()) + .map(|entry| StringOrBytes::String(entry.to_owned())), + AttributeMapping::OptionalBinary { name, is_binary: true } => entry - .bin_attr_first(name) + .bin_attrs + .get(name) // If an entry encodes as UTF-8, it will still only be // available from the `.attr_first` function, even if ldap // presents it with the `::` delimiter. // // Hence the configuration, we just treat it as binary // data if this is requested. - .or_else(|| entry.attr_first(name).map(str::as_bytes)) - .map(|entry| StringOrBytes::Bytes(entry.to_vec())), + .and_then(|entry| entry.first().cloned()) + .or_else(|| { + entry + .attrs + .get(name) + .and_then(|entry| entry.first()) + .map(|entry| entry.as_bytes().to_vec()) + }) + .map(StringOrBytes::Bytes), } .ok_or(anyhow!("missing `{}` values for `{}`", attribute, entry.dn)) } @@ -199,56 +228,67 @@ pub struct LdapSourceConfig { pub tls: Option, } -impl From for ldap_poller::Config { - fn from(cfg: LdapSourceConfig) -> ldap_poller::Config { - let starttls = cfg.tls.as_ref().is_some_and(|tls| tls.danger_use_start_tls); - let no_tls_verify = cfg.tls.as_ref().is_some_and(|tls| tls.danger_disable_tls_verify); - let root_certificates_path = - cfg.tls.as_ref().and_then(|tls| tls.server_certificate.clone()); - let client_key_path = cfg.tls.as_ref().and_then(|tls| tls.client_key.clone()); - let client_certificate_path = - cfg.tls.as_ref().and_then(|tls| tls.client_certificate.clone()); - - let tls = TLSConfig { - starttls, - no_tls_verify, - root_certificates_path, - client_key_path, - client_certificate_path, +impl LdapSourceConfig { + /// Get the attribute list, taking into account whether we should + /// be using the attribute filter or not. + fn get_attribute_list(self) -> Vec { + if self.use_attribute_filter { + self.attributes.get_attribute_list() + } else { + vec!["*".to_owned()] + } + } +} + +impl TryFrom for LdapConnSettings { + type Error = anyhow::Error; + + fn try_from(cfg: LdapSourceConfig) -> Result { + let mut settings = LdapConnSettings::new() + .set_starttls(cfg.tls.as_ref().is_some_and(|tls| tls.danger_use_start_tls)) + .set_no_tls_verify(cfg.tls.as_ref().is_some_and(|tls| tls.danger_disable_tls_verify)); + + if let Some(tls) = cfg.tls { + let root_cert: Option = tls + .server_certificate + .as_ref() + .map(std::fs::read) + .transpose() + .context("Failed to read server certificate")? + .map(|cert_data| Certificate::from_pem(cert_data.as_slice())) + .transpose() + .context("Invalid server certificate")?; + + let identity: Option = match (tls.client_key, tls.client_certificate) { + (Some(client_key), Some(client_cert)) => Some( + Identity::from_pkcs8( + std::fs::read(client_cert)?.as_slice(), + std::fs::read(client_key)?.as_slice(), + ) + .context("Could not create client identity")?, + ), + (None, None) => None, + _ => { + bail!("Both client key *and* certificate must be specified") + } + }; + + if root_cert.is_some() || identity.is_some() { + let mut connector = TlsConnector::builder(); + + if let Some(root_cert) = root_cert { + connector.add_root_certificate(root_cert); + } + + if let Some(identity) = identity { + connector.identity(identity); + } + + settings = settings.set_connector(connector.build()?); + }; }; - let attributes = cfg.attributes; - ldap_poller::Config { - url: cfg.url, - connection: ConnectionConfig { - timeout: cfg.timeout, - operation_timeout: std::time::Duration::from_secs(cfg.timeout), - tls, - }, - search_user: cfg.bind_dn, - search_password: cfg.bind_password, - searches: Searches { - user_base: cfg.base_dn, - user_filter: cfg.user_filter, - page_size: None, - }, - attributes: AttributeConfig { - pid: attributes.user_id.get_name(), - updated: attributes.last_modified.map(AttributeMapping::get_name), - additional: vec![], - filter_attributes: cfg.use_attribute_filter, - attrs_to_track: vec![ - attributes.status.get_name(), - attributes.first_name.get_name(), - attributes.last_name.get_name(), - attributes.preferred_username.get_name(), - attributes.email.get_name(), - attributes.phone.get_name(), - ], - }, - cache_method: CacheMethod::Disabled, - check_for_deleted_entries: cfg.check_for_deleted_entries, - } + Ok(settings) } } @@ -279,6 +319,30 @@ pub struct LdapAttributesMapping { pub last_modified: Option, } +impl LdapAttributesMapping { + /// Get the attribute list; *Some* LDAP implementations accept + /// `[*]` to report all attributes, but notably AD does not, so we + /// need to send an exhaustive list of all attributes we want to + /// get back. + fn get_attribute_list(self) -> Vec { + let mut attrs = vec![ + self.first_name.get_name(), + self.last_name.get_name(), + self.preferred_username.get_name(), + self.email.get_name(), + self.phone.get_name(), + self.user_id.get_name(), + self.status.get_name(), + ]; + + if let Some(last_modified) = self.last_modified { + attrs.push(last_modified.get_name()); + } + + attrs + } +} + /// How an attribute should be defined in config - it can either be a /// raw string, *or* it can be a struct defining both an attribute /// name and whether the attribute should be treated as binary. @@ -359,9 +423,8 @@ mod tests { use std::collections::HashMap; use indoc::indoc; + use itertools::Itertools; use ldap3::SearchEntry; - use ldap_poller::ldap::EntryStatus; - use tokio::sync::mpsc; use crate::{sources::ldap::LdapSource, Config}; @@ -390,6 +453,7 @@ mod tests { email: "mail" phone: "telephoneNumber" user_id: "uid" + last_modified: "timestamp" status: name: "shadowFlag" is_binary: false @@ -423,104 +487,33 @@ mod tests { #[test] fn test_attribute_filter_use() { let config = load_config(); - let ldap_config = config.sources.ldap.expect("Expected LDAP config"); assert_eq!( - Into::::into(ldap_config).attributes.get_attr_filter(), - vec!["uid", "shadowFlag", "cn", "sn", "displayName", "mail", "telephoneNumber"] + ldap_config.get_attribute_list().into_iter().sorted().collect_vec(), + vec![ + "uid", + "shadowFlag", + "cn", + "sn", + "displayName", + "mail", + "telephoneNumber", + "timestamp" + ] + .into_iter() + .sorted() + .collect_vec() ); } #[test] fn test_no_attribute_filters() { let config = load_config(); - let mut ldap_config = config.sources.ldap.as_ref().expect("Expected LDAP config").clone(); - ldap_config.use_attribute_filter = false; - assert_eq!( - Into::::into(ldap_config).attributes.get_attr_filter(), - vec!["*"] - ); - } - - #[tokio::test] - async fn test_get_user_changes_new_and_changed() { - let (tx, rx) = mpsc::channel(32); - let config = load_config(); - let ldap_source = LdapSource { ldap_config: config.sources.ldap.unwrap() }; - - let mut user = new_user(); - - // Simulate new user entry - tx.send(EntryStatus::New(SearchEntry { - dn: "uid=testuser,ou=testorg,dc=example,dc=org".to_owned(), - attrs: user.clone(), - bin_attrs: HashMap::new(), - })) - .await - .unwrap(); - - // Modify user attributes to simulate a change - user.insert("mail".to_owned(), vec!["newemail@example.com".to_owned()]); - user.insert("telephoneNumber".to_owned(), vec!["987654321".to_owned()]); - - // Simulate changed user entry - tx.send(EntryStatus::Changed { - old: SearchEntry { - dn: "uid=testuser,ou=testorg,dc=example,dc=org".to_owned(), - attrs: new_user(), - bin_attrs: HashMap::new(), - }, - new: SearchEntry { - dn: "uid=testuser,ou=testorg,dc=example,dc=org".to_owned(), - attrs: user.clone(), - bin_attrs: HashMap::new(), - }, - }) - .await - .unwrap(); - - // Close the sender side of the channel - drop(tx); - - let result = ldap_source.get_user_changes(rx).await; - - assert!(result.is_ok(), "Failed to get user changes: {:?}", result); - let added = result.unwrap(); - assert_eq!(added.len(), 1, "Unexpected number of added users"); - } - - #[tokio::test] - async fn test_get_user_changes_removed() { - let (tx, rx) = mpsc::channel(32); - let config = load_config(); - let ldap_source = LdapSource { ldap_config: config.sources.ldap.unwrap() }; - - let user = new_user(); - - // Simulate new user entry - tx.send(EntryStatus::New(SearchEntry { - dn: "uid=testuser,ou=testorg,dc=example,dc=org".to_owned(), - attrs: user.clone(), - bin_attrs: HashMap::new(), - })) - .await - .unwrap(); - - // Simulate removed user entry - tx.send(EntryStatus::Removed("uid=testuser".as_bytes().to_vec())).await.unwrap(); - - // Close the sender side of the channel - drop(tx); - - let result = ldap_source.get_user_changes(rx).await; - - assert!(result.is_ok(), "Failed to get user changes: {:?}", result); - let added = result.unwrap(); - assert_eq!(added.len(), 1, "Unexpected number of added users"); + assert_eq!(ldap_config.get_attribute_list(), vec!["*"]); } #[tokio::test] diff --git a/tests/e2e.rs b/tests/e2e.rs index 9b2583c..478ae23 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -540,6 +540,70 @@ async fn test_e2e_ldaps() { assert!(user.is_some()); } +#[test(tokio::test)] +#[test_log(default_log_filter = "debug")] +async fn test_e2e_ldaps_no_ident() { + let mut config = ldap_config().await.clone(); + config + .sources + .ldap + .as_mut() + .map(|ldap_config| { + ldap_config.url = Url::parse("ldaps://localhost:1636").expect("invalid ldaps url"); + if let Some(tls_config) = ldap_config.tls.as_mut() { + tls_config.client_certificate = None; + tls_config.client_key = None; + } + }) + .expect("ldap must be configured for this test"); + + let mut ldap = Ldap::new().await; + ldap.create_user( + "Bob", + "Tables", + "Bobby", + "servertls@famedly.de", + Some("+12015550123"), + "servertls", + false, + ) + .await; + + perform_sync(&config).await.expect("syncing failed"); + + let zitadel = open_zitadel_connection().await; + let user = zitadel + .get_user_by_login_name("tls@famedly.de") + .await + .expect("could not query Zitadel users"); + + assert!(user.is_some()); +} + +#[test(tokio::test)] +#[test_log(default_log_filter = "debug")] +async fn test_e2e_ldaps_invalid_ident() { + let mut config = ldap_config().await.clone(); + config + .sources + .ldap + .as_mut() + .map(|ldap_config| { + ldap_config.url = Url::parse("ldaps://localhost:1636").expect("invalid ldaps url"); + if let Some(tls_config) = ldap_config.tls.as_mut() { + tls_config.client_key = None; + } + }) + .expect("ldap must be configured for this test"); + + let result = perform_sync(&config).await; + + assert!(result.is_err()); + assert!(result.unwrap_err().source().is_some_and(|source| { + source.to_string().contains("Both client key *and* certificate must be specified") + })); +} + #[test(tokio::test)] #[test_log(default_log_filter = "debug")] async fn test_e2e_ldaps_starttls() {