diff --git a/src/domain/handler.rs b/src/domain/handler.rs index ed463f4..1e93788 100644 --- a/src/domain/handler.rs +++ b/src/domain/handler.rs @@ -10,9 +10,17 @@ pub struct BindRequest { pub password: String, } +#[cfg_attr(test, derive(PartialEq, Eq, Debug))] +pub enum RequestFilter { + And(Vec), + Or(Vec), + Not(Box), + Equality(String, String), +} + #[cfg_attr(test, derive(PartialEq, Eq, Debug))] pub struct ListUsersRequest { - // filters + pub filters: Option, } #[cfg_attr(test, derive(PartialEq, Eq, Debug))] diff --git a/src/infra/ldap_handler.rs b/src/infra/ldap_handler.rs index ccef962..44a8535 100644 --- a/src/infra/ldap_handler.rs +++ b/src/infra/ldap_handler.rs @@ -1,4 +1,4 @@ -use crate::domain::handler::{BackendHandler, ListUsersRequest, User}; +use crate::domain::handler::{BackendHandler, ListUsersRequest, RequestFilter, User}; use anyhow::{bail, Result}; use ldap3_server::simple::*; @@ -77,6 +77,28 @@ fn is_subtree(subtree: &[(String, String)], base_tree: &[(String, String)]) -> b true } +fn convert_filter(filter: &LdapFilter) -> Result { + match filter { + LdapFilter::And(filters) => Ok(RequestFilter::And( + filters + .into_iter() + .map(convert_filter) + .collect::>()?, + )), + LdapFilter::Or(filters) => Ok(RequestFilter::Or( + filters + .into_iter() + .map(convert_filter) + .collect::>()?, + )), + LdapFilter::Not(filter) => Ok(RequestFilter::Not(Box::new(convert_filter(&*filter)?))), + LdapFilter::Equality(field, value) => { + Ok(RequestFilter::Equality(field.clone(), value.clone())) + } + _ => bail!("Unsupported filter"), + } +} + pub struct LdapHandler { dn: String, backend_handler: Backend, @@ -138,7 +160,20 @@ impl LdapHandler { // Search path is not in our tree, just return an empty success. return vec![lsr.gen_success()]; } - let users = match self.backend_handler.list_users(ListUsersRequest {}).await { + let filters = match convert_filter(&lsr.filter) { + Ok(f) => Some(f), + Err(_) => { + return vec![lsr.gen_error( + LdapResultCode::UnwillingToPerform, + "Unsupported filter".to_string(), + )] + } + }; + let users = match self + .backend_handler + .list_users(ListUsersRequest { filters }) + .await + { Ok(users) => users, Err(e) => { return vec![lsr.gen_error( @@ -188,6 +223,21 @@ mod tests { use mockall::predicate::eq; use tokio; + async fn setup_bound_handler( + mut mock: MockTestBackendHandler, + ) -> LdapHandler { + mock.expect_bind().return_once(|_| Ok(())); + let mut ldap_handler = + LdapHandler::new(mock, "dc=example,dc=com".to_string(), "test".to_string()); + let request = SimpleBindRequest { + msgid: 1, + dn: "test".to_string(), + pw: "pass".to_string(), + }; + ldap_handler.do_bind(&request).await; + ldap_handler + } + #[tokio::test] async fn test_bind() { let mut mock = MockTestBackendHandler::new(); @@ -300,38 +350,27 @@ mod tests { #[tokio::test] async fn test_search() { let mut mock = MockTestBackendHandler::new(); - mock.expect_bind().return_once(|_| Ok(())); - mock.expect_list_users() - .with(eq(ListUsersRequest {})) - .times(1) - .return_once(|_| { - Ok(vec![ - User { - user_id: "bob_1".to_string(), - email: "bob@bobmail.bob".to_string(), - display_name: "Bôb Böbberson".to_string(), - first_name: "Bôb".to_string(), - last_name: "Böbberson".to_string(), - creation_date: NaiveDateTime::from_timestamp(1_000_000_000, 0), - }, - User { - user_id: "jim".to_string(), - email: "jim@cricket.jim".to_string(), - display_name: "Jimminy Cricket".to_string(), - first_name: "Jim".to_string(), - last_name: "Cricket".to_string(), - creation_date: NaiveDateTime::from_timestamp(1_003_000_000, 0), - }, - ]) - }); - let mut ldap_handler = - LdapHandler::new(mock, "dc=example,dc=com".to_string(), "test".to_string()); - let request = SimpleBindRequest { - msgid: 1, - dn: "test".to_string(), - pw: "pass".to_string(), - }; - assert_eq!(ldap_handler.do_bind(&request).await, request.gen_success()); + mock.expect_list_users().times(1).return_once(|_| { + Ok(vec![ + User { + user_id: "bob_1".to_string(), + email: "bob@bobmail.bob".to_string(), + display_name: "Bôb Böbberson".to_string(), + first_name: "Bôb".to_string(), + last_name: "Böbberson".to_string(), + creation_date: NaiveDateTime::from_timestamp(1_000_000_000, 0), + }, + User { + user_id: "jim".to_string(), + email: "jim@cricket.jim".to_string(), + display_name: "Jimminy Cricket".to_string(), + first_name: "Jim".to_string(), + last_name: "Cricket".to_string(), + creation_date: NaiveDateTime::from_timestamp(1_003_000_000, 0), + }, + ]) + }); + let mut ldap_handler = setup_bound_handler(mock).await; let request = SearchRequest { msgid: 2, base: "ou=people,dc=example,dc=com".to_string(), @@ -419,4 +458,54 @@ mod tests { ] ); } + + #[tokio::test] + async fn test_search_filters() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users() + .with(eq(ListUsersRequest { + filters: Some(RequestFilter::And(vec![RequestFilter::Or(vec![ + RequestFilter::Not(Box::new(RequestFilter::Equality( + "uid".to_string(), + "bob".to_string(), + ))), + ])])), + })) + .times(1) + .return_once(|_| Ok(vec![])); + let mut ldap_handler = setup_bound_handler(mock).await; + let request = SearchRequest { + msgid: 2, + base: "ou=people,dc=example,dc=com".to_string(), + scope: LdapSearchScope::Base, + filter: LdapFilter::And(vec![LdapFilter::Or(vec![LdapFilter::Not(Box::new( + LdapFilter::Equality("uid".to_string(), "bob".to_string()), + ))])]), + attrs: vec!["objectClass".to_string()], + }; + assert_eq!( + ldap_handler.do_search(&request).await, + vec![request.gen_success()] + ); + } + + #[tokio::test] + async fn test_search_unsupported_filters() { + let mut mock = MockTestBackendHandler::new(); + let mut ldap_handler = setup_bound_handler(mock).await; + let request = SearchRequest { + msgid: 2, + base: "ou=people,dc=example,dc=com".to_string(), + scope: LdapSearchScope::Base, + filter: LdapFilter::Present("uid".to_string()), + attrs: vec!["objectClass".to_string()], + }; + assert_eq!( + ldap_handler.do_search(&request).await, + vec![request.gen_error( + LdapResultCode::UnwillingToPerform, + "Unsupported filter".to_string() + )] + ); + } }