simple_ldap/
lib.rs

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