From e97f18ccae25eaa53753f0fddbb8c64b8afdd9c2 Mon Sep 17 00:00:00 2001 From: Kasun Date: Wed, 9 Oct 2024 23:15:19 +0530 Subject: [PATCH 01/13] Refactor streaming_search_inner to return a Stream object This commit refactors the `streaming_search_inner` method in the `LdapClient` struct to return a `Stream` object instead of a vector of `SearchEntry` objects. The `Stream` object encapsulates the search stream and provides methods for iterating over the search results. This change improves the usability and flexibility of the `streaming_search` method. --- src/lib.rs | 300 ++++++++++++++++++++++++++++------------------------- 1 file changed, 160 insertions(+), 140 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0b94ea6..de83cfd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,7 +38,7 @@ use deadpool::managed::{Object, PoolError}; use filter::{AndFilter, EqFilter, Filter}; use ldap3::{ log::{debug, error}, - Ldap, LdapError, Mod, Scope, SearchEntry, StreamState, + Ldap, LdapError, Mod, Scope, SearchEntry, SearchStream, StreamState, }; use pool::Manager; @@ -378,14 +378,14 @@ impl LdapClient { } } - async fn streaming_search_inner( - &mut self, - base: &str, + async fn streaming_search_inner<'a>( + mut self, + base: &'a str, scope: Scope, - filter: &(impl Filter + ?Sized), + filter: &'a (impl Filter + ?Sized), limit: i32, - attributes: &Vec<&str>, - ) -> Result, Error> { + attributes: &'a Vec<&'a str>, + ) -> Result>, Error> { let search_stream = self .ldap .streaming_search(base, scope, filter.filter().as_str(), attributes) @@ -396,40 +396,11 @@ impl LdapClient { error, )); } - let mut search_stream = search_stream.unwrap(); - - let mut entries = Vec::new(); - let mut count = 0; - - loop { - let next = search_stream.next().await; - if next.is_err() { - break; - } - - if search_stream.state() != StreamState::Active { - break; - } - - let entry = next.unwrap(); - if entry.is_none() { - break; - } - if let Some(entry) = entry { - entries.push(SearchEntry::construct(entry)); - count += 1; - } - - if count == limit { - break; - } - } - let _res = search_stream.finish().await; - let msgid = search_stream.ldap_handle().last_id(); - self.ldap.abandon(msgid).await.unwrap(); + let search_stream = search_stream.unwrap(); - Ok(entries) + let stream = Stream::new(self.ldap, search_stream, limit); + Ok(stream) } /// @@ -474,102 +445,19 @@ impl LdapClient { /// let user = ldap.streaming_search::("", self::ldap3::Scope::OneLevel, &name_filter, 2, vec!["cn", "sn", "uid"]).await; /// } /// ``` - pub async fn streaming_search serde::Deserialize<'a>>( - &mut self, - base: &str, + pub async fn streaming_search<'a>( + self, + base: &'a str, scope: Scope, - filter: &impl Filter, + filter: &'a impl Filter, limit: i32, - attributes: &Vec<&str>, - ) -> Result, Error> { - let entries = self + attributes: &'a Vec<&'a str>, + ) -> Result>, Error> { + let entry = self .streaming_search_inner(base, scope, filter, limit, attributes) .await?; - if entries.is_empty() { - return Ok(Vec::new()); - } - - let jsons = entries - .iter() - .map(|entry| LdapClient::create_json_signle_value(entry.to_owned()).unwrap()) - .collect::>(); - - let data = jsons - .iter() - .map(|json| LdapClient::map_to_struct::(json.to_owned()).unwrap()) - .collect::>(); - - Ok(data) - } - - /// - /// This method is used to search multiple records from the LDAP server. The search is performed using the provided filter. - /// This operatrion is useful when records has single value attributes. - /// Method will return a vector of structs of type T. return vector will be maximum of the limit provided. - /// - /// # Arguments - /// * `base` - The base DN to search for the user - /// * `scope` - The scope of the search - /// * `filter` - The filter to search for the user - /// * `limit` - The maximum number of records to return - /// * `attributes` - The attributes to return from the search - /// - /// # Returns - /// * `Result` - The result will be mapped to a struct of type T - /// - /// # Example - /// ``` - /// use simple_ldap::filter::EqFilter; - /// use simple_ldap::LdapClient; - /// use simple_ldap::pool::LdapConfig; - /// - /// - /// #[derive(Debug, Deserialize)] - /// struct TestMultiValued { - /// key1: Vec, - /// key2: Vec, - /// } - /// - /// async fn main(){ - /// let ldap_config = LdapConfig { - /// bind_dn: "cn=manager".to_string(), - /// bind_pw: "password".to_string(), - /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), - /// pool_size: 10, - /// dn_attribute: None - /// }; - /// - /// let pool = pool::build_connection_pool(&ldap_config).await; - /// let mut ldap = pool.get_connection().await; - /// - /// let name_filter = EqFilter::from("cn".to_string(), "Sam".to_string()); - /// let user = ldap.streaming_search_multi_valued::("", self::ldap3::Scope::OneLevel, &name_filter, 2, vec!["cn", "sn", "uid"]).await; - /// } - /// ``` - pub async fn streaming_search_multi_valued serde::Deserialize<'a>>( - &mut self, - base: &str, - scope: Scope, - filter: &impl Filter, - limit: i32, - attributes: &Vec<&str>, - ) -> Result, Error> { - let entries = self - .streaming_search_inner(base, scope, filter, limit, attributes) - .await?; - - let jsons = entries - .iter() - .map(|entry| LdapClient::create_json_multi_value(entry.to_owned()).unwrap()) - .collect::>(); - - let data = jsons - .iter() - .map(|json| LdapClient::map_to_struct::(json.to_owned()).unwrap()) - .collect::>(); - - Ok(data) + Ok(entry) } /// @@ -1225,6 +1113,114 @@ impl LdapClient { } } +pub struct Stream<'a, S, A> { + ldap: Object, + search_stream: SearchStream<'a, S, A>, + limit: i32, + count: i32, +} + +impl<'a, S, A> Stream<'a, S, A> +where + S: AsRef + Send + Sync + 'a, + A: AsRef<[S]> + Send + Sync + 'a, +{ + fn new( + ldap: Object, + search_stream: SearchStream<'a, S, A>, + limit: i32, + ) -> Stream<'a, S, A> { + Stream { + ldap, + search_stream, + limit, + count: 0, + } + } + + async fn next_inner(&mut self) -> Result, Error> { + if self.count == self.limit { + let _res = self.search_stream.finish().await; + let msgid = self.search_stream.ldap_handle().last_id(); + self.ldap.abandon(msgid).await.unwrap(); + return Ok(StreamResult::Done); + } + + let next = self.search_stream.next().await; + if let Err(err) = next { + return Err(Error::Query( + format!("Error getting next record: {:?}", err), + err, + )); + } + + if self.search_stream.state() != StreamState::Active { + let _res = self.search_stream.finish().await; + let msgid = self.search_stream.ldap_handle().last_id(); + self.ldap.abandon(msgid).await.unwrap(); + return Ok(StreamResult::Finished); + } + + let entry = next.unwrap(); + match entry { + Some(entry) => { + self.count += 1; + let entry = SearchEntry::construct(entry); + return Ok(StreamResult::Record(entry)); + } + None => { + let _res = self.search_stream.finish().await; + let msgid = self.search_stream.ldap_handle().last_id(); + self.ldap.abandon(msgid).await.unwrap(); + return Ok(StreamResult::Finished); + } + } + } + + pub async fn next serde::Deserialize<'b>>( + &mut self, + ) -> Result, Error> { + let entry = self.next_inner().await?; + + match entry { + StreamResult::Record(entry) => { + let json = LdapClient::create_json_signle_value(entry).unwrap(); + let data = LdapClient::map_to_struct::(json); + if let Err(err) = data { + return Err(Error::Mapping(format!("Error mapping record: {:?}", err))); + } + return Ok(StreamResult::Record(data.unwrap())); + } + StreamResult::Done => Ok(StreamResult::Done), + StreamResult::Finished => Ok(StreamResult::Finished), + } + } + + pub async fn multi_valued_next serde::Deserialize<'b>>( + &mut self, + ) -> Result, Error> { + let entry = self.next_inner().await?; + match entry { + StreamResult::Record(entry) => { + let json = LdapClient::create_json_multi_value(entry).unwrap(); + let data = LdapClient::map_to_struct::(json); + if let Err(err) = data { + return Err(Error::Mapping(format!("Error mapping record: {:?}", err))); + } + return Ok(StreamResult::Record(data.unwrap())); + } + StreamResult::Done => Ok(StreamResult::Done), + StreamResult::Finished => Ok(StreamResult::Finished), + } + } +} + +pub enum StreamResult { + Record(T), + Done, + Finished, +} + /// /// The error type for the LDAP client /// @@ -1556,21 +1552,33 @@ mod tests { }; let pool = pool::build_connection_pool(&ldap_config).await; - let mut ldap = pool.get_connection().await.unwrap(); + let ldap = pool.get_connection().await.unwrap(); let name_filter = EqFilter::from("cn".to_string(), "James".to_string()); + let attra = vec!["cn", "sn", "uid"]; let result = ldap - .streaming_search::( + .streaming_search( "ou=people,dc=example,dc=com", self::ldap3::Scope::OneLevel, &name_filter, 2, - &vec!["cn", "sn", "uid"], + &attra, ) .await; assert!(result.is_ok()); - let result = result.unwrap(); - assert!(result.len() == 2); + let mut result = result.unwrap(); + let mut count = 0; + loop { + match result.next::().await { + Ok(StreamResult::Record(_)) => { + count += 1; + } + _ => { + break; + } + } + } + assert!(count == 2); } #[tokio::test] @@ -1584,21 +1592,33 @@ mod tests { }; let pool = pool::build_connection_pool(&ldap_config).await; - let mut ldap = pool.get_connection().await.unwrap(); + let ldap = pool.get_connection().await.unwrap(); let name_filter = EqFilter::from("cn".to_string(), "JamesX".to_string()); + let attra = vec!["cn", "sn", "uid"]; let result = ldap - .streaming_search::( + .streaming_search( "ou=people,dc=example,dc=com", self::ldap3::Scope::OneLevel, &name_filter, 2, - &vec!["cn", "sn", "uid"], + &attra, ) .await; assert!(result.is_ok()); - let result = result.unwrap(); - assert_eq!(result.len(), 0); + let mut result = result.unwrap(); + let mut count = 0; + loop { + match result.next::().await { + Ok(StreamResult::Record(_)) => { + count += 1; + } + _ => { + break; + } + } + } + assert_eq!(count, 0); } #[tokio::test] From 94d41dbbd846700879788e8e89be7ace584e1283 Mon Sep 17 00:00:00 2001 From: Kasun Date: Wed, 9 Oct 2024 23:19:47 +0530 Subject: [PATCH 02/13] Bump version to 1.7.0 in Cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 11ebdb4..0a4086c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ readme = "README.md" repository = "https://github.com/keaz/simple-ldap" keywords = ["ldap", "ldap3", "async", "high-level"] name = "simple-ldap" -version = "1.6.1" +version = "1.7.0" edition = "2021" From 931de5de40c7ffc16ca1a57c14f8256cf6fe4d0d Mon Sep 17 00:00:00 2001 From: Kasun Date: Wed, 9 Oct 2024 23:24:18 +0530 Subject: [PATCH 03/13] updated the README --- README.md | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d0519ce..d03d3c7 100644 --- a/README.md +++ b/README.md @@ -131,21 +131,33 @@ async fn main() -> Result<()> { }; let pool = pool::build_connection_pool(&ldap_config).await; - let mut ldap = pool.pool.get_connection().await.unwrap(); - - let name_filter = EqFilter::from("cn".to_string(), "James".to_string()); - let result = ldap - .streaming_search::( - "ou=people,dc=example,dc=com", - self::ldap3::Scope::OneLevel, - &name_filter, - 2, - vec!["cn", "sn", "uid"], - ) - .await; - assert!(result.is_ok()); - let result = result.unwrap(); - assert!(result.len() == 2); + let ldap = pool.get_connection().await.unwrap(); + + let name_filter = EqFilter::from("cn".to_string(), "James".to_string()); + let attra = vec!["cn", "sn", "uid"]; + let result = ldap + .streaming_search( + "ou=people,dc=example,dc=com", + self::ldap3::Scope::OneLevel, + &name_filter, + 2, + &attra, + ) + .await; + + let mut result = result.unwrap(); + let mut count = 0; + loop { + match result.next::().await { + Ok(StreamResult::Record(_)) => { + count += 1; + } + _ => { + break; + } + } + } + assert!(count == 2); Ok(ldap.unbind().await?) } ``` From d881589409e34c8181b4abf853b35db54caa7377 Mon Sep 17 00:00:00 2001 From: Kasun Date: Fri, 11 Oct 2024 23:30:49 +0530 Subject: [PATCH 04/13] Implemented stream for stream_search --- Cargo.toml | 13 +++--- data/data.ldif | 71 +++++++++++++++++++++-------- src/lib.rs | 121 ++++++++++++++++++++++++++++++++++--------------- 3 files changed, 144 insertions(+), 61 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0a4086c..0026cf4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,19 +6,20 @@ readme = "README.md" repository = "https://github.com/keaz/simple-ldap" keywords = ["ldap", "ldap3", "async", "high-level"] name = "simple-ldap" -version = "1.7.0" +version = "2.0.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -async-trait = "0.1.75" +async-trait = "0.1.83" deadpool = "0.10.0" -ldap3 = { version = "0.11.3", default-features = false } -log = "0.4.20" -serde = { version = "1.0.193", features = ["derive"] } -serde_json = "1.0.108" +futures = "0.3.31" +ldap3 = { version = "0.11.5", default-features = false } +log = "0.4.22" +serde = { version = "1.0.210", features = ["derive"] } +serde_json = "1.0.128" [features] default = ["ldap3/default"] diff --git a/data/data.ldif b/data/data.ldif index 0d36729..b0729eb 100644 --- a/data/data.ldif +++ b/data/data.ldif @@ -1,84 +1,117 @@ -version: 1 - dn: dc=example,dc=com -objectClass: domain objectClass: top +objectClass: domain dc: example +entryUUID: 724bddc1-a3d1-3d8d-b43c-ae7f3fa6954f dn: ou=people,dc=example,dc=com -objectClass: organizationalUnit objectClass: top +objectClass: organizationalUnit ou: people description: Users - +entryUUID: eb8f50dc-6283-3541-a1b3-d0ac3cae9e28 dn: uid=f92f4cb2-e821-44a4-bb13-b8ebadf4ecc5,ou=people,dc=example,dc=com +objectClass: top objectClass: inetorgperson objectClass: organizationalPerson objectClass: person -objectClass: top -cn: Sam sn: Smith +cn: Sam ou: people uid: f92f4cb2-e821-44a4-bb13-b8ebadf4ecc5 +entryUUID: d03a2650-afae-3e43-8fe6-3df3b486a8f9 dn: uid=42b05942-4f18-4279-827d-36534da1e437,ou=people,dc=example,dc=com +objectClass: top objectClass: inetorgperson objectClass: organizationalPerson objectClass: person -objectClass: top -cn: James sn: Gunn +cn: James ou: people uid: 42b05942-4f18-4279-827d-36534da1e437 +entryUUID: b34503f5-c572-302c-a135-4f78665f0032 dn: uid=0dbaece8-f5f8-4e85-97d8-8bd614304bef,ou=people,dc=example,dc=com +objectClass: top objectClass: inetorgperson objectClass: organizationalPerson objectClass: person -objectClass: top -cn: James sn: Gunn +cn: James ou: people uid: 0dbaece8-f5f8-4e85-97d8-8bd614304bef +entryUUID: cb0bafca-46aa-3db4-9c3c-abab3a273a70 dn: uid=0xbaece8-f5f8-4e85-97d8-8bd614304bef,ou=people,dc=example,dc=com +objectClass: top objectClass: inetorgperson objectClass: organizationalPerson objectClass: person -objectClass: top -cn: James sn: Gunn +cn: James ou: people uid: 0xbaece8-f5f8-4e85-97d8-8bd614304bef +entryUUID: 352b9f11-3394-3bf1-857b-9d4df00bb4a4 dn: uid=e219fbc0-6df5-4bc3-a6ee-986843bb157e,ou=people,dc=example,dc=com +objectClass: top objectClass: inetorgperson objectClass: organizationalPerson objectClass: person -objectClass: top -cn: Jhone sn: Eliet +cn: Jhone ou: people uid: e219fbc0-6df5-4bc3-a6ee-986843bb157e +entryUUID: 158464c3-f19b-388d-856a-4bed06c24dcb dn: uid=cb4bc91e-21d8-4bcc-bf6a-317b84c2e58b,ou=people,dc=example,dc=com +objectClass: top objectClass: inetorgperson objectClass: organizationalPerson objectClass: person -objectClass: top -cn: David sn: Hanks +cn: David ou: people uid: cb4bc91e-21d8-4bcc-bf6a-317b84c2e58b +entryUUID: 82b49fc0-c700-3b64-881a-7a9fb315ba85 dn: uid=4d9b08fe-9a14-4df0-9831-ea9992837f0d,ou=people,dc=example,dc=com +objectClass: top objectClass: inetorgperson objectClass: organizationalPerson objectClass: person -objectClass: top -cn: David sn: Hanks +cn: David ou: people uid: 4d9b08fe-9a14-4df0-9831-ea9992837f0d +entryUUID: 7036d8ba-8170-31d5-9f7f-38e11f63d13f + +dn: ou=group,dc=example,dc=com +objectClass: top +objectClass: organizationalUnit +ou: group +description: Groups +createTimestamp: 20241011170327Z +creatorsName: cn=Directory Manager,cn=Root DNs,cn=config +entryUUID: 68d83ad5-29d5-4505-a58c-1ef19121683a + +dn: cn=grp1,ou=group,dc=example,dc=com +objectClass: top +objectClass: groupOfNames +cn: grp1 +member: uid=e219fbc0-6df5-4bc3-a6ee-986843bb157e,ou=people,dc=example,dc=com +createTimestamp: 20241011170342Z +creatorsName: cn=Directory Manager,cn=Root DNs,cn=config +entryUUID: 4618dc11-b54c-4db7-97f2-da09a33112e7 + +dn: cn=grp2,ou=group,dc=example,dc=com +objectClass: top +objectClass: groupOfNames +cn: grp2 +member: uid=e219fbc0-6df5-4bc3-a6ee-986843bb157e,ou=people,dc=example,dc=com +createTimestamp: 20241011170356Z +creatorsName: cn=Directory Manager,cn=Root DNs,cn=config +entryUUID: 415d5386-3812-499c-a951-9a5945400ff8 diff --git a/src/lib.rs b/src/lib.rs index de83cfd..111c39f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,10 +32,15 @@ //! * [x] Remove Users from Group //! * [x] Get Group Members //! -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + pin::Pin, + task::{Context, Poll}, +}; use deadpool::managed::{Object, PoolError}; use filter::{AndFilter, EqFilter, Filter}; +use futures::FutureExt; use ldap3::{ log::{debug, error}, Ldap, LdapError, Mod, Scope, SearchEntry, SearchStream, StreamState, @@ -383,7 +388,7 @@ impl LdapClient { base: &'a str, scope: Scope, filter: &'a (impl Filter + ?Sized), - limit: i32, + limit: usize, attributes: &'a Vec<&'a str>, ) -> Result>, Error> { let search_stream = self @@ -450,7 +455,7 @@ impl LdapClient { base: &'a str, scope: Scope, filter: &'a impl Filter, - limit: i32, + limit: usize, attributes: &'a Vec<&'a str>, ) -> Result>, Error> { let entry = self @@ -1116,8 +1121,8 @@ impl LdapClient { pub struct Stream<'a, S, A> { ldap: Object, search_stream: SearchStream<'a, S, A>, - limit: i32, - count: i32, + limit: usize, + count: usize, } impl<'a, S, A> Stream<'a, S, A> @@ -1128,7 +1133,7 @@ where fn new( ldap: Object, search_stream: SearchStream<'a, S, A>, - limit: i32, + limit: usize, ) -> Stream<'a, S, A> { Stream { ldap, @@ -1155,9 +1160,7 @@ where } if self.search_stream.state() != StreamState::Active { - let _res = self.search_stream.finish().await; - let msgid = self.search_stream.ldap_handle().last_id(); - self.ldap.abandon(msgid).await.unwrap(); + self.limit = self.count; // Set the limit to the count, to that poll_next will return None return Ok(StreamResult::Finished); } @@ -1169,22 +1172,19 @@ where return Ok(StreamResult::Record(entry)); } None => { - let _res = self.search_stream.finish().await; - let msgid = self.search_stream.ldap_handle().last_id(); - self.ldap.abandon(msgid).await.unwrap(); + self.limit = self.count; // Set the limit to the count, to that poll_next will return None return Ok(StreamResult::Finished); } } } - pub async fn next serde::Deserialize<'b>>( + pub async fn multi_valued_next serde::Deserialize<'b>>( &mut self, ) -> Result, Error> { let entry = self.next_inner().await?; - match entry { StreamResult::Record(entry) => { - let json = LdapClient::create_json_signle_value(entry).unwrap(); + let json = LdapClient::create_json_multi_value(entry).unwrap(); let data = LdapClient::map_to_struct::(json); if let Err(err) = data { return Err(Error::Mapping(format!("Error mapping record: {:?}", err))); @@ -1195,26 +1195,71 @@ where StreamResult::Finished => Ok(StreamResult::Finished), } } +} - pub async fn multi_valued_next serde::Deserialize<'b>>( - &mut self, - ) -> Result, Error> { - let entry = self.next_inner().await?; - match entry { - StreamResult::Record(entry) => { - let json = LdapClient::create_json_multi_value(entry).unwrap(); - let data = LdapClient::map_to_struct::(json); - if let Err(err) = data { - return Err(Error::Mapping(format!("Error mapping record: {:?}", err))); +impl<'a, S, A> futures::stream::Stream for Stream<'a, S, A> +where + S: AsRef + Send + Sync + 'a, + A: AsRef<[S]> + Send + Sync + 'a, +{ + type Item = Result; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let poll = self.next_inner().boxed().as_mut().poll(cx); + match poll { + Poll::Ready(result) => match result { + Ok(result) => match result { + StreamResult::Record(record) => Poll::Ready(Some(Ok(Record { + search_entry: record, + }))), + StreamResult::Done => Poll::Ready(None), + StreamResult::Finished => Poll::Ready(None), + }, + Err(er) => { + return Poll::Ready(Some(Err(er))); } - return Ok(StreamResult::Record(data.unwrap())); + }, + Poll::Pending => { + if self.count == self.limit { + return Poll::Ready(None); + } + // Ensure the task is woken when the next record is ready + cx.waker().wake_by_ref(); + Poll::Pending } - StreamResult::Done => Ok(StreamResult::Done), - StreamResult::Finished => Ok(StreamResult::Finished), } } } +pub struct Record { + search_entry: SearchEntry, +} + +impl Record { + pub fn to_record serde::Deserialize<'b>>(self) -> Result { + let json = LdapClient::create_json_signle_value(self.search_entry).unwrap(); + let data = LdapClient::map_to_struct::(json); + if let Err(err) = data { + return Err(Error::Mapping(format!("Error mapping record: {:?}", err))); + } + return Ok(data.unwrap()); + } + + pub fn to_multi_valued_record_ serde::Deserialize<'b>>( + self, + ) -> Result, Error> { + let json = LdapClient::create_json_multi_value(self.search_entry).unwrap(); + let data = LdapClient::map_to_struct::(json); + if let Err(err) = data { + return Err(Error::Mapping(format!("Error mapping record: {:?}", err))); + } + return Ok(StreamResult::Record(data.unwrap())); + } +} + pub enum StreamResult { Record(T), Done, @@ -1251,6 +1296,7 @@ pub enum Error { #[cfg(test)] mod tests { + use futures::StreamExt; use ldap3::tokio; use serde::Deserialize; @@ -1568,12 +1614,13 @@ mod tests { assert!(result.is_ok()); let mut result = result.unwrap(); let mut count = 0; - loop { - match result.next::().await { - Ok(StreamResult::Record(_)) => { + while let Some(record) = result.next().await { + match record { + Ok(record) => { + let _ = record.to_record::().unwrap(); count += 1; } - _ => { + Err(_) => { break; } } @@ -1608,12 +1655,14 @@ mod tests { assert!(result.is_ok()); let mut result = result.unwrap(); let mut count = 0; - loop { - match result.next::().await { - Ok(StreamResult::Record(_)) => { + + while let Some(record) = result.next().await { + match record { + Ok(record) => { + let _ = record.to_record::().unwrap(); count += 1; } - _ => { + Err(_) => { break; } } From 8175f35e8e3d660a71649186c9d40512c43b774b Mon Sep 17 00:00:00 2001 From: Kasun Date: Fri, 11 Oct 2024 23:32:15 +0530 Subject: [PATCH 05/13] Updated ReadME --- README.md | 53 +++++++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index d03d3c7..f0a9e25 100644 --- a/README.md +++ b/README.md @@ -129,34 +129,35 @@ async fn main() -> Result<()> { pool_size: 10, dn_attribute: None, }; - - let pool = pool::build_connection_pool(&ldap_config).await; - let ldap = pool.get_connection().await.unwrap(); - - let name_filter = EqFilter::from("cn".to_string(), "James".to_string()); - let attra = vec!["cn", "sn", "uid"]; - let result = ldap - .streaming_search( - "ou=people,dc=example,dc=com", - self::ldap3::Scope::OneLevel, - &name_filter, - 2, - &attra, - ) - .await; - - let mut result = result.unwrap(); - let mut count = 0; - loop { - match result.next::().await { - Ok(StreamResult::Record(_)) => { - count += 1; - } - _ => { - break; + + let pool = pool::build_connection_pool(&ldap_config).await; + let ldap = pool.get_connection().await.unwrap(); + + let name_filter = EqFilter::from("cn".to_string(), "James".to_string()); + let attra = vec!["cn", "sn", "uid"]; + let result = ldap + .streaming_search( + "ou=people,dc=example,dc=com", + self::ldap3::Scope::OneLevel, + &name_filter, + 2, + &attra, + ) + .await; + assert!(result.is_ok()); + let mut result = result.unwrap(); + let mut count = 0; + while let Some(record) = result.next().await { + match record { + Ok(record) => { + let _ = record.to_record::().unwrap(); + count += 1; + } + Err(_) => { + break; + } } } - } assert!(count == 2); Ok(ldap.unbind().await?) } From 66cdc07adcacad5f17aa6e69828cdc411e30924b Mon Sep 17 00:00:00 2001 From: Kasun Date: Thu, 17 Oct 2024 22:32:11 +0530 Subject: [PATCH 06/13] Incoperated review comments --- data/data.ldif | 11 --- src/lib.rs | 177 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 141 insertions(+), 47 deletions(-) diff --git a/data/data.ldif b/data/data.ldif index b0729eb..1ec0b49 100644 --- a/data/data.ldif +++ b/data/data.ldif @@ -33,17 +33,6 @@ ou: people uid: 42b05942-4f18-4279-827d-36534da1e437 entryUUID: b34503f5-c572-302c-a135-4f78665f0032 -dn: uid=0dbaece8-f5f8-4e85-97d8-8bd614304bef,ou=people,dc=example,dc=com -objectClass: top -objectClass: inetorgperson -objectClass: organizationalPerson -objectClass: person -sn: Gunn -cn: James -ou: people -uid: 0dbaece8-f5f8-4e85-97d8-8bd614304bef -entryUUID: cb0bafca-46aa-3db4-9c3c-abab3a273a70 - dn: uid=0xbaece8-f5f8-4e85-97d8-8bd614304bef,ou=people,dc=example,dc=com objectClass: top objectClass: inetorgperson diff --git a/src/lib.rs b/src/lib.rs index 111c39f..b2a78c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,8 +40,9 @@ use std::{ use deadpool::managed::{Object, PoolError}; use filter::{AndFilter, EqFilter, Filter}; -use futures::FutureExt; +use futures::{future::BoxFuture, FutureExt}; use ldap3::{ + adapters::PagedResults, log::{debug, error}, Ldap, LdapError, Mod, Scope, SearchEntry, SearchStream, StreamState, }; @@ -388,10 +389,9 @@ impl LdapClient { base: &'a str, scope: Scope, filter: &'a (impl Filter + ?Sized), - limit: usize, attributes: &'a Vec<&'a str>, ) -> Result>, Error> { - let search_stream = self + let search_stream: Result>, LdapError> = self .ldap .streaming_search(base, scope, filter.filter().as_str(), attributes) .await; @@ -404,23 +404,22 @@ impl LdapClient { let search_stream = search_stream.unwrap(); - let stream = Stream::new(self.ldap, search_stream, limit); + let stream = Stream::new(self.ldap, search_stream); Ok(stream) } /// /// This method is used to search multiple records from the LDAP server. The search is performed using the provided filter. - /// Method will return a vector of structs of type T. return vector will be maximum of the limit provided. + /// Method will return a Stream. The stream can be used to iterate through the search results. /// /// # Arguments /// * `base` - The base DN to search for the user /// * `scope` - The scope of the search /// * `filter` - The filter to search for the user - /// * `limit` - The maximum number of records to return /// * `attributes` - The attributes to return from the search /// /// # Returns - /// * `Result` - The result will be mapped to a struct of type T + /// * `Result` - The result will be mapped to a Stream. /// /// # Example /// ``` @@ -447,7 +446,7 @@ impl LdapClient { /// let mut ldap = pool.get_connection().await; /// /// let name_filter = EqFilter::from("cn".to_string(), "Sam".to_string()); - /// let user = ldap.streaming_search::("", self::ldap3::Scope::OneLevel, &name_filter, 2, vec!["cn", "sn", "uid"]).await; + /// let user = ldap.streaming_search::("", self::ldap3::Scope::OneLevel, &name_filter,vec!["cn", "sn", "uid"]).await; /// } /// ``` pub async fn streaming_search<'a>( @@ -455,16 +454,89 @@ impl LdapClient { base: &'a str, scope: Scope, filter: &'a impl Filter, - limit: usize, attributes: &'a Vec<&'a str>, ) -> Result>, Error> { let entry = self - .streaming_search_inner(base, scope, filter, limit, attributes) + .streaming_search_inner(base, scope, filter, attributes) .await?; Ok(entry) } + /// + /// This method is used to search multiple records from the LDAP server and results will be pageinated. + /// Method will return a Stream. The stream can be used to iterate through the search results. + /// + /// # Arguments + /// * `base` - The base DN to search for the user + /// * `scope` - The scope of the search + /// * `filter` - The filter to search for the user + /// * `page_size` - The maximum number of records in a page + /// * `attributes` - The attributes to return from the search + /// + /// # Returns + /// * `Result` - A stream that can be used to iterate through the search results. + /// + /// # Example + /// ``` + /// use simple_ldap::filter::EqFilter; + /// use simple_ldap::LdapClient; + /// use simple_ldap::pool::LdapConfig; + /// + /// + /// #[derive(Debug, Deserialize)] + /// struct TestMultiValued { + /// key1: Vec, + /// key2: Vec, + /// } + /// async fn main(){ + /// let ldap_config = LdapConfig { + /// bind_dn: "cn=manager".to_string(), + /// bind_pw: "password".to_string(), + /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// pool_size: 10, + /// dn_attribute: None + /// }; + /// + /// let pool = pool::build_connection_pool(&ldap_config).await; + /// let mut ldap = pool.get_connection().await; + /// + /// let name_filter = EqFilter::from("cn".to_string(), "Sam".to_string()); + /// let user = ldap.streaming_search::("", self::ldap3::Scope::OneLevel, &name_filter, 3, vec!["cn", "sn", "uid"]).await; + /// } + /// ``` + pub async fn streaming_search_with<'a>( + mut self, + base: &'a str, + scope: Scope, + filter: &'a (impl Filter + ?Sized), + attributes: &'a Vec<&'a str>, + page_size: i32, + ) -> Result>, Error> { + let search_stream = self + .ldap + .streaming_search_with( + PagedResults::new(page_size), + base, + scope, + filter.filter().as_str(), + attributes, + ) + .await; + + if let Err(error) = search_stream { + return Err(Error::Query( + format!("Error searching for record: {:?}", error), + error, + )); + } + + let search_stream = search_stream.unwrap(); + + let stream = Stream::new(self.ldap, search_stream); + Ok(stream) + } + /// /// Create a new record in the LDAP server. The record will be created in the provided base DN. /// @@ -1121,8 +1193,7 @@ impl LdapClient { pub struct Stream<'a, S, A> { ldap: Object, search_stream: SearchStream<'a, S, A>, - limit: usize, - count: usize, + cleanup_future: Option>, } impl<'a, S, A> Stream<'a, S, A> @@ -1130,27 +1201,15 @@ where S: AsRef + Send + Sync + 'a, A: AsRef<[S]> + Send + Sync + 'a, { - fn new( - ldap: Object, - search_stream: SearchStream<'a, S, A>, - limit: usize, - ) -> Stream<'a, S, A> { + fn new(ldap: Object, search_stream: SearchStream<'a, S, A>) -> Stream<'a, S, A> { Stream { ldap, search_stream, - limit, - count: 0, + cleanup_future: None, } } async fn next_inner(&mut self) -> Result, Error> { - if self.count == self.limit { - let _res = self.search_stream.finish().await; - let msgid = self.search_stream.ldap_handle().last_id(); - self.ldap.abandon(msgid).await.unwrap(); - return Ok(StreamResult::Done); - } - let next = self.search_stream.next().await; if let Err(err) = next { return Err(Error::Query( @@ -1160,19 +1219,19 @@ where } if self.search_stream.state() != StreamState::Active { - self.limit = self.count; // Set the limit to the count, to that poll_next will return None + // self.limit = self.count; // Set the limit to the count, to that poll_next will return None return Ok(StreamResult::Finished); } let entry = next.unwrap(); match entry { Some(entry) => { - self.count += 1; + // self.count += 1; let entry = SearchEntry::construct(entry); return Ok(StreamResult::Record(entry)); } None => { - self.limit = self.count; // Set the limit to the count, to that poll_next will return None + // self.limit = self.count; // Set the limit to the count, to that poll_next will return None return Ok(StreamResult::Finished); } } @@ -1195,6 +1254,13 @@ where StreamResult::Finished => Ok(StreamResult::Finished), } } + + pub async fn cleanup(&mut self) { + println!("Cleaning up"); + let _res = self.search_stream.finish().await; + let msgid = self.search_stream.ldap_handle().last_id(); + self.ldap.abandon(msgid).await.unwrap(); + } } impl<'a, S, A> futures::stream::Stream for Stream<'a, S, A> @@ -1223,11 +1289,10 @@ where } }, Poll::Pending => { - if self.count == self.limit { - return Poll::Ready(None); - } + // if self.count == self.limit { + // return Poll::Ready(None); + // } // Ensure the task is woken when the next record is ready - cx.waker().wake_by_ref(); Poll::Pending } } @@ -1296,6 +1361,7 @@ pub enum Error { #[cfg(test)] mod tests { + use filter::{ContainsFilter, LikeFilter, WildardOn}; use futures::StreamExt; use ldap3::tokio; use serde::Deserialize; @@ -1607,7 +1673,6 @@ mod tests { "ou=people,dc=example,dc=com", self::ldap3::Scope::OneLevel, &name_filter, - 2, &attra, ) .await; @@ -1617,7 +1682,7 @@ mod tests { while let Some(record) = result.next().await { match record { Ok(record) => { - let _ = record.to_record::().unwrap(); + let user = record.to_record::().unwrap(); count += 1; } Err(_) => { @@ -1628,6 +1693,47 @@ mod tests { assert!(count == 2); } + #[tokio::test] + async fn test_streaming_search_with() { + let ldap_config = LdapConfig { + bind_dn: "cn=manager".to_string(), + bind_pw: "password".to_string(), + ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + pool_size: 10, + dn_attribute: None, + }; + + let pool = pool::build_connection_pool(&ldap_config).await; + let ldap = pool.get_connection().await.unwrap(); + + let name_filter = ContainsFilter::from("cn".to_string(), "J".to_string()); + let attra = vec!["cn", "sn", "uid"]; + let result = ldap + .streaming_search_with( + "ou=people,dc=example,dc=com", + self::ldap3::Scope::OneLevel, + &name_filter, + &attra, + 3, + ) + .await; + assert!(result.is_ok()); + let mut result = result.unwrap(); + let mut count = 0; + while let Some(record) = result.next().await { + match record { + Ok(record) => { + let _ = record.to_record::().unwrap(); + count += 1; + } + Err(_) => { + break; + } + } + } + assert!(count == 3); + } + #[tokio::test] async fn test_streaming_search_no_records() { let ldap_config = LdapConfig { @@ -1648,7 +1754,6 @@ mod tests { "ou=people,dc=example,dc=com", self::ldap3::Scope::OneLevel, &name_filter, - 2, &attra, ) .await; From c7961cd16f88676171b8174590e2f0a2651d6b4e Mon Sep 17 00:00:00 2001 From: Kasun Date: Mon, 21 Oct 2024 21:28:26 +0530 Subject: [PATCH 07/13] Addressed review comments --- src/lib.rs | 165 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 127 insertions(+), 38 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b2a78c3..e463a5c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,7 @@ use deadpool::managed::{Object, PoolError}; use filter::{AndFilter, EqFilter, Filter}; use futures::{future::BoxFuture, FutureExt}; use ldap3::{ - adapters::PagedResults, + adapters::{Adapter, EntriesOnly, PagedResults}, log::{debug, error}, Ldap, LdapError, Mod, Scope, SearchEntry, SearchStream, StreamState, }; @@ -411,6 +411,7 @@ impl LdapClient { /// /// This method is used to search multiple records from the LDAP server. The search is performed using the provided filter. /// Method will return a Stream. The stream can be used to iterate through the search results. + /// This method take the ownership of the LdapClient. /// /// # Arguments /// * `base` - The base DN to search for the user @@ -446,7 +447,19 @@ impl LdapClient { /// let mut ldap = pool.get_connection().await; /// /// let name_filter = EqFilter::from("cn".to_string(), "Sam".to_string()); - /// let user = ldap.streaming_search::("", self::ldap3::Scope::OneLevel, &name_filter,vec!["cn", "sn", "uid"]).await; + /// let mut stream = ldap.streaming_search("", self::ldap3::Scope::OneLevel, &name_filter, vec!["cn", "sn", "uid"], 3).await.unwrap(); + /// while let Some(record) = stream.next().await { + /// match record { + /// Ok(record) => { + /// let user: User = record.to_record().unwrap(); + /// println!("User: {:?}", user); + /// } + /// Err(err) => { + /// println!("Error: {:?}", err); + /// } + /// } + /// } + /// stream.cleanup().await; /// } /// ``` pub async fn streaming_search<'a>( @@ -464,8 +477,9 @@ impl LdapClient { } /// - /// This method is used to search multiple records from the LDAP server and results will be pageinated. + /// This method is used to search multiple records from the LDAP server and results will be paginated. /// Method will return a Stream. The stream can be used to iterate through the search results. + /// This method take the ownership of the LdapClient. /// /// # Arguments /// * `base` - The base DN to search for the user @@ -502,7 +516,19 @@ impl LdapClient { /// let mut ldap = pool.get_connection().await; /// /// let name_filter = EqFilter::from("cn".to_string(), "Sam".to_string()); - /// let user = ldap.streaming_search::("", self::ldap3::Scope::OneLevel, &name_filter, 3, vec!["cn", "sn", "uid"]).await; + /// let mut stream = ldap.streaming_search("", self::ldap3::Scope::OneLevel, &name_filter, vec!["cn", "sn", "uid"], 3).await.unwrap(); + /// while let Some(record) = stream.next().await { + /// match record { + /// Ok(record) => { + /// let user: User = record.to_record().unwrap(); + /// println!("User: {:?}", user); + /// } + /// Err(err) => { + /// println!("Error: {:?}", err); + /// } + /// } + /// } + /// stream.cleanup().await; /// } /// ``` pub async fn streaming_search_with<'a>( @@ -513,15 +539,13 @@ impl LdapClient { attributes: &'a Vec<&'a str>, page_size: i32, ) -> Result>, Error> { + let adapters: Vec>> = vec![ + Box::new(EntriesOnly::new()), + Box::new(PagedResults::new(page_size)), + ]; let search_stream = self .ldap - .streaming_search_with( - PagedResults::new(page_size), - base, - scope, - filter.filter().as_str(), - attributes, - ) + .streaming_search_with(adapters, base, scope, filter.filter().as_str(), attributes) .await; if let Err(error) = search_stream { @@ -1190,10 +1214,13 @@ impl LdapClient { } } +/// The Stream struct is used to iterate through the search results. +/// The stream will return a Record object. The Record object can be used to map the search result to a struct. +/// After the stream is finished, the cleanup method should be called to cleanup the stream. +/// pub struct Stream<'a, S, A> { ldap: Object, search_stream: SearchStream<'a, S, A>, - cleanup_future: Option>, } impl<'a, S, A> Stream<'a, S, A> @@ -1205,7 +1232,6 @@ where Stream { ldap, search_stream, - cleanup_future: None, } } @@ -1237,24 +1263,40 @@ where } } - pub async fn multi_valued_next serde::Deserialize<'b>>( - &mut self, - ) -> Result, Error> { - let entry = self.next_inner().await?; - match entry { - StreamResult::Record(entry) => { - let json = LdapClient::create_json_multi_value(entry).unwrap(); - let data = LdapClient::map_to_struct::(json); - if let Err(err) = data { - return Err(Error::Mapping(format!("Error mapping record: {:?}", err))); - } - return Ok(StreamResult::Record(data.unwrap())); - } - StreamResult::Done => Ok(StreamResult::Done), - StreamResult::Finished => Ok(StreamResult::Finished), - } - } - + /// Cleanup the stream. This method should be called after the stream is finished. + /// This method will cleanup the stream and close the connection. + /// # Example + /// ``` + /// use simple_ldap::LdapClient; + /// use simple_ldap::pool::LdapConfig; + /// + /// async fn main(){ + /// let ldap_config = LdapConfig { + /// bind_dn: "cn=manager".to_string(), + /// bind_pw: "password".to_string(), + /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// pool_size: 10, + /// dn_attribute: None + /// }; + /// + /// let pool = pool::build_connection_pool(&ldap_config).await; + /// let mut ldap = pool.get_connection().await; + /// + /// let mut stream = ldap.streaming_search("", self::ldap3::Scope::OneLevel, &name_filter, vec!["cn", "sn", "uid"], 3).await.unwrap(); + /// while let Some(record) = stream.next().await { + /// match record { + /// Ok(record) => { + /// let user: User = record.to_record().unwrap(); + /// println!("User: {:?}", user); + /// } + /// Err(err) => { + /// println!("Error: {:?}", err); + /// } + /// } + /// } + /// stream.cleanup().await; + /// } + /// ``` pub async fn cleanup(&mut self) { println!("Cleaning up"); let _res = self.search_stream.finish().await; @@ -1288,22 +1330,68 @@ where return Poll::Ready(Some(Err(er))); } }, - Poll::Pending => { - // if self.count == self.limit { - // return Poll::Ready(None); - // } - // Ensure the task is woken when the next record is ready - Poll::Pending - } + Poll::Pending => Poll::Pending, } } } +/// The Record struct is used to map the search result to a struct. +/// The Record struct has a method to_record which will map the search result to a struct. +/// The Record struct has a method to_multi_valued_record which will map the search result to a struct with multi valued attributes. pub struct Record { search_entry: SearchEntry, } impl Record { + /// Create a new Record object with single valued attributes + /// # Example + /// ``` + /// use simple_ldap::LdapClient; + /// use simple_ldap::pool::LdapConfig; + /// use simple_ldap::filter::EqFilter; + /// + /// #[derive(Debug, Deserialize)] + /// struct User { + /// uid: String, + /// cn: String, + /// sn: String, + /// } + /// + /// async fn main(){ + /// + /// let ldap_config = LdapConfig { + /// bind_dn: "cn=manager".to_string(), + /// bind_pw: "password".to_string(), + /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// pool_size: 10, + /// dn_attribute: None, + /// }; + /// let pool = pool::build_connection_pool(&ldap_config).await; + /// let ldap = pool.get_connection().await.unwrap(); + /// + /// let name_filter = EqFilter::from("cn".to_string(), "James".to_string()); + /// let attra = vec!["cn", "sn", "uid"]; + /// let result = ldap.streaming_search( + /// "ou=people,dc=example,dc=com", + /// self::ldap3::Scope::OneLevel, + /// &name_filter, + /// &attra,).await; + /// + /// let mut result = result.unwrap(); + /// let mut count = 0; + /// while let Some(record) = result.next().await { + /// match record { + /// Ok(record) => { + /// let user = record.to_record::().unwrap(); + /// count += 1; + /// } + /// Err(_) => { + /// break; + /// } + /// } + /// } + /// result.cleanup().await; + /// } pub fn to_record serde::Deserialize<'b>>(self) -> Result { let json = LdapClient::create_json_signle_value(self.search_entry).unwrap(); let data = LdapClient::map_to_struct::(json); @@ -1690,6 +1778,7 @@ mod tests { } } } + result.cleanup().await; assert!(count == 2); } From c9683e12d93b7746bd5c7f4bc41b01ec920ce963 Mon Sep 17 00:00:00 2001 From: Kasun Date: Mon, 21 Oct 2024 21:33:36 +0530 Subject: [PATCH 08/13] Updated ReadME --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f0a9e25..381944a 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ async fn main() -> Result<()> { } } assert!(count == 2); - Ok(ldap.unbind().await?) + Ok(result.cleanup()?) } ``` From 33a6b3e405c9ab8ce791f5a4d68bf6e225464d85 Mon Sep 17 00:00:00 2001 From: Kasun Ranasinghe Date: Wed, 23 Oct 2024 09:48:09 +0530 Subject: [PATCH 09/13] Handled errors in cleanup methos Improved README --- README.md | 2 +- src/lib.rs | 29 +++++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 381944a..b2a04a6 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ async fn main() -> Result<()> { } } assert!(count == 2); - Ok(result.cleanup()?) + Ok(result.cleanup().await?) } ``` diff --git a/src/lib.rs b/src/lib.rs index e463a5c..ffc77b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,7 +40,7 @@ use std::{ use deadpool::managed::{Object, PoolError}; use filter::{AndFilter, EqFilter, Filter}; -use futures::{future::BoxFuture, FutureExt}; +use futures::FutureExt; use ldap3::{ adapters::{Adapter, EntriesOnly, PagedResults}, log::{debug, error}, @@ -1297,11 +1297,28 @@ where /// stream.cleanup().await; /// } /// ``` - pub async fn cleanup(&mut self) { - println!("Cleaning up"); + pub async fn cleanup(&mut self) -> Result<(), Error> { + let state = self.search_stream.state(); + if state == StreamState::Done || state == StreamState::Closed { + return Ok(()); + } let _res = self.search_stream.finish().await; let msgid = self.search_stream.ldap_handle().last_id(); - self.ldap.abandon(msgid).await.unwrap(); + let result = self.ldap.abandon(msgid).await; + + match result { + Ok(_) => { + debug!("Sucessfully abandoned search result: {:?}", msgid); + Ok(()) + } + Err(err) => { + error!("Error abandoning search result: {:?}", err); + Err(Error::Abandon( + format!("Error abandoning search result: {:?}", err), + err, + )) + } + } } } @@ -1444,6 +1461,8 @@ pub enum Error { Connection(String, LdapError), /// Error occurred while using the connection pool Pool(PoolError), + /// Error occurred while abandoning the search result + Abandon(String, LdapError), } #[cfg(test)] @@ -1821,6 +1840,7 @@ mod tests { } } assert!(count == 3); + result.cleanup().await; } #[tokio::test] @@ -1862,6 +1882,7 @@ mod tests { } } assert_eq!(count, 0); + result.cleanup().await; } #[tokio::test] From d84c665d69f4e92934b3be6a476fb0716e189863 Mon Sep 17 00:00:00 2001 From: Kasun Date: Wed, 23 Oct 2024 23:09:23 +0530 Subject: [PATCH 10/13] Fixed CI --- .github/workflows/ci.yml | 56 +++++++++++++++++++++++++++++++++++ .github/workflows/publish.yml | 55 ++++++++++++++++++++++++++++++++++ .github/workflows/rust.yml | 22 -------------- data/data.ldif | 23 -------------- 4 files changed, 111 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish.yml delete mode 100644 .github/workflows/rust.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4be55e5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI/CD for Rust LDAP Client + +on: + push: + branches: + - '*' + - '!main' + pull_request: + branches: + - '*' + +jobs: + build: + runs-on: ubuntu-latest + + services: + ldap: + image: openidentityplatform/opendj + ports: + - 1389:1389 + options: > + --env ROOT_USER_DN="cn=manager" + + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Get LDAP container ID + id: ldap_container_id + run: echo "LDAP_CONTAINER_ID=$(docker ps --filter 'ancestor=openidentityplatform/opendj:latest' -q)" >> $GITHUB_ENV + + - name: Copy LDIF to LDAP container + run: docker cp ./data/data.ldif ${{ env.LDAP_CONTAINER_ID }}:/tmp/data.ldif + + - name: Import LDIF into OpenDJ + run: | + docker exec ${{ job.services.ldap.id }} \ + /opt/opendj/bin/ldapmodify -h localhost -p 1389 -D "cn=manager" -w password -a -f /tmp/data.ldif + + # Step 3: Install Rust + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + + # Step 4: Build the Rust project + - name: Build + run: cargo build --verbose + + # Step 5: Run unit tests + - name: Run tests + run: cargo test --verbose + + diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..7f9c0ae --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,55 @@ +name: CI/CD for Rust LDAP Client + +on: + push: + branches: + - 'main' + +jobs: + build: + runs-on: ubuntu-latest + + services: + ldap: + image: openidentityplatform/opendj + ports: + - 1389:1389 + options: > + --env ROOT_USER_DN="cn=manager" + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Get LDAP container ID + id: ldap_container_id + run: echo "LDAP_CONTAINER_ID=$(docker ps --filter 'ancestor=openidentityplatform/opendj:latest' -q)" >> $GITHUB_ENV + + - name: Copy LDIF to LDAP container + run: docker cp ./data/data.ldif ${{ env.LDAP_CONTAINER_ID }}:/tmp/data.ldif + + - name: Import LDIF into OpenDJ + run: | + docker exec ${{ job.services.ldap.id }} \ + /opt/opendj/bin/ldapmodify -h localhost -p 1389 -D "cn=manager" -w password -a -f /tmp/data.ldif + + # Step 3: Install Rust + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + + # Step 4: Build the Rust project + - name: Build + run: cargo build --verbose + + # Step 5: Run unit tests + - name: Run tests + run: cargo test --verbose + + - name: Deploy to crates.io + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: cargo publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index 31000a2..0000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Rust - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose diff --git a/data/data.ldif b/data/data.ldif index 1ec0b49..a978148 100644 --- a/data/data.ldif +++ b/data/data.ldif @@ -1,15 +1,8 @@ -dn: dc=example,dc=com -objectClass: top -objectClass: domain -dc: example -entryUUID: 724bddc1-a3d1-3d8d-b43c-ae7f3fa6954f - dn: ou=people,dc=example,dc=com objectClass: top objectClass: organizationalUnit ou: people description: Users -entryUUID: eb8f50dc-6283-3541-a1b3-d0ac3cae9e28 dn: uid=f92f4cb2-e821-44a4-bb13-b8ebadf4ecc5,ou=people,dc=example,dc=com objectClass: top @@ -20,7 +13,6 @@ sn: Smith cn: Sam ou: people uid: f92f4cb2-e821-44a4-bb13-b8ebadf4ecc5 -entryUUID: d03a2650-afae-3e43-8fe6-3df3b486a8f9 dn: uid=42b05942-4f18-4279-827d-36534da1e437,ou=people,dc=example,dc=com objectClass: top @@ -31,7 +23,6 @@ sn: Gunn cn: James ou: people uid: 42b05942-4f18-4279-827d-36534da1e437 -entryUUID: b34503f5-c572-302c-a135-4f78665f0032 dn: uid=0xbaece8-f5f8-4e85-97d8-8bd614304bef,ou=people,dc=example,dc=com objectClass: top @@ -42,7 +33,6 @@ sn: Gunn cn: James ou: people uid: 0xbaece8-f5f8-4e85-97d8-8bd614304bef -entryUUID: 352b9f11-3394-3bf1-857b-9d4df00bb4a4 dn: uid=e219fbc0-6df5-4bc3-a6ee-986843bb157e,ou=people,dc=example,dc=com objectClass: top @@ -53,7 +43,6 @@ sn: Eliet cn: Jhone ou: people uid: e219fbc0-6df5-4bc3-a6ee-986843bb157e -entryUUID: 158464c3-f19b-388d-856a-4bed06c24dcb dn: uid=cb4bc91e-21d8-4bcc-bf6a-317b84c2e58b,ou=people,dc=example,dc=com objectClass: top @@ -64,7 +53,6 @@ sn: Hanks cn: David ou: people uid: cb4bc91e-21d8-4bcc-bf6a-317b84c2e58b -entryUUID: 82b49fc0-c700-3b64-881a-7a9fb315ba85 dn: uid=4d9b08fe-9a14-4df0-9831-ea9992837f0d,ou=people,dc=example,dc=com objectClass: top @@ -75,32 +63,21 @@ sn: Hanks cn: David ou: people uid: 4d9b08fe-9a14-4df0-9831-ea9992837f0d -entryUUID: 7036d8ba-8170-31d5-9f7f-38e11f63d13f dn: ou=group,dc=example,dc=com objectClass: top objectClass: organizationalUnit ou: group description: Groups -createTimestamp: 20241011170327Z -creatorsName: cn=Directory Manager,cn=Root DNs,cn=config -entryUUID: 68d83ad5-29d5-4505-a58c-1ef19121683a dn: cn=grp1,ou=group,dc=example,dc=com objectClass: top objectClass: groupOfNames cn: grp1 member: uid=e219fbc0-6df5-4bc3-a6ee-986843bb157e,ou=people,dc=example,dc=com -createTimestamp: 20241011170342Z -creatorsName: cn=Directory Manager,cn=Root DNs,cn=config -entryUUID: 4618dc11-b54c-4db7-97f2-da09a33112e7 dn: cn=grp2,ou=group,dc=example,dc=com objectClass: top objectClass: groupOfNames cn: grp2 member: uid=e219fbc0-6df5-4bc3-a6ee-986843bb157e,ou=people,dc=example,dc=com -createTimestamp: 20241011170356Z -creatorsName: cn=Directory Manager,cn=Root DNs,cn=config -entryUUID: 415d5386-3812-499c-a951-9a5945400ff8 - From fc4a2338863524d13e72f43ee73304e69b02aeef Mon Sep 17 00:00:00 2001 From: Kasun Date: Wed, 23 Oct 2024 23:13:14 +0530 Subject: [PATCH 11/13] Fixed CI --- .github/workflows/publish.yml | 2 +- src/lib.rs | 60 +++++++++++++++++------------------ 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7f9c0ae..8dfeb3f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -name: CI/CD for Rust LDAP Client +name: Publish to crates.io on: push: diff --git a/src/lib.rs b/src/lib.rs index ffc77b4..4292674 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,7 +109,7 @@ impl LdapClient { /// let ldap_config = LdapConfig { /// bind_dn: "cn=manager".to_string(), /// bind_pw: "password".to_string(), - /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), /// pool_size: 10, /// dn_attribute: None /// }; @@ -249,7 +249,7 @@ impl LdapClient { /// let ldap_config = LdapConfig { /// bind_dn: "cn=manager".to_string(), /// bind_pw: "password".to_string(), - /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), /// pool_size: 10, /// dn_attribute: None /// }; @@ -312,7 +312,7 @@ impl LdapClient { /// let ldap_config = LdapConfig { /// bind_dn: "cn=manager".to_string(), /// bind_pw: "password".to_string(), - /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), /// pool_size: 10, /// dn_attribute: None /// }; @@ -438,7 +438,7 @@ impl LdapClient { /// let ldap_config = LdapConfig { /// bind_dn: "cn=manager".to_string(), /// bind_pw: "password".to_string(), - /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), /// pool_size: 10, /// dn_attribute: None /// }; @@ -507,7 +507,7 @@ impl LdapClient { /// let ldap_config = LdapConfig { /// bind_dn: "cn=manager".to_string(), /// bind_pw: "password".to_string(), - /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), /// pool_size: 10, /// dn_attribute: None /// }; @@ -581,7 +581,7 @@ impl LdapClient { /// let ldap_config = LdapConfig { /// bind_dn: "cn=manager".to_string(), /// bind_pw: "password".to_string(), - /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), /// pool_size: 10, /// dn_attribute: None /// }; @@ -648,7 +648,7 @@ impl LdapClient { /// let ldap_config = LdapConfig { /// bind_dn: "cn=manager".to_string(), /// bind_pw: "password".to_string(), - /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), /// pool_size: 10, /// dn_attribute: None /// }; @@ -755,7 +755,7 @@ impl LdapClient { /// let ldap_config = LdapConfig { /// bind_dn: "cn=manager".to_string(), /// bind_pw: "password".to_string(), - /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), /// pool_size: 10, /// dn_attribute: None /// }; @@ -819,7 +819,7 @@ impl LdapClient { /// let ldap_config = LdapConfig { /// bind_dn: "cn=manager".to_string(), /// bind_pw: "password".to_string(), - /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), /// pool_size: 10, /// dn_attribute: None /// }; @@ -883,7 +883,7 @@ impl LdapClient { /// let ldap_config = LdapConfig { /// bind_dn: "cn=manager".to_string(), /// bind_pw: "password".to_string(), - /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), /// pool_size: 10, /// dn_attribute: None /// }; @@ -961,7 +961,7 @@ impl LdapClient { /// let ldap_config = LdapConfig { /// bind_dn: "cn=manager".to_string(), /// bind_pw: "password".to_string(), - /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), /// pool_size: 10, /// dn_attribute: None /// }; @@ -1069,7 +1069,7 @@ impl LdapClient { /// let ldap_config = LdapConfig { /// bind_dn: "cn=manager".to_string(), /// bind_pw: "password".to_string(), - /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), /// pool_size: 10, /// dn_attribute: None /// }; @@ -1138,7 +1138,7 @@ impl LdapClient { /// let ldap_config = LdapConfig { /// bind_dn: "cn=manager".to_string(), /// bind_pw: "password".to_string(), - /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), /// pool_size: 10, /// dn_attribute: None /// }; @@ -1274,7 +1274,7 @@ where /// let ldap_config = LdapConfig { /// bind_dn: "cn=manager".to_string(), /// bind_pw: "password".to_string(), - /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), /// pool_size: 10, /// dn_attribute: None /// }; @@ -1379,7 +1379,7 @@ impl Record { /// let ldap_config = LdapConfig { /// bind_dn: "cn=manager".to_string(), /// bind_pw: "password".to_string(), - /// ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + /// ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), /// pool_size: 10, /// dn_attribute: None, /// }; @@ -1538,7 +1538,7 @@ mod tests { let ldap_config = LdapConfig { bind_dn: "cn=manager".to_string(), bind_pw: "password".to_string(), - ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), pool_size: 10, dn_attribute: None, }; @@ -1575,7 +1575,7 @@ mod tests { let ldap_config = LdapConfig { bind_dn: "cn=manager".to_string(), bind_pw: "password".to_string(), - ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), pool_size: 10, dn_attribute: None, }; @@ -1602,7 +1602,7 @@ mod tests { let ldap_config = LdapConfig { bind_dn: "cn=manager".to_string(), bind_pw: "password".to_string(), - ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), pool_size: 10, dn_attribute: None, }; @@ -1631,7 +1631,7 @@ mod tests { let ldap_config = LdapConfig { bind_dn: "cn=manager".to_string(), bind_pw: "password".to_string(), - ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), pool_size: 10, dn_attribute: None, }; @@ -1660,7 +1660,7 @@ mod tests { let ldap_config = LdapConfig { bind_dn: "cn=manager".to_string(), bind_pw: "password".to_string(), - ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), pool_size: 10, dn_attribute: None, }; @@ -1687,7 +1687,7 @@ mod tests { let ldap_config = LdapConfig { bind_dn: "cn=manager".to_string(), bind_pw: "password".to_string(), - ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), pool_size: 10, dn_attribute: None, }; @@ -1719,7 +1719,7 @@ mod tests { let ldap_config = LdapConfig { bind_dn: "cn=manager".to_string(), bind_pw: "password".to_string(), - ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), pool_size: 10, dn_attribute: None, }; @@ -1765,7 +1765,7 @@ mod tests { let ldap_config = LdapConfig { bind_dn: "cn=manager".to_string(), bind_pw: "password".to_string(), - ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), pool_size: 10, dn_attribute: None, }; @@ -1806,7 +1806,7 @@ mod tests { let ldap_config = LdapConfig { bind_dn: "cn=manager".to_string(), bind_pw: "password".to_string(), - ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), pool_size: 10, dn_attribute: None, }; @@ -1848,7 +1848,7 @@ mod tests { let ldap_config = LdapConfig { bind_dn: "cn=manager".to_string(), bind_pw: "password".to_string(), - ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), pool_size: 10, dn_attribute: None, }; @@ -1890,7 +1890,7 @@ mod tests { let ldap_config = LdapConfig { bind_dn: "cn=manager".to_string(), bind_pw: "password".to_string(), - ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), pool_size: 10, dn_attribute: None, }; @@ -1912,7 +1912,7 @@ mod tests { let ldap_config = LdapConfig { bind_dn: "cn=manager".to_string(), bind_pw: "password".to_string(), - ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), pool_size: 10, dn_attribute: None, }; @@ -1997,7 +1997,7 @@ mod tests { bind_dn: "cn=manager".to_string(), bind_pw: "password".to_string(), // ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), - ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), pool_size: 1, dn_attribute: None, }; @@ -2054,7 +2054,7 @@ mod tests { let ldap_config = LdapConfig { bind_dn: "cn=manager".to_string(), bind_pw: "password".to_string(), - ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), pool_size: 1, dn_attribute: None, }; @@ -2101,7 +2101,7 @@ mod tests { let ldap_config = LdapConfig { bind_dn: "cn=manager".to_string(), bind_pw: "password".to_string(), - ldap_url: "ldap://ldap_server:1389/dc=example,dc=com".to_string(), + ldap_url: "ldap://localhost:1389/dc=example,dc=com".to_string(), pool_size: 1, dn_attribute: None, }; From 908709b62562ff722313c419dfaf827e4d3c4c0d Mon Sep 17 00:00:00 2001 From: Kasun Date: Thu, 24 Oct 2024 18:22:26 +0530 Subject: [PATCH 12/13] Added Github CI status to README --- .github/workflows/ci.yml | 2 +- README.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4be55e5..3ce5e44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI/CD for Rust LDAP Client +name: CI for Rust LDAP Client on: push: diff --git a/README.md b/README.md index b2a04a6..6860421 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ A ldap client library that wraps [ldap3](https://github.com/inejge/ldap3) to make it easy to use. -### Status of the project -Currently this is in early alpha stage. Library only support use asynchronously. +![CI](https://github.com/keaz/simple-ldap/actions/workflows/ci.yml/badge.svg) +[![Crates.io](https://img.shields.io/crates/v/simple-ldap)](https://crates.io/crates/simple-ldap) +[![Documentation](https://docs.rs/simple-ldap/badge.svg)](https://docs.rs/simple-ldap) ## Usage ``` From 20bda5d91f663a1628edf06192a8f8913f2933f4 Mon Sep 17 00:00:00 2001 From: Kasun Ranasinghe Date: Fri, 25 Oct 2024 06:53:43 +0530 Subject: [PATCH 13/13] Update ci.yml Removed pull request from ci.yml --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ce5e44..1981174 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,9 +5,6 @@ on: branches: - '*' - '!main' - pull_request: - branches: - - '*' jobs: build: