diff --git a/server/src/infra/ldap/compare.rs b/server/src/infra/ldap/compare.rs new file mode 100644 index 0000000..ed1756e --- /dev/null +++ b/server/src/infra/ldap/compare.rs @@ -0,0 +1,239 @@ +use crate::domain::ldap::error::{LdapError, LdapResult}; +use ldap3_proto::proto::{LdapCompareRequest, LdapOp, LdapResult as LdapResultOp, LdapResultCode}; +use lldap_domain::types::AttributeName; + +pub fn compare( + request: LdapCompareRequest, + search_results: Vec, + base_dn_str: &str, +) -> LdapResult> { + if search_results.len() > 2 { + // SearchResultEntry + SearchResultDone + return Err(LdapError { + code: LdapResultCode::OperationsError, + message: "Too many search results".to_string(), + }); + } + let requested_attribute = AttributeName::from(&request.atype); + match search_results.first() { + Some(LdapOp::SearchResultEntry(entry)) => { + let available = entry.attributes.iter().any(|attr| { + AttributeName::from(&attr.atype) == requested_attribute + && attr.vals.contains(&request.val) + }); + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: if available { + LdapResultCode::CompareTrue + } else { + LdapResultCode::CompareFalse + }, + matcheddn: request.dn, + message: "".to_string(), + referral: vec![], + })]) + } + Some(LdapOp::SearchResultDone(_)) => Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::NoSuchObject, + matcheddn: base_dn_str.to_string(), + message: "".to_string(), + referral: vec![], + })]), + None => Err(LdapError { + code: LdapResultCode::OperationsError, + message: "Search request returned nothing".to_string(), + }), + _ => Err(LdapError { + code: LdapResultCode::OperationsError, + message: "Unexpected results from search".to_string(), + }), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::infra::{ + ldap::handler::tests::setup_bound_admin_handler, test_utils::MockTestBackendHandler, + }; + use chrono::TimeZone; + use lldap_domain::{types::*, uuid}; + use lldap_domain_handlers::handler::*; + use pretty_assertions::assert_eq; + use tokio; + + #[tokio::test] + async fn test_compare_user() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users().returning(|f, g| { + assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob")))); + assert!(!g); + Ok(vec![UserAndGroups { + user: User { + user_id: UserId::new("bob"), + email: "bob@bobmail.bob".into(), + ..Default::default() + }, + groups: None, + }]) + }); + mock.expect_list_groups().returning(|_| Ok(vec![])); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let dn = "uid=bob,ou=people,dc=example,dc=com"; + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "uid".to_owned(), + val: b"bob".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::CompareTrue, + matcheddn: dn.to_string(), + message: "".to_string(), + referral: vec![], + })]) + ); + // Non-canonical attribute. + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "eMail".to_owned(), + val: b"bob@bobmail.bob".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::CompareTrue, + matcheddn: dn.to_string(), + message: "".to_string(), + referral: vec![], + })]) + ); + } + + #[tokio::test] + async fn test_compare_group() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users().returning(|_, _| Ok(vec![])); + mock.expect_list_groups().returning(|f| { + assert_eq!(f, Some(GroupRequestFilter::DisplayName("group".into()))); + Ok(vec![Group { + id: GroupId(1), + display_name: "group".into(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + users: vec![UserId::new("bob")], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), + attributes: Vec::new(), + }]) + }); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let dn = "uid=group,ou=groups,dc=example,dc=com"; + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "uid".to_owned(), + val: b"group".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::CompareTrue, + matcheddn: dn.to_string(), + message: "".to_string(), + referral: vec![], + })]) + ); + } + + #[tokio::test] + async fn test_compare_not_found() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users().returning(|f, g| { + assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob")))); + assert!(!g); + Ok(vec![]) + }); + mock.expect_list_groups().returning(|_| Ok(vec![])); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let dn = "uid=bob,ou=people,dc=example,dc=com"; + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "uid".to_owned(), + val: b"bob".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::NoSuchObject, + matcheddn: "dc=example,dc=com".to_owned(), + message: "".to_string(), + referral: vec![], + })]) + ); + } + + #[tokio::test] + async fn test_compare_no_match() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users().returning(|f, g| { + assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob")))); + assert!(!g); + Ok(vec![UserAndGroups { + user: User { + user_id: UserId::new("bob"), + email: "bob@bobmail.bob".into(), + ..Default::default() + }, + groups: None, + }]) + }); + mock.expect_list_groups().returning(|_| Ok(vec![])); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let dn = "uid=bob,ou=people,dc=example,dc=com"; + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "mail".to_owned(), + val: b"bob@bob".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::CompareFalse, + matcheddn: dn.to_string(), + message: "".to_string(), + referral: vec![], + })]) + ); + } + + #[tokio::test] + async fn test_compare_group_member() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users().returning(|_, _| Ok(vec![])); + mock.expect_list_groups().returning(|f| { + assert_eq!(f, Some(GroupRequestFilter::DisplayName("group".into()))); + Ok(vec![Group { + id: GroupId(1), + display_name: "group".into(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + users: vec![UserId::new("bob")], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), + attributes: Vec::new(), + }]) + }); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let dn = "uid=group,ou=groups,dc=example,dc=com"; + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "uniqueMember".to_owned(), + val: b"uid=bob,ou=people,dc=example,dc=com".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::CompareTrue, + matcheddn: dn.to_owned(), + message: "".to_string(), + referral: vec![], + })]) + ); + } +} diff --git a/server/src/infra/ldap/handler.rs b/server/src/infra/ldap/handler.rs index 29a37b3..344502b 100644 --- a/server/src/infra/ldap/handler.rs +++ b/server/src/infra/ldap/handler.rs @@ -20,6 +20,7 @@ use crate::{ self, is_root_dse_request, make_search_error, make_search_request, make_search_success, root_dse_response, }, + compare, }, }, }; @@ -492,47 +493,7 @@ impl LdapHandler 2 { - // SearchResultEntry + SearchResultDone - return Err(LdapError { - code: LdapResultCode::OperationsError, - message: "Too many search results".to_string(), - }); - } - let requested_attribute = AttributeName::from(&request.atype); - match entries.first() { - Some(LdapOp::SearchResultEntry(entry)) => { - let available = entry.attributes.iter().any(|attr| { - AttributeName::from(&attr.atype) == requested_attribute - && attr.vals.contains(&request.val) - }); - Ok(vec![LdapOp::CompareResult(LdapResultOp { - code: if available { - LdapResultCode::CompareTrue - } else { - LdapResultCode::CompareFalse - }, - matcheddn: request.dn, - message: "".to_string(), - referral: vec![], - })]) - } - Some(LdapOp::SearchResultDone(_)) => Ok(vec![LdapOp::CompareResult(LdapResultOp { - code: LdapResultCode::NoSuchObject, - matcheddn: self.ldap_info.base_dn_str.clone(), - message: "".to_string(), - referral: vec![], - })]), - None => Err(LdapError { - code: LdapResultCode::OperationsError, - message: "Search request returned nothing".to_string(), - }), - _ => Err(LdapError { - code: LdapResultCode::OperationsError, - message: "Unexpected results from search".to_string(), - }), - } + compare::compare(request, self.do_search(&req).await?, &self.ldap_info.base_dn_str) } pub async fn handle_ldap_message(&mut self, ldap_op: LdapOp) -> Option> { @@ -768,179 +729,4 @@ pub mod tests { ); } - #[tokio::test] - async fn test_compare_user() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users().returning(|f, g| { - assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob")))); - assert!(!g); - Ok(vec![UserAndGroups { - user: User { - user_id: UserId::new("bob"), - email: "bob@bobmail.bob".into(), - ..Default::default() - }, - groups: None, - }]) - }); - mock.expect_list_groups().returning(|_| Ok(vec![])); - let mut ldap_handler = setup_bound_admin_handler(mock).await; - let dn = "uid=bob,ou=people,dc=example,dc=com"; - let request = LdapCompareRequest { - dn: dn.to_string(), - atype: "uid".to_owned(), - val: b"bob".to_vec(), - }; - assert_eq!( - ldap_handler.do_compare(request).await, - Ok(vec![LdapOp::CompareResult(LdapResultOp { - code: LdapResultCode::CompareTrue, - matcheddn: dn.to_string(), - message: "".to_string(), - referral: vec![], - })]) - ); - // Non-canonical attribute. - let request = LdapCompareRequest { - dn: dn.to_string(), - atype: "eMail".to_owned(), - val: b"bob@bobmail.bob".to_vec(), - }; - assert_eq!( - ldap_handler.do_compare(request).await, - Ok(vec![LdapOp::CompareResult(LdapResultOp { - code: LdapResultCode::CompareTrue, - matcheddn: dn.to_string(), - message: "".to_string(), - referral: vec![], - })]) - ); - } - - #[tokio::test] - async fn test_compare_group() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users().returning(|_, _| Ok(vec![])); - mock.expect_list_groups().returning(|f| { - assert_eq!(f, Some(GroupRequestFilter::DisplayName("group".into()))); - Ok(vec![Group { - id: GroupId(1), - display_name: "group".into(), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - users: vec![UserId::new("bob")], - uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), - attributes: Vec::new(), - }]) - }); - let mut ldap_handler = setup_bound_admin_handler(mock).await; - let dn = "uid=group,ou=groups,dc=example,dc=com"; - let request = LdapCompareRequest { - dn: dn.to_string(), - atype: "uid".to_owned(), - val: b"group".to_vec(), - }; - assert_eq!( - ldap_handler.do_compare(request).await, - Ok(vec![LdapOp::CompareResult(LdapResultOp { - code: LdapResultCode::CompareTrue, - matcheddn: dn.to_string(), - message: "".to_string(), - referral: vec![], - })]) - ); - } - - #[tokio::test] - async fn test_compare_not_found() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users().returning(|f, g| { - assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob")))); - assert!(!g); - Ok(vec![]) - }); - mock.expect_list_groups().returning(|_| Ok(vec![])); - let mut ldap_handler = setup_bound_admin_handler(mock).await; - let dn = "uid=bob,ou=people,dc=example,dc=com"; - let request = LdapCompareRequest { - dn: dn.to_string(), - atype: "uid".to_owned(), - val: b"bob".to_vec(), - }; - assert_eq!( - ldap_handler.do_compare(request).await, - Ok(vec![LdapOp::CompareResult(LdapResultOp { - code: LdapResultCode::NoSuchObject, - matcheddn: "dc=example,dc=com".to_owned(), - message: "".to_string(), - referral: vec![], - })]) - ); - } - - #[tokio::test] - async fn test_compare_no_match() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users().returning(|f, g| { - assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob")))); - assert!(!g); - Ok(vec![UserAndGroups { - user: User { - user_id: UserId::new("bob"), - email: "bob@bobmail.bob".into(), - ..Default::default() - }, - groups: None, - }]) - }); - mock.expect_list_groups().returning(|_| Ok(vec![])); - let mut ldap_handler = setup_bound_admin_handler(mock).await; - let dn = "uid=bob,ou=people,dc=example,dc=com"; - let request = LdapCompareRequest { - dn: dn.to_string(), - atype: "mail".to_owned(), - val: b"bob@bob".to_vec(), - }; - assert_eq!( - ldap_handler.do_compare(request).await, - Ok(vec![LdapOp::CompareResult(LdapResultOp { - code: LdapResultCode::CompareFalse, - matcheddn: dn.to_string(), - message: "".to_string(), - referral: vec![], - })]) - ); - } - - #[tokio::test] - async fn test_compare_group_member() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users().returning(|_, _| Ok(vec![])); - mock.expect_list_groups().returning(|f| { - assert_eq!(f, Some(GroupRequestFilter::DisplayName("group".into()))); - Ok(vec![Group { - id: GroupId(1), - display_name: "group".into(), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - users: vec![UserId::new("bob")], - uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), - attributes: Vec::new(), - }]) - }); - let mut ldap_handler = setup_bound_admin_handler(mock).await; - let dn = "uid=group,ou=groups,dc=example,dc=com"; - let request = LdapCompareRequest { - dn: dn.to_string(), - atype: "uniqueMember".to_owned(), - val: b"uid=bob,ou=people,dc=example,dc=com".to_vec(), - }; - assert_eq!( - ldap_handler.do_compare(request).await, - Ok(vec![LdapOp::CompareResult(LdapResultOp { - code: LdapResultCode::CompareTrue, - matcheddn: dn.to_owned(), - message: "".to_string(), - referral: vec![], - })]) - ); - } } diff --git a/server/src/infra/ldap/mod.rs b/server/src/infra/ldap/mod.rs index 44813b9..8195949 100644 --- a/server/src/infra/ldap/mod.rs +++ b/server/src/infra/ldap/mod.rs @@ -1,3 +1,4 @@ +pub mod compare; pub mod handler; pub mod password; pub mod search;