Skip to main content

simple_ldap/
lib.rs

1//! # simple-ldap
2//!
3//! This is a high-level LDAP client library created by wrapping the rust LDAP3 client.
4//! This provides high-level functions that helps to interact with LDAP.
5//!
6//! Wondering what this "LDAP" is anyway? Check this excellent [primer](https://github.com/inejge/ldap3/blob/27a247c8a6e4e2c86f664f4280c4c6499f0e9fe5/LDAP-primer.md) in the `ldap3` crate.
7//!
8//!
9//! ## Features
10//!
11//! - All the usual LDAP operations
12//! - Search result [deserialization](#deserialization)
13//! - Connection pooling
14//! - Streaming search with native rust [`Stream`](https://docs.rs/futures/latest/futures/stream/trait.Stream.html)s
15//! - Server Side Sort
16//!
17//!
18//! ## Usage
19//!
20//! Adding `simple_ldap` as a dependency to your project:
21//!
22//! ```commandline
23//! cargo add tokio --features rt-multi-thread
24//! cargo add simple-ldap
25//! ```
26//!
27//! Multithreaded executor is required.
28//!
29//! Most functionalities are defined on the [`LdapClient`] type. Have a look at the docs.
30//!
31//!
32//! ### Example
33//!
34//! Examples of individual operations are scattered throughout the docs, but here's the basic usage:
35//!
36//! ```no_run
37//! use simple_ldap::{
38//!     LdapClient, LdapConfig, SimpleDN,
39//!     filter::EqFilter,
40//!     ldap3::Scope
41//! };
42//! use url::Url;
43//! use serde::Deserialize;
44//!
45//! // A type for deserializing the search result into.
46//! #[derive(Debug, Deserialize)]
47//! struct User {
48//!     // // A convenience type for Distinguished Names.
49//!     pub dn: SimpleDN,
50//!     pub uid: String,
51//!     pub cn: String,
52//!     pub sn: String,
53//! }
54//!
55//!
56//! #[tokio::main]
57//! async fn main(){
58//!     let ldap_config = LdapConfig {
59//!         bind_dn: String::from("cn=manager"),
60//!         bind_password: String::from("password"),
61//!         ldap_url: Url::parse("ldaps://localhost:1389/dc=example,dc=com").unwrap(),
62//!         connection_settings: None
63//!     };
64//!     let mut client = LdapClient::new(ldap_config).await.unwrap();
65//!     let name_filter = EqFilter::from("cn".to_string(), "Sam".to_string());
66//!     let user: User = client
67//!         .search(
68//!         "ou=people,dc=example,dc=com",
69//!         Scope::OneLevel,
70//!         &name_filter,
71//!         vec!["dn", "cn", "sn", "uid"],
72//!     ).await.unwrap();
73//! }
74//! ```
75//!
76//!
77//! ### Deserialization
78//!
79//! Search results are deserialized into user provided types using [`serde`](https://serde.rs/).
80//! Define a type that reflects the expected results of your search, and derive `Deserialize` for it. For example:
81//!
82//! ```
83//! use serde::Deserialize;
84//! use serde_with::serde_as;
85//! use serde_with::OneOrMany;
86//!
87//! use simple_ldap::SimpleDN;
88//!
89//! // A type for deserializing the search result into.
90//! #[serde_as] // serde_with for multiple values
91//! #[derive(Debug, Deserialize)]
92//! struct User {
93//!     // DN is always returned, whether you ask it or not.
94//!     // You could deserialize it as a plain String, but using
95//!     // SimpleDN gives you type-safety.
96//!     pub dn: SimpleDN,
97//!     pub cn: String,
98//!     // LDAP and Rust naming conventions differ.
99//!     // You can make up for the difference by using serde's renaming annotations.
100//!     #[serde(rename = "mayNotExist")]
101//!     pub may_not_exist: Option<String>,
102//!     #[serde_as(as = "OneOrMany<_>")] // serde_with for multiple values
103//!     pub multivalued_attribute: Vec<String>
104//! }
105//! ```
106//!
107//! Take care to actually request for all the attribute fields in the search.
108//! Otherwise they won't be returned, and the deserialization will fail (unless you used an `Option`).
109//!
110//!
111//! #### String attributes
112//!
113//! Most attributes are returned as strings. You can deserialize them into just Strings, but also into
114//! anything else that can supports deserialization from a string. E.g. perhaps the string represents a
115//! timestamp, and you can deserialize it directly into [`chrono::DateTime`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html).
116//!
117//!
118//! #### Binary attributes
119//!
120//! Some attributes may be binary encoded. (Active Directory especially has a bad habit of using these.)
121//! You can just capture the bytes directly into a `Vec<u8>`, but you can also use a type that knows how to
122//! deserialize from bytes. E.g. [`uuid::Uuid`](https://docs.rs/uuid/latest/uuid/struct.Uuid.html)
123//!
124//!
125//! #### Multi-valued attributes
126//!
127//! Multi-valued attributes should be marked as #[serde_as(as = "OneOrMany<_>")] using `serde_with`. Currently, there is a limitation when handing
128//! binary attributes. This will be fixed in the future. As a workaround, you can use `search_multi_valued` or `Record::to_multi_valued_record_`.
129//! To use those method all the attributes should be multi-valued.
130//!
131//!
132//! ## Compile time features
133//!
134//! * `tls-native` - (Enabled by default) Enables TLS support using the systems native implementation.
135//! * `tls-rustls` - Enables TLS support using `rustls`. **Conflicts with `tls-native` so you need to disable default features to use this.**
136//! * `pool` - Enable connection pooling
137//!
138
139use futures::{Stream, StreamExt};
140use ldap3::{
141    Ldap, LdapConnAsync, LdapConnSettings, LdapError, Mod, Scope, SearchEntry,
142    adapters::{Adapter, EntriesOnly, PagedResults},
143};
144use serde::{Deserialize, Serialize};
145use serde_value::Value;
146use std::{
147    collections::{HashMap, HashSet},
148    fmt, iter,
149    num::NonZeroU16,
150};
151use thiserror::Error;
152use tracing::{Level, debug, error, instrument, warn};
153use url::Url;
154
155use filter::{AndFilter, EqFilter, Filter, OrFilter};
156use sort::adapter::ServerSideSort;
157
158pub mod filter;
159#[cfg(feature = "pool")]
160pub mod pool;
161pub mod simple_dn;
162mod sort;
163mod stream;
164// Export the main type of the module right here in the root.
165pub use simple_dn::SimpleDN;
166// Used as an argument in the public API.
167pub use sort::adapter::SortBy;
168
169use crate::stream::to_native_stream;
170
171// Would likely be better if we could avoid re-exporting this.
172// I suspect it's only used in some configs?
173// Also in errors actually.
174pub extern crate ldap3;
175
176
177const NO_SUCH_RECORD: u32 = 32;
178
179/// Possible choices for the `objectClass` attribute of group entries.
180///
181/// `GroupOfNames` is currently regarded as the default variant and is thus the one being returned
182/// by the impl of `Default`.
183#[derive(Debug, Copy, Clone)]
184pub enum GroupObjectClass {
185    Group,
186    GroupOfNames,
187    GroupOfUniqueNames,
188}
189
190impl fmt::Display for GroupObjectClass {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        match *self {
193            Self::Group => write!(f, "group"),
194            Self::GroupOfNames => write!(f, "groupOfNames"),
195            Self::GroupOfUniqueNames => write!(f, "groupOfUniqueNames"),
196        }
197    }
198}
199
200impl Default for GroupObjectClass {
201    fn default() -> Self {
202        Self::GroupOfNames
203    }
204}
205
206/// Configuration and authentication for LDAP connection
207#[derive(derive_more::Debug, Clone)]
208pub struct LdapConfig {
209    pub ldap_url: Url,
210    /// DistinguishedName, aka the "username" to use for the connection.
211    // Perhaps we don't want to use SimpleDN here, as it would make it impossible to bind to weird DNs.
212    pub bind_dn: String,
213    #[debug(skip)] // We don't want to print passwords.
214    pub bind_password: String,
215    /// Low level configuration for the connection.
216    /// You can probably skip it.
217    #[debug(skip)] // Debug omitted, because it just doesn't implement it.
218    pub connection_settings: Option<LdapConnSettings>,
219}
220
221///
222/// High-level LDAP client wrapper on top of ldap3 crate. This wrapper provides a high-level interface to perform LDAP operations
223/// including authentication, search, update, delete
224///
225#[derive(derive_more::Debug, Clone)]
226pub struct LdapClient {
227    /// The internal connection handle.
228    ldap: Ldap,
229
230    // We need to store credentials for rebinding in `self::authenticate()`.
231    bind_dn: String,
232    #[debug(skip)]
233    bind_password: String,
234}
235
236impl LdapClient {
237    ///
238    /// Creates a new asynchronous LDAP client.s
239    /// It's capable of running multiple operations concurrently.
240    ///
241    ///
242    /// # Bind
243    ///
244    /// This performs a simple bind on the connection so need to worry about that.
245    ///
246    pub async fn new(config: LdapConfig) -> Result<Self, Error> {
247        debug!("Creating new connection");
248
249        // With or without connection settings
250        let (conn, mut ldap) = match config.connection_settings {
251            None => LdapConnAsync::from_url(&config.ldap_url).await,
252            Some(settings) => {
253                LdapConnAsync::from_url_with_settings(settings, &config.ldap_url).await
254            }
255        }
256        .map_err(|ldap_err| {
257            Error::Connection(
258                String::from("Failed to initialize LDAP connection."),
259                ldap_err,
260            )
261        })?;
262
263        ldap3::drive!(conn);
264
265        bind(&mut ldap, &config.bind_dn, &config.bind_password).await?;
266
267        Ok(LdapClient {
268            ldap,
269            bind_dn: config.bind_dn,
270            bind_password: config.bind_password
271        })
272    }
273}
274
275impl LdapClient {
276    /// Returns the ldap3 client
277    #[deprecated = "This abstraction leakage will be removed in a future release.
278                    Use the provided methods instead. If something's missing, open an issue in github."]
279    pub fn get_inner(&self) -> Ldap {
280        self.ldap.clone()
281    }
282
283    /// End the LDAP connection.
284    ///
285    /// **Caution advised!**
286    ///
287    /// This will close the connection for all clones of this client as well,
288    /// including open streams. So make sure that you're really good to close.
289    ///
290    /// Closing an LDAP connection with an unbind is *a curtesy.*
291    /// It's fine to skip it, and because of the async hurdles outlined above,
292    /// I would perhaps even recommend it.
293    // Consuming self to prevent accidental use after unbind.
294    // This also conveniently prevents calling this with pooled clients, as the
295    // wrapper `Object` prohibits moving.
296    pub async fn unbind(mut self) -> Result<(), Error> {
297        match self.ldap.unbind().await {
298            Ok(_) => Ok(()),
299            Err(error) => Err(Error::Close(String::from("Failed to unbind"), error)),
300        }
301    }
302
303    ///
304    /// Implements a typical "login with LDAP" authentication flow. Intended for server side use.
305    ///
306    /// The user is authenticated by searching for the user in the LDAP server and then trying to bind
307    /// with the provided password and DN found from the `dn_attribute` attribute.
308    ///
309    /// The first few arguments define a search  performed using the provided filter. The filter should be a filter that matches a single user.
310    ///
311    ///
312    /// # Note ⚠️
313    ///
314    /// This will perform binds on the connection. Clones of this client should not be used elsewhere
315    /// concurrently.
316    ///
317    ///
318    /// # Arguments
319    ///
320    /// ## Search
321    ///
322    /// The first few arguments search for a single user.
323    /// It's okay if the search matches nothing but multiple results will be an error.
324    ///
325    /// - `base` - The base DN for the user search
326    /// - `scope` - Scope for the search
327    /// - `filter` - The filter to search for the user
328    ///
329    ///
330    /// ## Bind
331    ///
332    /// - `dn_attribute` - Single valued attribute on the LDAP user object that holds the user DN.
333    ///     - Very like you want to use ["entryDN"](https://datatracker.ietf.org/doc/rfc5020/)
334    /// - `password` - The password of the user we are authenticating
335    ///
336    ///
337    /// # Example
338    ///
339    /// ```no_run
340    /// use simple_ldap::{
341    ///     LdapClient, LdapConfig,
342    ///     AuthenticationResult,
343    ///     ldap3::Scope,
344    ///     filter::EqFilter
345    /// };
346    /// use url::Url;
347    ///
348    /// #[tokio::main]
349    /// async fn main(){
350    ///     let ldap_config = LdapConfig {
351    ///         bind_dn: String::from("cn=manager"),
352    ///         bind_password: String::from("password"),
353    ///         ldap_url: Url::parse("ldaps://localhost:1389/dc=example,dc=com").unwrap(),
354    ///         connection_settings: None
355    ///     };
356    ///
357    ///     let mut client = LdapClient::new(ldap_config).await.unwrap();
358    ///     let name_filter = EqFilter::from("cn".to_string(), "Sam".to_string());
359    ///
360    ///     let result = client.authenticate(
361    ///         "ou=people,dc=example,dc=com",
362    ///         Scope::Subtree,
363    ///         &name_filter,
364    ///         "entryDN",
365    ///         "password"
366    ///     ).await;
367    ///
368    ///     match result {
369    ///         Ok(AuthenticationResult::Success) => todo!(),
370    ///         Ok(AuthenticationResult::UserNotFound) => todo!(),
371    ///         Ok(AuthenticationResult::WrongPassword) => todo!(),
372    ///         Err(e) => todo!()
373    ///     }
374    /// }
375    /// ```
376    ///
377    /// # See also
378    ///
379    /// LDAP wiki has [a good article](https://ldapwiki.com/wiki/Wiki.jsp?page=LDAP%20Authentication) on LDAP authentication.
380    /// This method essentially implements the most common flow.
381    pub async fn authenticate<F>(
382        &mut self,
383        base: &str,
384        scope: Scope,
385        filter: &F,
386        dn_attribute: &str,
387        password: &str,
388    ) -> Result<AuthenticationResult, Error>
389    where
390        F: Filter
391    {
392        match self.search_inner(base, scope, filter, [dn_attribute]).await {
393            // Not finding a user is not an error here.
394            Err(Error::NotFound(..)) => Ok(AuthenticationResult::UserNotFound),
395            // Other errors we pass through.
396            // This includes finding multiple matches.
397            Err(e) => Err(e),
398            // Exactly one user found, continuing authentication.
399            Ok(user_entry) => {
400                // Get the value of the dn_attribute.
401                // (Honestly I'm not sure why we don't just use `dn` directly?)
402                match user_entry.attrs.get(dn_attribute).map(Vec::as_slice) {
403                    // No values
404                    None | Some([]) => {
405                        let message = format!("'DN Attribute' {dn_attribute} wasn't defined on object {dn}",
406                            dn = user_entry.dn
407                        );
408                        Err(Error::AuthenticationFailed(message))
409                    },
410                    // Too many values
411                    Some([_, _, ..]) => {
412                        let message = format!("'DN Attribute' {dn_attribute} is multivalued (on object {dn})",
413                            dn = user_entry.dn
414                        );
415                        Err(Error::AuthenticationFailed(message))
416                    }
417                    // Exactly one value found, as it should.
418                    Some([dn_attribute_value]) => {
419                        // ⚠️ This is a tad unsound. If there are other clones using this ldap handle they too may
420                        // temporarily switch to the user we're authenticating. Ongoing operations may also be cancelled?
421                        // An alternative approach might be to open a new connection for this bind and thus not mess
422                        // with the existing one.
423                        match bind(&mut self.ldap, dn_attribute_value, password).await {
424                            // Authentication was successful. Still need to restore the original user.
425                            Ok(()) => {
426                                bind(&mut self.ldap, &self.bind_dn, &self.bind_password).await
427                                    // Opting to panic here as the connection would be otherwise left with
428                                    // an unexpected user. Some kind of poisoning would be a more complicated option.
429                                    .expect("Failed to restore the connection to it's original user.");
430                                Ok(AuthenticationResult::Success)
431                            },
432                            Err(Error::AuthenticationFailed(_)) => Ok(AuthenticationResult::WrongPassword),
433                            Err(e) => Err(e)
434                        }
435                    }
436                }
437            }
438        }
439    }
440
441    async fn search_inner<'a, F, A, S>(
442        &mut self,
443        base: &str,
444        scope: Scope,
445        filter: &F,
446        attributes: A,
447    ) -> Result<SearchEntry, Error>
448    where
449        F: Filter,
450        A: AsRef<[S]> + Send + Sync + 'a,
451        S: AsRef<str> + Send + Sync + 'a,
452    {
453        let search = self
454            .ldap
455            .search(base, scope, filter.filter().as_str(), attributes)
456            .await;
457        if let Err(error) = search {
458            return Err(Error::Query(
459                format!("Error searching for record: {error:?}"),
460                error,
461            ));
462        }
463        let result = search.unwrap().success();
464        if let Err(error) = result {
465            return Err(Error::Query(
466                format!("Error searching for record: {error:?}"),
467                error,
468            ));
469        }
470
471        let records = result.unwrap().0;
472
473        if records.len() > 1 {
474            return Err(Error::MultipleResults(String::from(
475                "Found multiple records for the search criteria",
476            )));
477        }
478
479        if records.is_empty() {
480            return Err(Error::NotFound(String::from(
481                "No records found for the search criteria",
482            )));
483        }
484
485        let record = records.first().unwrap();
486
487        Ok(SearchEntry::construct(record.to_owned()))
488    }
489
490    ///
491    /// Search a single value from the LDAP server. The search is performed using the provided filter.
492    /// The filter should be a filter that matches a single record. if the filter matches multiple users, an error is returned.
493    /// This operation will treat all the attributes as single-valued, silently ignoring the possible extra
494    /// values.
495    ///
496    ///
497    /// # Arguments
498    ///
499    /// * `base` - The base DN to search for the user
500    /// * `scope` - The scope of the search
501    /// * `filter` - The filter to search for the user
502    /// * `attributes` - The attributes to return from the search
503    ///
504    ///
505    /// # Returns
506    ///
507    /// * `Result<T, Error>` - The result will be mapped to a struct of type T
508    ///
509    ///
510    /// # Example
511    ///
512    /// ```no_run
513    /// use simple_ldap::{
514    ///     LdapClient, LdapConfig,
515    ///     filter::EqFilter,
516    ///     ldap3::Scope
517    /// };
518    /// use url::Url;
519    /// use serde::Deserialize;
520    ///
521    ///
522    /// #[derive(Debug, Deserialize)]
523    /// struct User {
524    ///     uid: String,
525    ///     cn: String,
526    ///     sn: String,
527    /// }
528    ///
529    /// #[tokio::main]
530    /// async fn main(){
531    ///     let ldap_config = LdapConfig {
532    ///         bind_dn: String::from("cn=manager"),
533    ///         bind_password: String::from("password"),
534    ///         ldap_url: Url::parse("ldaps://localhost:1389/dc=example,dc=com").unwrap(),
535    ///         connection_settings: None
536    ///     };
537    ///
538    ///     let mut client = LdapClient::new(ldap_config).await.unwrap();
539    ///
540    ///     let name_filter = EqFilter::from("cn".to_string(), "Sam".to_string());
541    ///     let user_result: User = client
542    ///         .search(
543    ///         "ou=people,dc=example,dc=com",
544    ///         Scope::OneLevel,
545    ///         &name_filter,
546    ///         vec!["cn", "sn", "uid"],
547    ///     ).await
548    ///     .unwrap();
549    /// }
550    /// ```
551    ///
552    pub async fn search<'a, F, A, S, T>(
553        &mut self,
554        base: &str,
555        scope: Scope,
556        filter: &F,
557        attributes: A,
558    ) -> Result<T, Error>
559    where
560        F: Filter,
561        A: AsRef<[S]> + Send + Sync + 'a,
562        S: AsRef<str> + Send + Sync + 'a,
563        T: for<'de> serde::Deserialize<'de>,
564    {
565        let search_entry = self.search_inner(base, scope, filter, attributes).await?;
566        to_value(search_entry)
567    }
568
569    ///
570    /// Search a single value from the LDAP server. The search is performed using the provided filter.
571    /// The filter should be a filter that matches a single record. if the filter matches multiple users, an error is returned.
572    /// This operation is useful when records has multi-valued attributes.
573    ///
574    ///
575    /// # Arguments
576    ///
577    /// * `base` - The base DN to search for the user
578    /// * `scope` - The scope of the search
579    /// * `filter` - The filter to search for the user
580    /// * `attributes` - The attributes to return from the search
581    ///
582    ///
583    /// # Returns
584    ///
585    /// * `Result<T, Error>` - The result will be mapped to a struct of type T
586    ///
587    ///
588    /// # Example
589    ///
590    /// ```no_run
591    /// use simple_ldap::{
592    ///     LdapClient, LdapConfig,
593    ///     filter::EqFilter,
594    ///     ldap3::Scope
595    /// };
596    /// use url::Url;
597    /// use serde::Deserialize;
598    ///
599    ///
600    /// #[derive(Debug, Deserialize)]
601    /// struct TestMultiValued {
602    ///    key1: Vec<String>,
603    ///    key2: Vec<String>,
604    /// }
605    ///
606    /// #[tokio::main]
607    /// async fn main(){
608    ///     let ldap_config = LdapConfig {
609    ///         bind_dn: String::from("cn=manager"),
610    ///         bind_password: String::from("password"),
611    ///         ldap_url: Url::parse("ldaps://localhost:1389/dc=example,dc=com").unwrap(),
612    ///         connection_settings: None
613    ///     };
614    ///
615    ///     let mut client = LdapClient::new(ldap_config).await.unwrap();
616    ///
617    ///     let name_filter = EqFilter::from("cn".to_string(), "Sam".to_string());
618    ///     let user_result = client.search_multi_valued::<TestMultiValued>(
619    ///         "",
620    ///         Scope::OneLevel,
621    ///         &name_filter,
622    ///         &vec!["cn", "sn", "uid"]
623    ///     ).await;
624    /// }
625    /// ```
626    ///
627    pub async fn search_multi_valued<T: for<'a> serde::Deserialize<'a>>(
628        &mut self,
629        base: &str,
630        scope: Scope,
631        filter: &impl Filter,
632        attributes: &Vec<&str>,
633    ) -> Result<T, Error> {
634        let search_entry = self.search_inner(base, scope, filter, attributes).await?;
635        to_multi_value(search_entry)
636    }
637
638    ///
639    /// This method is used to search multiple records from the LDAP server. The search is performed using the provided filter.
640    /// Method will return a Stream. The stream will lazily fetch the results, resulting in a smaller
641    /// memory footprint.
642    ///
643    /// This is the recommended search method, especially if you don't know that the result set is going to be small.
644    ///
645    ///
646    /// # Arguments
647    ///
648    /// * `base` - The base DN to search for the user
649    /// * `scope` - The scope of the search
650    /// * `filter` - The filter to search for the user
651    /// * `attributes` - The attributes to return from the search
652    /// * `page_size` - Fetch the results in pages. Recommended for large result sets.
653    ///   Uses the Simple Paged Results LDAP extension.
654    /// * `sort_by` - Sort the results using Server Side Sort LDAP extension.
655    ///
656    ///
657    /// # Returns
658    //
659    /// A stream that can be used to iterate through the search results.
660    ///
661    ///
662    /// ## Blocking drop caveat
663    ///
664    /// Dropping this stream may issue blocking network requests to cancel the search.
665    /// Running the stream to it's end will minimize the chances of this happening.
666    /// You should take this into account if latency is critical to your application.
667    ///
668    /// We're waiting for [`AsyncDrop`](https://github.com/rust-lang/rust/issues/126482) for implementing this properly.
669    ///
670    ///
671    /// # Example
672    ///
673    /// ```no_run
674    /// use simple_ldap::{
675    ///     LdapClient, LdapConfig, SortBy,
676    ///     filter::EqFilter,
677    ///     ldap3::Scope,
678    /// };
679    /// use url::Url;
680    /// use serde::Deserialize;
681    /// use futures::{StreamExt, TryStreamExt};
682    /// use std::num::NonZero;
683    ///
684    ///
685    /// #[derive(Deserialize, Debug)]
686    /// struct User {
687    ///     uid: String,
688    ///     cn: String,
689    ///     sn: String,
690    /// }
691    ///
692    /// #[tokio::main]
693    /// async fn main(){
694    ///     let ldap_config = LdapConfig {
695    ///         bind_dn: String::from("cn=manager"),
696    ///         bind_password: String::from("password"),
697    ///         ldap_url: Url::parse("ldaps://localhost:1389/dc=example,dc=com").unwrap(),
698    ///         connection_settings: None
699    ///     };
700    ///
701    ///     let mut client = LdapClient::new(ldap_config).await.unwrap();
702    ///
703    ///     let name_filter = EqFilter::from(String::from("cn"), String::from("Sam"));
704    ///     let attributes = vec!["cn", "sn", "uid"];
705    ///     let sort = vec![
706    ///         SortBy {
707    ///             attribute: String::from("sn"),
708    ///             reverse: true
709    ///         }
710    ///     ];
711    ///
712    ///     let stream = client.streaming_search(
713    ///         "ou=people,dc=example,dc=com",
714    ///         Scope::OneLevel,
715    ///         &name_filter,
716    ///         attributes,
717    ///         Some(NonZero::new(200).unwrap()), // The pagesize
718    ///         sort
719    ///     ).await.unwrap();
720    ///
721    ///     // Map the search results to User type.
722    ///     stream.and_then(async |record| record.to_record())
723    ///          // Do something with the Users concurrently.
724    ///         .try_for_each(async |user: User| {
725    ///             println!("User: {:?}", user);
726    ///             Ok(())
727    ///         })
728    ///         .await
729    ///         .unwrap();
730    /// }
731    /// ```
732    ///
733    pub async fn streaming_search<'a, F, A, S>(
734        // This self reference  lifetime has some nuance behind it.
735        //
736        // In principle it could just be a value, but then you wouldn't be able to call this
737        // with a pooled client, as the deadpool `Object` wrapper only ever gives out references.
738        //
739        // The lifetime is needed to guarantee that the client is not returned to the pool before
740        // the returned stream is finished. This requirement is artificial. Internally the `ldap3` client
741        // just makes copy. So this lifetime is here just to enforce correct pool usage.
742        &'a mut self,
743        base: &str,
744        scope: Scope,
745        filter: &F,
746        attributes: A,
747        // The internal adapter takes i32, but half of its range is invalid.
748        page_size: Option<NonZeroU16>,
749        sort_by: Vec<SortBy>,
750    ) -> Result<impl Stream<Item = Result<Record, Error>> + use<'a, F, A, S>, Error>
751    where
752        F: Filter,
753        // PagedResults requires Clone and Debug too.
754        A: AsRef<[S]> + Send + Sync + Clone + fmt::Debug + 'a,
755        S: AsRef<str> + Send + Sync + Clone + fmt::Debug + 'a,
756    {
757        // Define the needed adapters.
758
759        // Entries only is only needed with paging.
760        let (paging_adapter, entries_only_adapter) = page_size
761            .map(|non_zero| (PagedResults::new(non_zero.get().into()), EntriesOnly::new()))
762            .map(|(page_adapter, entries_adapter)| {
763                (Box::new(page_adapter) as _, Box::new(entries_adapter) as _)
764            })
765            .unzip();
766
767        // Empty vec just means that we won't use the search adapter.
768        let sort_adapter: Option<Box<dyn Adapter<'a, S, A>>> = vec_to_option(sort_by)
769            .map(ServerSideSort::new)
770            .transpose()
771            .map_err(|duplicate_args_err| Error::Sort(duplicate_args_err.to_string()))?
772            .map(|adapter| Box::new(adapter) as _);
773
774        let maybe_adapters: Vec<Option<Box<dyn Adapter<'a, S, A>>>> = vec![
775            // Sort needs to be before paging, so that it's control will be included in all the page requests.
776            sort_adapter,
777            entries_only_adapter,
778            paging_adapter,
779        ];
780
781        // This might end up as no adapters but that's perfectly fine too.
782        // Internally the non adapted streaming search would anyway just call the same thing with an empty adapter list.
783        let adapters: Vec<_> = maybe_adapters.into_iter().flatten().collect();
784
785        let search_stream = self
786            .ldap
787            .streaming_search_with(adapters, base, scope, filter.filter().as_str(), attributes)
788            .await
789            .map_err(|ldap_error| {
790                Error::Query(
791                    format!("Error searching for record: {ldap_error:?}"),
792                    ldap_error,
793                )
794            })?;
795
796        to_native_stream(search_stream)
797    }
798
799    ///
800    /// Create a new record in the LDAP server. The record will be created in the provided base DN.
801    ///
802    /// # Arguments
803    ///
804    /// * `uid` - The uid of the record
805    /// * `base` - The base DN to create the record
806    /// * `data` - The attributes of the record
807    ///
808    ///
809    /// # Returns
810    ///
811    /// * `Result<(), Error>` - Returns an error if the record creation fails
812    ///
813    ///
814    /// # Example
815    ///
816    /// ```no_run
817    /// use simple_ldap::{LdapClient, LdapConfig};
818    /// use url::Url;
819    /// use std::collections::HashSet;
820    ///
821    /// #[tokio::main]
822    /// async fn main(){
823    ///     let ldap_config = LdapConfig {
824    ///         bind_dn: String::from("cn=manager"),
825    ///         bind_password: String::from("password"),
826    ///         ldap_url: Url::parse("ldaps://localhost:1389/dc=example,dc=com").unwrap(),
827    ///         connection_settings: None
828    ///     };
829    ///
830    ///     let mut client = LdapClient::new(ldap_config).await.unwrap();
831    ///
832    ///     let data = vec![
833    ///         ( "objectClass",HashSet::from(["organizationalPerson", "inetorgperson", "top", "person"]),),
834    ///         ("uid",HashSet::from(["bd9b91ec-7a69-4166-bf67-cc7e553b2fd9"]),),
835    ///         ("cn", HashSet::from(["Kasun"])),
836    ///         ("sn", HashSet::from(["Ranasingh"])),
837    ///     ];
838    ///
839    ///     let result = client.create("bd9b91ec-7a69-4166-bf67-cc7e553b2fd9", "ou=people,dc=example,dc=com", data).await;
840    /// }
841    /// ```
842    ///
843    pub async fn create(
844        &mut self,
845        uid: &str,
846        base: &str,
847        data: Vec<(&str, HashSet<&str>)>,
848    ) -> Result<(), Error> {
849        let dn = format!("uid={uid},{base}");
850        let save = self.ldap.add(dn.as_str(), data).await;
851        if let Err(err) = save {
852            return Err(Error::Create(format!("Error saving record: {err:?}"), err));
853        }
854        let save = save.unwrap().success();
855
856        if let Err(err) = save {
857            return Err(Error::Create(format!("Error saving record: {err:?}"), err));
858        }
859        let res = save.unwrap();
860        debug!("Successfully created record result: {:?}", res);
861        Ok(())
862    }
863
864    ///
865    /// Update a record in the LDAP server. The record will be updated in the provided base DN.
866    ///
867    /// # Arguments
868    ///
869    /// * `uid` - The uid of the record
870    /// * `base` - The base DN to update the record
871    /// * `data` - The attributes of the record
872    /// * `new_uid` - The new uid of the record. If the new uid is provided, the uid of the record will be updated.
873    ///
874    ///
875    /// # Returns
876    ///
877    /// * `Result<(), Error>` - Returns an error if the record update fails
878    ///
879    ///
880    /// # Example
881    ///
882    /// ```no_run
883    /// use simple_ldap::{
884    ///     LdapClient, LdapConfig,
885    ///     ldap3::Mod
886    /// };
887    /// use url::Url;
888    /// use std::collections::HashSet;
889    ///
890    /// #[tokio::main]
891    /// async fn main(){
892    ///     let ldap_config = LdapConfig {
893    ///         bind_dn: String::from("cn=manager"),
894    ///         bind_password: String::from("password"),
895    ///         ldap_url: Url::parse("ldaps://localhost:1389/dc=example,dc=com").unwrap(),
896    ///         connection_settings: None
897    ///     };
898    ///
899    ///     let mut client = LdapClient::new(ldap_config).await.unwrap();
900    ///
901    ///     let data = vec![
902    ///         Mod::Replace("cn", HashSet::from(["Jhon_Update"])),
903    ///         Mod::Replace("sn", HashSet::from(["Eliet_Update"])),
904    ///     ];
905    ///
906    ///     let result = client.update(
907    ///         "e219fbc0-6df5-4bc3-a6ee-986843bb157e",
908    ///         "ou=people,dc=example,dc=com",
909    ///         data,
910    ///         None
911    ///     ).await;
912    /// }
913    /// ```
914    ///
915    pub async fn update(
916        &mut self,
917        uid: &str,
918        base: &str,
919        data: Vec<Mod<&str>>,
920        new_uid: Option<&str>,
921    ) -> Result<(), Error> {
922        let dn = format!("uid={uid},{base}");
923
924        let res = self.ldap.modify(dn.as_str(), data).await;
925        if let Err(err) = res {
926            return Err(Error::Update(
927                format!("Error updating record: {err:?}"),
928                err,
929            ));
930        }
931
932        let res = res.unwrap().success();
933        if let Err(err) = res {
934            match err {
935                LdapError::LdapResult { result } => {
936                    if result.rc == NO_SUCH_RECORD {
937                        return Err(Error::NotFound(format!(
938                            "No records found for the uid: {uid:?}"
939                        )));
940                    }
941                }
942                _ => {
943                    return Err(Error::Update(
944                        format!("Error updating record: {err:?}"),
945                        err,
946                    ));
947                }
948            }
949        }
950
951        if new_uid.is_none() {
952            return Ok(());
953        }
954
955        let new_uid = new_uid.unwrap();
956        if !uid.eq_ignore_ascii_case(new_uid) {
957            let new_dn = format!("uid={new_uid}");
958            let dn_update = self
959                .ldap
960                .modifydn(dn.as_str(), new_dn.as_str(), true, None)
961                .await;
962            if let Err(err) = dn_update {
963                error!("Failed to update dn for record {:?} error {:?}", uid, err);
964                return Err(Error::Update(
965                    format!("Failed to update dn for record {uid:?}"),
966                    err,
967                ));
968            }
969
970            let dn_update = dn_update.unwrap().success();
971            if let Err(err) = dn_update {
972                error!("Failed to update dn for record {:?} error {:?}", uid, err);
973                return Err(Error::Update(
974                    format!("Failed to update dn for record {uid:?}"),
975                    err,
976                ));
977            }
978
979            let res = dn_update.unwrap();
980            debug!("Successfully updated dn result: {:?}", res);
981        }
982
983        Ok(())
984    }
985
986    ///
987    /// Delete a record in the LDAP server. The record will be deleted in the provided base DN.
988    ///
989    /// # Arguments
990    ///
991    /// * `uid` - The uid of the record
992    /// * `base` - The base DN to delete the record
993    ///
994    ///
995    /// # Returns
996    ///
997    /// * `Result<(), Error>` - Returns an error if the record delete fails
998    ///
999    ///
1000    /// # Example
1001    ///
1002    /// ```no_run
1003    /// use simple_ldap::{LdapClient, LdapConfig};
1004    /// use url::Url;
1005    ///
1006    /// #[tokio::main]
1007    /// async fn main(){
1008    ///     let ldap_config = LdapConfig {
1009    ///         bind_dn: String::from("cn=manager"),
1010    ///         bind_password: String::from("password"),
1011    ///         ldap_url: Url::parse("ldaps://localhost:1389/dc=example,dc=com").unwrap(),
1012    ///         connection_settings: None
1013    ///     };
1014    ///
1015    ///     let mut client = LdapClient::new(ldap_config).await.unwrap();
1016    ///
1017    ///     let result = client.delete("e219fbc0-6df5-4bc3-a6ee-986843bb157e", "ou=people,dc=example,dc=com").await;
1018    /// }
1019    /// ```
1020    pub async fn delete(&mut self, uid: &str, base: &str) -> Result<(), Error> {
1021        let dn = format!("uid={uid},{base}");
1022        let delete = self.ldap.delete(dn.as_str()).await;
1023
1024        if let Err(err) = delete {
1025            return Err(Error::Delete(
1026                format!("Error deleting record: {err:?}"),
1027                err,
1028            ));
1029        }
1030        let delete = delete.unwrap().success();
1031        if let Err(err) = delete {
1032            match err {
1033                LdapError::LdapResult { result } => {
1034                    if result.rc == NO_SUCH_RECORD {
1035                        return Err(Error::NotFound(format!(
1036                            "No records found for the uid: {uid:?}"
1037                        )));
1038                    }
1039                }
1040                _ => {
1041                    return Err(Error::Delete(
1042                        format!("Error deleting record: {err:?}"),
1043                        err,
1044                    ));
1045                }
1046            }
1047        }
1048        debug!("Successfully deleted record result: {:?}", uid);
1049        Ok(())
1050    }
1051
1052    ///
1053    /// Create a new group in the LDAP server. The group will be created in the provided base DN.
1054    ///
1055    /// # Arguments
1056    ///
1057    /// * `group_name` - The name of the group
1058    /// * `group_ou` - The ou of the group
1059    /// * `description` - The description of the group
1060    ///
1061    /// # Returns
1062    ///
1063    /// * `Result<(), Error>` - Returns an error if the group creation fails
1064    ///
1065    ///
1066    /// # Example
1067    ///
1068    /// ```no_run
1069    /// use simple_ldap::{LdapClient, LdapConfig};
1070    /// use url::Url;
1071    ///
1072    /// #[tokio::main]
1073    /// async fn main(){
1074    ///     let ldap_config = LdapConfig {
1075    ///         bind_dn: String::from("cn=manager"),
1076    ///         bind_password: String::from("password"),
1077    ///         ldap_url: Url::parse("ldaps://localhost:1389/dc=example,dc=com").unwrap(),
1078    ///         connection_settings: None
1079    ///     };
1080    ///
1081    ///     let mut client = LdapClient::new(ldap_config).await.unwrap();
1082    ///
1083    ///     let result = client.create_group("test_group", "ou=groups,dc=example,dc=com", "test group").await;
1084    /// }
1085    /// ```
1086    pub async fn create_group(
1087        &mut self,
1088        group_name: &str,
1089        group_ou: &str,
1090        description: &str,
1091    ) -> Result<(), Error> {
1092        let dn = format!("cn={group_name},{group_ou}");
1093
1094        let data = vec![
1095            ("objectClass", HashSet::from(["top", "groupOfNames"])),
1096            ("cn", HashSet::from([group_name])),
1097            ("ou", HashSet::from([group_ou])),
1098            ("description", HashSet::from([description])),
1099        ];
1100        let save = self.ldap.add(dn.as_str(), data).await;
1101        if let Err(err) = save {
1102            return Err(Error::Create(format!("Error saving record: {err:?}"), err));
1103        }
1104        let save = save.unwrap().success();
1105
1106        if let Err(err) = save {
1107            return Err(Error::Create(format!("Error creating group: {err:?}"), err));
1108        }
1109        let res = save.unwrap();
1110        debug!("Successfully created group result: {:?}", res);
1111        Ok(())
1112    }
1113
1114    ///
1115    /// Add users to a group in the LDAP server. The group will be updated in the provided base DN.
1116    ///
1117    /// # Arguments
1118    ///
1119    /// * `users` - The list of users to add to the group
1120    /// * `group_dn` - The dn of the group
1121    ///
1122    ///
1123    /// # Returns
1124    ///
1125    /// * `Result<(), Error>` - Returns an error if failed to add users to the group
1126    ///
1127    ///
1128    /// # Example
1129    ///
1130    /// ```no_run
1131    /// use simple_ldap::{LdapClient, LdapConfig};
1132    /// use url::Url;
1133    ///
1134    /// #[tokio::main]
1135    /// async fn main(){
1136    ///     let ldap_config = LdapConfig {
1137    ///         bind_dn: String::from("cn=manager"),
1138    ///         bind_password: String::from("password"),
1139    ///         ldap_url: Url::parse("ldaps://localhost:1389/dc=example,dc=com").unwrap(),
1140    ///         connection_settings: None
1141    ///     };
1142    ///
1143    ///     let mut client = LdapClient::new(ldap_config).await.unwrap();
1144    ///
1145    ///     let result = client.add_users_to_group(
1146    ///         vec!["uid=bd9b91ec-7a69-4166-bf67-cc7e553b2fd9,ou=people,dc=example,dc=com"],
1147    ///         "cn=test_group,ou=groups,dc=example,dc=com").await;
1148    /// }
1149    /// ```
1150    pub async fn add_users_to_group(
1151        &mut self,
1152        users: Vec<&str>,
1153        group_dn: &str,
1154    ) -> Result<(), Error> {
1155        let mut mods = Vec::new();
1156        let users = users.iter().copied().collect::<HashSet<&str>>();
1157        mods.push(Mod::Replace("member", users));
1158        let res = self.ldap.modify(group_dn, mods).await;
1159        if let Err(err) = res {
1160            return Err(Error::Update(
1161                format!("Error updating record: {err:?}"),
1162                err,
1163            ));
1164        }
1165
1166        let res = res.unwrap().success();
1167        if let Err(err) = res {
1168            match err {
1169                LdapError::LdapResult { result } => {
1170                    if result.rc == NO_SUCH_RECORD {
1171                        return Err(Error::NotFound(format!(
1172                            "No records found for the uid: {group_dn:?}"
1173                        )));
1174                    }
1175                }
1176                _ => {
1177                    return Err(Error::Update(
1178                        format!("Error updating record: {err:?}"),
1179                        err,
1180                    ));
1181                }
1182            }
1183        }
1184        Ok(())
1185    }
1186
1187    ///
1188    /// Get users of a group in the LDAP server. The group will be searched in the provided base DN.
1189    ///
1190    /// # Arguments
1191    ///
1192    /// * `group_dn` - The dn of the group
1193    /// * `base_dn` - The base dn to search for the users
1194    /// * `scope` - The scope of the search
1195    /// * `attributes` - The attributes to return from the search
1196    ///
1197    ///
1198    /// # Returns
1199    ///
1200    /// * `Result<Vec<T>, Error>` - Returns a vector of structs of type T
1201    ///
1202    ///
1203    /// # Example
1204    ///
1205    /// ```no_run
1206    /// use simple_ldap::{
1207    ///     LdapClient, LdapConfig,
1208    ///     ldap3::Scope
1209    /// };
1210    /// use url::Url;
1211    /// use serde::Deserialize;
1212    ///
1213    /// #[derive(Debug, Deserialize)]
1214    /// struct User {
1215    ///     uid: String,
1216    ///     cn: String,
1217    ///     sn: String,
1218    /// }
1219    ///
1220    /// #[tokio::main]
1221    /// async fn main(){
1222    ///     let ldap_config = LdapConfig {
1223    ///         bind_dn: String::from("cn=manager"),
1224    ///         bind_password: String::from("password"),
1225    ///         ldap_url: Url::parse("ldaps://localhost:1389/dc=example,dc=com").unwrap(),
1226    ///         connection_settings: None
1227    ///     };
1228    ///
1229    ///     let mut client = LdapClient::new(ldap_config).await.unwrap();
1230    ///
1231    ///     let members: Vec<User> = client.get_members(
1232    ///         "cn=test_group,ou=groups,dc=example,dc=com",
1233    ///         "ou=people,dc=example,dc=com",
1234    ///         Scope::OneLevel,
1235    ///         vec!["cn", "sn", "uid"]
1236    ///     ).await
1237    ///     .unwrap();
1238    /// }
1239    /// ```
1240    ///
1241    pub async fn get_members<'a, A, S, T>(
1242        &mut self,
1243        group_dn: &str,
1244        base_dn: &str,
1245        scope: Scope,
1246        attributes: A,
1247    ) -> Result<Vec<T>, Error>
1248    where
1249        A: AsRef<[S]> + Send + Sync + Clone + fmt::Debug + 'a,
1250        S: AsRef<str> + Send + Sync + Clone + fmt::Debug + 'a,
1251        T: for<'de> serde::Deserialize<'de>,
1252    {
1253        let search = self
1254            .ldap
1255            .search(
1256                group_dn,
1257                Scope::Base,
1258                "(objectClass=groupOfNames)",
1259                vec!["member"],
1260            )
1261            .await;
1262
1263        if let Err(error) = search {
1264            return Err(Error::Query(
1265                format!("Error searching for record: {error:?}"),
1266                error,
1267            ));
1268        }
1269        let result = search.unwrap().success();
1270        if let Err(error) = result {
1271            return Err(Error::Query(
1272                format!("Error searching for record: {error:?}"),
1273                error,
1274            ));
1275        }
1276
1277        let records = result.unwrap().0;
1278
1279        if records.len() > 1 {
1280            return Err(Error::MultipleResults(String::from(
1281                "Found multiple records for the search criteria",
1282            )));
1283        }
1284
1285        if records.is_empty() {
1286            return Err(Error::NotFound(String::from(
1287                "No records found for the search criteria",
1288            )));
1289        }
1290
1291        let record = records.first().unwrap();
1292
1293        let mut or_filter = OrFilter::default();
1294
1295        let search_entry = SearchEntry::construct(record.to_owned());
1296        search_entry
1297            .attrs
1298            .into_iter()
1299            .filter(|(_, value)| !value.is_empty())
1300            .map(|(arrta, value)| (arrta.to_owned(), value.to_owned()))
1301            .filter(|(attra, _)| attra.eq("member"))
1302            .flat_map(|(_, value)| value)
1303            .map(|val| {
1304                val.split(',').collect::<Vec<&str>>()[0]
1305                    .split('=')
1306                    .map(|split| split.to_string())
1307                    .collect::<Vec<String>>()
1308            })
1309            .map(|uid| EqFilter::from(uid[0].to_string(), uid[1].to_string()))
1310            .for_each(|eq| or_filter.add(Box::new(eq)));
1311
1312        let result = self
1313            .streaming_search(base_dn, scope, &or_filter, attributes, None, Vec::new())
1314            .await;
1315
1316        let mut members = Vec::new();
1317        match result {
1318            Ok(result) => {
1319                let mut stream = Box::pin(result);
1320                while let Some(member) = stream.next().await {
1321                    match member {
1322                        Ok(member) => {
1323                            let user: T = member.to_record().unwrap();
1324                            members.push(user);
1325                        }
1326                        Err(err) => {
1327                            // TODO: Exit with an error instead?
1328                            error!("Error getting member error {:?}", err);
1329                        }
1330                    }
1331                }
1332                return Ok(members);
1333            }
1334            Err(err) => {
1335                // TODO: Exit with an error instead?
1336                error!("Error getting members {:?} error {:?}", group_dn, err);
1337            }
1338        }
1339
1340        Ok(members)
1341    }
1342
1343    ///
1344    /// Remove users from a group in the LDAP server. The group will be updated in the provided base DN.
1345    /// This method will remove all the users provided from the group.
1346    ///
1347    ///
1348    /// # Arguments
1349    ///
1350    /// * `group_dn` - The dn of the group
1351    /// * `users` - The list of users to remove from the group
1352    ///
1353    ///
1354    /// # Returns
1355    ///
1356    /// * `Result<(), Error>` - Returns an error if failed to remove users from the group
1357    ///
1358    ///
1359    /// # Example
1360    ///
1361    /// ```no_run
1362    /// use simple_ldap::{LdapClient, LdapConfig};
1363    /// use url::Url;
1364    /// use std::collections::HashSet;
1365    ///
1366    /// #[tokio::main]
1367    /// async fn main(){
1368    ///     let ldap_config = LdapConfig {
1369    ///         bind_dn: String::from("cn=manager"),
1370    ///         bind_password: String::from("password"),
1371    ///         ldap_url: Url::parse("ldaps://localhost:1389/dc=example,dc=com").unwrap(),
1372    ///         connection_settings: None
1373    ///     };
1374    ///
1375    ///     let mut client = LdapClient::new(ldap_config).await.unwrap();
1376    ///
1377    ///     let result = client.remove_users_from_group("cn=test_group,ou=groups,dc=example,dc=com",
1378    ///     vec!["uid=bd9b91ec-7a69-4166-bf67-cc7e553b2fd9,ou=people,dc=example,dc=com"]).await;
1379    /// }
1380    /// ```
1381    pub async fn remove_users_from_group(
1382        &mut self,
1383        group_dn: &str,
1384        users: Vec<&str>,
1385    ) -> Result<(), Error> {
1386        let mut mods = Vec::new();
1387        let users = users.iter().copied().collect::<HashSet<&str>>();
1388        mods.push(Mod::Delete("member", users));
1389        let res = self.ldap.modify(group_dn, mods).await;
1390        if let Err(err) = res {
1391            return Err(Error::Update(
1392                format!("Error removing users from group:{group_dn:?}: {err:?}"),
1393                err,
1394            ));
1395        }
1396
1397        let res = res.unwrap().success();
1398        if let Err(err) = res {
1399            match err {
1400                LdapError::LdapResult { result } => {
1401                    if result.rc == NO_SUCH_RECORD {
1402                        return Err(Error::NotFound(format!(
1403                            "No records found for the uid: {group_dn:?}"
1404                        )));
1405                    }
1406                }
1407                _ => {
1408                    return Err(Error::Update(
1409                        format!("Error removing users from group:{group_dn:?}: {err:?}"),
1410                        err,
1411                    ));
1412                }
1413            }
1414        }
1415        Ok(())
1416    }
1417
1418    ///
1419    /// Get the groups associated with a user in the LDAP server. The user will be searched in the provided base DN.
1420    ///
1421    /// # Arguments
1422    ///
1423    /// * `group_ou` - The ou to search for the groups
1424    /// * `user_dn` - The dn of the user
1425    /// * `group_object_class` - The object class of groups to use during the search
1426    ///
1427    /// # Returns
1428    ///
1429    /// * `Result<Vec<String>, Error>` - Returns a vector of group names. Will be empty when there are no associated groups
1430    ///
1431    ///
1432    /// # Example
1433    ///
1434    /// ```no_run
1435    /// use simple_ldap::{GroupObjectClass, LdapClient, LdapConfig};
1436    /// use url::Url;
1437    ///
1438    /// #[tokio::main]
1439    /// async fn main(){
1440    ///     let ldap_config = LdapConfig {
1441    ///         bind_dn: String::from("cn=manager"),
1442    ///         bind_password: String::from("password"),
1443    ///         ldap_url: Url::parse("ldaps://localhost:1389/dc=example,dc=com").unwrap(),
1444    ///         connection_settings: None
1445    ///     };
1446    ///
1447    ///     let mut client = LdapClient::new(ldap_config).await.unwrap();
1448    ///
1449    ///     let result = client.get_associated_groups("ou=groups,dc=example,dc=com",
1450    ///         "uid=bd9b91ec-7a69-4166-bf67-cc7e553b2fd9,ou=people,dc=example,dc=com",
1451    ///         GroupObjectClass::default()).await;
1452    /// }
1453    /// ```
1454    pub async fn get_associated_groups(
1455        &mut self,
1456        group_ou: &str,
1457        user_dn: &str,
1458        group_object_class: GroupObjectClass,
1459    ) -> Result<Vec<String>, Error> {
1460        let group_filter = Box::new(EqFilter::from(
1461            "objectClass".to_string(),
1462            group_object_class.to_string(),
1463        ));
1464
1465        let user_filter = Box::new(EqFilter::from("member".to_string(), user_dn.to_string()));
1466        let mut filter = AndFilter::default();
1467        filter.add(group_filter);
1468        filter.add(user_filter);
1469
1470        let search = self
1471            .ldap
1472            .search(
1473                group_ou,
1474                Scope::Subtree,
1475                filter.filter().as_str(),
1476                vec!["cn"],
1477            )
1478            .await;
1479
1480        if let Err(error) = search {
1481            return Err(Error::Query(
1482                format!("Error searching for record: {error:?}"),
1483                error,
1484            ));
1485        }
1486        let result = search.unwrap().success();
1487        if let Err(error) = result {
1488            return Err(Error::Query(
1489                format!("Error searching for record: {error:?}"),
1490                error,
1491            ));
1492        }
1493
1494        let records = result.unwrap().0;
1495
1496        if records.is_empty() {
1497            return Ok(Vec::new());
1498        }
1499
1500        let record = records
1501            .iter()
1502            .map(|record| SearchEntry::construct(record.to_owned()))
1503            .map(|se| se.attrs)
1504            .flat_map(|att| {
1505                att.get("cn")
1506                    .unwrap()
1507                    .iter()
1508                    .map(|x| x.to_owned())
1509                    .collect::<Vec<String>>()
1510            })
1511            .collect::<Vec<String>>();
1512
1513        Ok(record)
1514    }
1515
1516    ///
1517    /// Get the groups associated with a user in the LDAP server. The user will be searched in the provided base DN.
1518    ///
1519    /// # Arguments
1520    ///
1521    /// * `group_ou` - The ou to search for the groups
1522    /// * `user_dn` - The dn of the user
1523    ///
1524    /// # Returns
1525    ///
1526    /// * `Result<Vec<String>, Error>` - Returns a vector of group names
1527    ///
1528    ///
1529    /// # Example
1530    ///
1531    /// ```no_run
1532    /// use simple_ldap::{LdapClient, LdapConfig};
1533    /// use url::Url;
1534    ///
1535    /// #[tokio::main]
1536    /// async fn main(){
1537    ///     let ldap_config = LdapConfig {
1538    ///         bind_dn: String::from("cn=manager"),
1539    ///         bind_password: String::from("password"),
1540    ///         ldap_url: Url::parse("ldaps://localhost:1389/dc=example,dc=com").unwrap(),
1541    ///         connection_settings: None
1542    ///     };
1543    ///
1544    ///     let mut client = LdapClient::new(ldap_config).await.unwrap();
1545    ///
1546    ///     let result = client.get_associtated_groups("ou=groups,dc=example,dc=com",
1547    ///     "uid=bd9b91ec-7a69-4166-bf67-cc7e553b2fd9,ou=people,dc=example,dc=com").await;
1548    /// }
1549    /// ```
1550    #[deprecated(
1551        since = "10.1.0",
1552        note = "Please use `get_associated_groups` instead which also allows specifiying a group's object class. This method will be removed in a future release."
1553    )]
1554    pub async fn get_associtated_groups(
1555        &mut self,
1556        group_ou: &str,
1557        user_dn: &str,
1558    ) -> Result<Vec<String>, Error> {
1559        match self
1560            .get_associated_groups(group_ou, user_dn, GroupObjectClass::default())
1561            .await
1562        {
1563            Ok(v) if v.is_empty() => Err(Error::NotFound(String::from(
1564                "User does not belong to any groups",
1565            ))),
1566            r => r,
1567        }
1568    }
1569}
1570
1571/// Result type for [LdapClient::authenticate()].
1572#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1573pub enum AuthenticationResult {
1574    /// User authenticated successfully.
1575    Success,
1576    /// No user was found.
1577    UserNotFound,
1578    /// User was found but the authentication failed.
1579    WrongPassword,
1580}
1581
1582
1583/// Private helper for binding.
1584async fn bind(inner_handle: &mut Ldap , bind_dn: &str, bind_password: &str) -> Result<(), Error> {
1585    inner_handle.simple_bind(bind_dn, bind_password)
1586        .await
1587        .map_err(|ldap_err| Error::AuthenticationFailed(format!("Bind failed: {ldap_err}")))?
1588        .success()
1589        .map_err(|ldap_err| Error::AuthenticationFailed(format!("Bind failed: {ldap_err}")))?;
1590
1591    Ok(())
1592}
1593
1594/// Empty vec becomes None, otherwise it gets wrapped in Some.
1595fn vec_to_option<T>(vec: Vec<T>) -> Option<Vec<T>> {
1596    if vec.is_empty() { None } else { Some(vec) }
1597}
1598
1599/// A proxy type for deriving `Serialize` for `ldap3::SearchEntry`.
1600/// https://serde.rs/remote-derive.html
1601#[derive(Serialize)]
1602#[serde(remote = "ldap3::SearchEntry")]
1603struct Ldap3SearchEntry {
1604    /// Entry DN.
1605    pub dn: String,
1606    /// Attributes.
1607    /// Flattening to ease up the serialization step.
1608    #[serde(flatten)]
1609    pub attrs: HashMap<String, Vec<String>>,
1610    /// Binary-valued attributes.
1611    /// Flattening to ease up the serialization step.
1612    #[serde(flatten)]
1613    pub bin_attrs: HashMap<String, Vec<Vec<u8>>>,
1614}
1615
1616/// This is needed for invoking the deserialize impl directly.
1617/// https://serde.rs/remote-derive.html#invoking-the-remote-impl-directly
1618#[derive(Serialize)]
1619#[serde(transparent)]
1620struct SerializeWrapper(#[serde(with = "Ldap3SearchEntry")] ldap3::SearchEntry);
1621
1622// Allowing users to debug serialization issues from the logs.
1623#[instrument(level = Level::DEBUG)]
1624fn to_single_value<T: for<'a> Deserialize<'a>>(search_entry: SearchEntry) -> Result<T, Error> {
1625    let string_attributes = search_entry
1626        .attrs
1627        .into_iter()
1628        .filter(|(_, value)| !value.is_empty())
1629        .map(|(arrta, value)| {
1630            if value.len() > 1 {
1631                warn!("Treating multivalued attribute {arrta} as singlevalued.")
1632            }
1633            (Value::String(arrta), map_to_single_value(value.first()))
1634        });
1635
1636    let binary_attributes = search_entry
1637        .bin_attrs
1638        .into_iter()
1639        // I wonder if it's possible to have empties here..?
1640        .filter(|(_, value)| !value.is_empty())
1641        .map(|(arrta, value)| {
1642            if value.len() > 1 {
1643                warn!("Treating multivalued attribute {arrta} as singlevalued.")
1644            }
1645            (
1646                Value::String(arrta),
1647                map_to_single_value_bin(value.first().cloned()),
1648            )
1649        });
1650
1651    // DN is always returned.
1652    // Adding it to the serialized fields as well.
1653    let dn_iter = iter::once(search_entry.dn)
1654        .map(|dn| (Value::String(String::from("dn")), Value::String(dn)));
1655
1656    let all_fields = string_attributes
1657        .chain(binary_attributes)
1658        .chain(dn_iter)
1659        .collect();
1660
1661    let value = serde_value::Value::Map(all_fields);
1662
1663    T::deserialize(value)
1664        .map_err(|err| Error::Mapping(format!("Error converting search result to object, {err:?}")))
1665}
1666
1667#[instrument(level = Level::TRACE)]
1668fn to_value<T: for<'a> Deserialize<'a>>(search_entry: SearchEntry) -> Result<T, Error> {
1669    let string_attributes = search_entry
1670        .attrs
1671        .into_iter()
1672        .filter(|(_, value)| !value.is_empty())
1673        .map(|(arrta, value)| {
1674            if value.len() == 1 {
1675                return (Value::String(arrta), map_to_single_value(value.first()));
1676            }
1677            (Value::String(arrta), map_to_multi_value(value))
1678        });
1679
1680    let binary_attributes = search_entry
1681        .bin_attrs
1682        .into_iter()
1683        // I wonder if it's possible to have empties here..?
1684        .filter(|(_, value)| !value.is_empty())
1685        .map(|(arrta, value)| {
1686            if value.len() > 1 {
1687                //#TODO: This is a bit of a hack to get multi-valued attributes to work for non binary values. SHOULD fix this.
1688                warn!("Treating multivalued attribute {arrta} as singlevalued.")
1689            }
1690            (
1691                Value::String(arrta),
1692                map_to_single_value_bin(value.first().cloned()),
1693            )
1694            // if value.len() == 1 {
1695            //     return (
1696            //         Value::String(arrta),
1697            //         map_to_single_value_bin(value.first().cloned()),
1698            //     );
1699            // }
1700            // (Value::String(arrta), map_to_multi_value_bin(value))
1701        });
1702
1703    // DN is always returned.
1704    // Adding it to the serialized fields as well.
1705    let dn_iter = iter::once(search_entry.dn)
1706        .map(|dn| (Value::String(String::from("dn")), Value::String(dn)));
1707
1708    let all_fields = string_attributes
1709        .chain(binary_attributes)
1710        .chain(dn_iter)
1711        .collect();
1712
1713    let value = serde_value::Value::Map(all_fields);
1714
1715    T::deserialize(value)
1716        .map_err(|err| Error::Mapping(format!("Error converting search result to object, {err:?}")))
1717}
1718
1719fn map_to_multi_value(attra_value: Vec<String>) -> serde_value::Value {
1720    serde_value::Value::Seq(
1721        attra_value
1722            .iter()
1723            .map(|value| serde_value::Value::String(value.to_string()))
1724            .collect(),
1725    )
1726}
1727
1728fn map_to_multi_value_bin(attra_values: Vec<Vec<u8>>) -> serde_value::Value {
1729    let value_bytes = attra_values
1730        .iter()
1731        .map(|value| {
1732            value
1733                .iter()
1734                .map(|byte| Value::U8(*byte))
1735                .collect::<Vec<Value>>()
1736        })
1737        .map(serde_value::Value::Seq)
1738        .collect::<Vec<Value>>();
1739
1740    serde_value::Value::Seq(value_bytes)
1741}
1742
1743// Allowing users to debug serialization issues from the logs.
1744#[instrument(level = Level::DEBUG)]
1745fn to_multi_value<T: for<'a> Deserialize<'a>>(search_entry: SearchEntry) -> Result<T, Error> {
1746    let value = serde_value::to_value(SerializeWrapper(search_entry)).map_err(|err| {
1747        Error::Mapping(format!("Error converting search result to object, {err:?}"))
1748    })?;
1749
1750    T::deserialize(value)
1751        .map_err(|err| Error::Mapping(format!("Error converting search result to object, {err:?}")))
1752}
1753
1754fn map_to_single_value(attra_value: Option<&String>) -> serde_value::Value {
1755    match attra_value {
1756        Some(value) => serde_value::Value::String(value.to_string()),
1757        None => serde_value::Value::Option(Option::None),
1758    }
1759}
1760
1761fn map_to_single_value_bin(attra_values: Option<Vec<u8>>) -> serde_value::Value {
1762    match attra_values {
1763        Some(bytes) => {
1764            let value_bytes = bytes.into_iter().map(Value::U8).collect();
1765
1766            serde_value::Value::Seq(value_bytes)
1767        }
1768        None => serde_value::Value::Option(Option::None),
1769    }
1770}
1771
1772/// The Record struct is used to map the search result to a struct.
1773/// The Record struct has a method to_record which will map the search result to a struct.
1774/// The Record struct has a method to_multi_valued_record which will map the search result to a struct with multi valued attributes.
1775//
1776// It would be nice to hide this record type from the public API and just expose already
1777// deserialized user types.
1778pub struct Record {
1779    search_entry: SearchEntry,
1780}
1781
1782impl Record {
1783    ///
1784    /// Create a new Record object with single valued attributes.
1785    /// This is essentially parsing the response records into usable types.
1786    //
1787    // This is kind of misnomer, as we aren't creating records here.
1788    // Perhaps something like "deserialize" would fit better?
1789    pub fn to_record<T: for<'b> serde::Deserialize<'b>>(self) -> Result<T, Error> {
1790        to_value(self.search_entry)
1791    }
1792
1793    #[deprecated(
1794        since = "6.0.0",
1795        note = "Use to_record instead. This method is deprecated and will be removed in future versions."
1796    )]
1797    pub fn to_multi_valued_record_<T: for<'b> serde::Deserialize<'b>>(self) -> Result<T, Error> {
1798        to_multi_value(self.search_entry)
1799    }
1800}
1801
1802pub enum StreamResult<T> {
1803    Record(T),
1804    Done,
1805    Finished,
1806}
1807
1808///
1809/// The error type for the LDAP client
1810///
1811#[derive(Debug, Error)]
1812pub enum Error {
1813    /// Error occurred when performing a LDAP query
1814    #[error("{0}")]
1815    Query(String, #[source] LdapError),
1816    /// No records found for the search criteria
1817    #[error("{0}")]
1818    NotFound(String),
1819    /// Multiple records found for the search criteria
1820    #[error("{0}")]
1821    MultipleResults(String),
1822    /// Bind failed.
1823    #[error("{0}")]
1824    AuthenticationFailed(String),
1825    /// Error occurred when creating a record
1826    #[error("{0}")]
1827    Create(String, #[source] LdapError),
1828    /// Error occurred when updating a record
1829    #[error("{0}")]
1830    Update(String, #[source] LdapError),
1831    /// Error occurred when deleting a record
1832    #[error("{0}")]
1833    Delete(String, #[source] LdapError),
1834    /// Error occurred when mapping the search result to a struct
1835    #[error("{0}")]
1836    Mapping(String),
1837    /// Error occurred while attempting to create an LDAP connection
1838    #[error("{0}")]
1839    Connection(String, #[source] LdapError),
1840    /// Error occurred while attempting to close an LDAP connection.
1841    /// Includes unbind issues.
1842    #[error("{0}")]
1843    Close(String, #[source] LdapError),
1844    /// Error occurred while abandoning the search result
1845    #[error("{0}")]
1846    Abandon(String, #[source] LdapError),
1847
1848    /// Something wrong with Server Side Sort
1849    #[error("{0}")]
1850    Sort(String),
1851}
1852
1853#[cfg(test)]
1854mod tests {
1855    //! Local tests that don't need to connect to a server.
1856
1857    use super::*;
1858    use anyhow::anyhow;
1859    use serde::Deserialize;
1860    use serde_with::OneOrMany;
1861    use serde_with::serde_as;
1862    use uuid::Uuid;
1863
1864    #[test]
1865    fn create_multi_value_test() {
1866        let mut map: HashMap<String, Vec<String>> = HashMap::new();
1867        map.insert(
1868            "key1".to_string(),
1869            vec!["value1".to_string(), "value2".to_string()],
1870        );
1871        map.insert(
1872            "key2".to_string(),
1873            vec!["value3".to_string(), "value4".to_string()],
1874        );
1875
1876        let dn = "CN=Thing,OU=Unit,DC=example,DC=org";
1877        let entry = SearchEntry {
1878            dn: dn.to_string(),
1879            attrs: map,
1880            bin_attrs: HashMap::new(),
1881        };
1882
1883        let test = to_multi_value::<TestMultiValued>(entry);
1884
1885        let test = test.unwrap();
1886        assert_eq!(test.key1, vec!["value1".to_string(), "value2".to_string()]);
1887        assert_eq!(test.key2, vec!["value3".to_string(), "value4".to_string()]);
1888        assert_eq!(test.dn, dn);
1889    }
1890
1891    #[test]
1892    fn create_single_value_test() {
1893        let mut map: HashMap<String, Vec<String>> = HashMap::new();
1894        map.insert("key1".to_string(), vec!["value1".to_string()]);
1895        map.insert("key2".to_string(), vec!["value2".to_string()]);
1896        map.insert("key4".to_string(), vec!["value4".to_string()]);
1897
1898        let dn = "CN=Thing,OU=Unit,DC=example,DC=org";
1899
1900        let entry = SearchEntry {
1901            dn: dn.to_string(),
1902            attrs: map,
1903            bin_attrs: HashMap::new(),
1904        };
1905
1906        let test = to_single_value::<TestSingleValued>(entry);
1907
1908        let test = test.unwrap();
1909        assert_eq!(test.key1, "value1".to_string());
1910        assert_eq!(test.key2, "value2".to_string());
1911        assert!(test.key3.is_none());
1912        assert_eq!(test.key4.unwrap(), "value4".to_string());
1913        assert_eq!(test.dn, dn);
1914    }
1915
1916    #[test]
1917    fn create_to_value_string_test() {
1918        let mut map: HashMap<String, Vec<String>> = HashMap::new();
1919        map.insert("key1".to_string(), vec!["value1".to_string()]);
1920        map.insert("key2".to_string(), vec!["value2".to_string()]);
1921        map.insert("key4".to_string(), vec!["value4".to_string()]);
1922        map.insert(
1923            "key5".to_string(),
1924            vec!["value5".to_string(), "value6".to_string()],
1925        );
1926
1927        let dn = "CN=Thing,OU=Unit,DC=example,DC=org";
1928
1929        let entry = SearchEntry {
1930            dn: dn.to_string(),
1931            attrs: map,
1932            bin_attrs: HashMap::new(),
1933        };
1934
1935        let test = to_value::<TestValued>(entry);
1936
1937        let test = test.unwrap();
1938        assert_eq!(test.key1, "value1".to_string());
1939        assert!(test.key3.is_none());
1940        let key4 = test.key4;
1941        assert_eq!(key4[0], "value4".to_string());
1942        let key5 = test.key5;
1943        assert_eq!(key5[0], "value5".to_string());
1944        assert_eq!(key5[1], "value6".to_string());
1945
1946        assert_eq!(test.dn, dn);
1947    }
1948
1949    #[test]
1950    fn binary_single_to_value_test() -> anyhow::Result<()> {
1951        #[derive(Deserialize)]
1952        struct TestMultivalueBinary {
1953            pub uuids: Uuid,
1954            pub key1: String,
1955        }
1956
1957        let (bytes, correct_string_representation) = get_binary_uuid();
1958
1959        let entry = SearchEntry {
1960            dn: String::from("CN=Thing,OU=Unit,DC=example,DC=org"),
1961            attrs: HashMap::from([(String::from("key1"), vec![String::from("value1")])]),
1962            bin_attrs: HashMap::from([(String::from("uuids"), vec![bytes])]),
1963        };
1964
1965        let test = to_value::<TestMultivalueBinary>(entry).unwrap();
1966
1967        let string_uuid = test.uuids.hyphenated().to_string();
1968        assert_eq!(string_uuid, correct_string_representation);
1969        Ok(())
1970    }
1971
1972    // #[test] // This test is not working, because the OneOrMany trait is not implemented for Uuid. Will fix this later.
1973    fn binary_multi_to_value_test() -> anyhow::Result<()> {
1974        #[serde_as]
1975        #[derive(Deserialize)]
1976        struct TestMultivalueBinary {
1977            #[serde_as(as = "OneOrMany<_>")]
1978            pub uuids: Vec<Uuid>,
1979            pub key1: String,
1980        }
1981
1982        let (bytes, correct_string_representation) = get_binary_uuid();
1983
1984        let entry = SearchEntry {
1985            dn: String::from("CN=Thing,OU=Unit,DC=example,DC=org"),
1986            attrs: HashMap::from([(String::from("key1"), vec![String::from("value1")])]),
1987            bin_attrs: HashMap::from([(String::from("uuids"), vec![bytes])]),
1988        };
1989
1990        let test = to_value::<TestMultivalueBinary>(entry).unwrap();
1991
1992        match test.uuids.as_slice() {
1993            [one] => {
1994                let string_uuid = one.hyphenated().to_string();
1995                assert_eq!(string_uuid, correct_string_representation);
1996                Ok(())
1997            }
1998            [..] => Err(anyhow!("There was supposed to be exactly one uuid.")),
1999        }
2000    }
2001
2002    #[derive(Debug, Deserialize)]
2003    struct TestMultiValued {
2004        dn: String,
2005        key1: Vec<String>,
2006        key2: Vec<String>,
2007    }
2008
2009    #[derive(Debug, Deserialize)]
2010    struct TestSingleValued {
2011        dn: String,
2012        key1: String,
2013        key2: String,
2014        key3: Option<String>,
2015        key4: Option<String>,
2016    }
2017
2018    #[serde_as]
2019    #[derive(Debug, Deserialize)]
2020    struct TestValued {
2021        dn: String,
2022        key1: String,
2023        key3: Option<String>,
2024        #[serde_as(as = "OneOrMany<_>")]
2025        key4: Vec<String>,
2026        #[serde_as(as = "OneOrMany<_>")]
2027        key5: Vec<String>,
2028    }
2029    /// Get the binary and hyphenated string representations of an UUID for testing.
2030    fn get_binary_uuid() -> (Vec<u8>, String) {
2031        // Example grabbed from uuid docs:
2032        // https://docs.rs/uuid/latest/uuid/struct.Uuid.html#method.from_bytes
2033        let bytes = vec![
2034            0xa1, 0xa2, 0xa3, 0xa4, 0xb1, 0xb2, 0xc1, 0xc2, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6,
2035            0xd7, 0xd8,
2036        ];
2037
2038        let correct_string_representation = String::from("a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8");
2039
2040        (bytes, correct_string_representation)
2041    }
2042
2043    #[test]
2044    fn deserialize_binary_multi_value_test() -> anyhow::Result<()> {
2045        #[derive(Deserialize)]
2046        struct TestMultivalueBinary {
2047            pub uuids: Vec<Uuid>,
2048        }
2049
2050        let (bytes, correct_string_representation) = get_binary_uuid();
2051
2052        let entry = SearchEntry {
2053            dn: String::from("CN=Thing,OU=Unit,DC=example,DC=org"),
2054            attrs: HashMap::new(),
2055            bin_attrs: HashMap::from([(String::from("uuids"), vec![bytes])]),
2056        };
2057
2058        let record = Record {
2059            search_entry: entry,
2060        };
2061
2062        let deserialized: TestMultivalueBinary = record.to_multi_valued_record_()?;
2063
2064        match deserialized.uuids.as_slice() {
2065            [one] => {
2066                let string_uuid = one.hyphenated().to_string();
2067                assert_eq!(string_uuid, correct_string_representation);
2068                Ok(())
2069            }
2070            [..] => Err(anyhow!("There was supposed to be exactly one uuid.")),
2071        }
2072    }
2073
2074    #[test]
2075    fn deserialize_binary_single_value_test() -> anyhow::Result<()> {
2076        #[derive(Deserialize)]
2077        struct TestSingleValueBinary {
2078            pub uuid: Uuid,
2079        }
2080
2081        let (bytes, correct_string_representation) = get_binary_uuid();
2082
2083        let entry = SearchEntry {
2084            dn: String::from("CN=Thing,OU=Unit,DC=example,DC=org"),
2085            attrs: HashMap::new(),
2086            bin_attrs: HashMap::from([(String::from("uuid"), vec![bytes])]),
2087        };
2088
2089        let record = Record {
2090            search_entry: entry,
2091        };
2092
2093        let deserialized: TestSingleValueBinary = record.to_record()?;
2094
2095        let string_uuid = deserialized.uuid.hyphenated().to_string();
2096        assert_eq!(string_uuid, correct_string_representation);
2097
2098        Ok(())
2099    }
2100}
2101
2102// Add readme examples to doctests:
2103// https://doc.rust-lang.org/rustdoc/write-documentation/documentation-tests.html#include-items-only-when-collecting-doctests
2104#[doc = include_str!("../README.md")]
2105#[cfg(doctest)]
2106pub struct ReadmeDoctests;