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