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