diff --git a/crates/ldap/src/core/group.rs b/crates/ldap/src/core/group.rs index a4a467e..b11a283 100644 --- a/crates/ldap/src/core/group.rs +++ b/crates/ldap/src/core/group.rs @@ -388,3 +388,305 @@ pub fn convert_groups_to_ldap_op<'a>( )) }) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + handler::tests::{make_group_search_request, setup_bound_admin_handler}, + search::{make_search_request, make_search_success}, + }; + use ldap3_proto::proto::LdapSubstringFilter; + use lldap_domain::{ + types::{GroupId, UserId}, + uuid, + }; + use lldap_domain_handlers::handler::*; + use lldap_test_utils::MockTestBackendHandler; + use mockall::predicate::eq; + use pretty_assertions::assert_eq; + + #[tokio::test] + async fn test_search_groups() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_groups() + .with(eq(Some(GroupRequestFilter::True))) + .times(1) + .return_once(|_| { + Ok(vec![ + Group { + id: GroupId(1), + display_name: "group_1".into(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + users: vec![UserId::new("bob"), UserId::new("john")], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), + attributes: Vec::new(), + modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + }, + Group { + id: GroupId(3), + display_name: "BestGroup".into(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + users: vec![UserId::new("john")], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), + attributes: Vec::new(), + modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + }, + ]) + }); + let ldap_handler = setup_bound_admin_handler(mock).await; + let request = make_group_search_request( + LdapFilter::And(vec![]), + vec![ + "objectClass", + "dn", + "cn", + "uniqueMember", + "entryUuid", + "entryDN", + ], + ); + assert_eq!( + ldap_handler.do_search_or_dse(&request).await, + Ok(vec![ + LdapOp::SearchResultEntry(LdapSearchResultEntry { + dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(), + attributes: vec![ + LdapPartialAttribute { + atype: "cn".to_string(), + vals: vec![b"group_1".to_vec()] + }, + LdapPartialAttribute { + atype: "entryDN".to_string(), + vals: vec![b"uid=group_1,ou=groups,dc=example,dc=com".to_vec()], + }, + LdapPartialAttribute { + atype: "entryUuid".to_string(), + vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()], + }, + LdapPartialAttribute { + atype: "objectClass".to_string(), + vals: vec![b"groupOfUniqueNames".to_vec(), b"groupOfNames".to_vec()] + }, + LdapPartialAttribute { + atype: "uniqueMember".to_string(), + vals: vec![ + b"uid=bob,ou=people,dc=example,dc=com".to_vec(), + b"uid=john,ou=people,dc=example,dc=com".to_vec(), + ], + }, + ], + }), + LdapOp::SearchResultEntry(LdapSearchResultEntry { + dn: "cn=BestGroup,ou=groups,dc=example,dc=com".to_string(), + attributes: vec![ + LdapPartialAttribute { + atype: "cn".to_string(), + vals: vec![b"BestGroup".to_vec()] + }, + LdapPartialAttribute { + atype: "entryDN".to_string(), + vals: vec![b"uid=BestGroup,ou=groups,dc=example,dc=com".to_vec()], + }, + LdapPartialAttribute { + atype: "entryUuid".to_string(), + vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()], + }, + LdapPartialAttribute { + atype: "objectClass".to_string(), + vals: vec![b"groupOfUniqueNames".to_vec(), b"groupOfNames".to_vec()] + }, + LdapPartialAttribute { + atype: "uniqueMember".to_string(), + vals: vec![b"uid=john,ou=people,dc=example,dc=com".to_vec()], + }, + ], + }), + make_search_success(), + ]) + ); + } + + #[tokio::test] + async fn test_search_groups_by_groupid() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_groups() + .with(eq(Some(GroupRequestFilter::GroupId(GroupId(1))))) + .times(1) + .return_once(|_| { + Ok(vec![Group { + display_name: "group_1".into(), + id: GroupId(1), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + users: vec![], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), + attributes: Vec::new(), + modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + }]) + }); + let ldap_handler = setup_bound_admin_handler(mock).await; + let request = make_group_search_request( + LdapFilter::Equality("groupid".to_string(), "1".to_string()), + vec!["dn"], + ); + assert_eq!( + ldap_handler.do_search_or_dse(&request).await, + Ok(vec![ + LdapOp::SearchResultEntry(LdapSearchResultEntry { + dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(), + attributes: vec![], + }), + make_search_success(), + ]) + ); + } + + #[tokio::test] + async fn test_search_groups_filter() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_groups() + .with(eq(Some(GroupRequestFilter::And(vec![ + GroupRequestFilter::DisplayName("group_1".into()), + GroupRequestFilter::Member(UserId::new("bob")), + GroupRequestFilter::DisplayName("rockstars".into()), + false.into(), + GroupRequestFilter::Uuid(uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc")), + false.into(), + GroupRequestFilter::DisplayNameSubString(SubStringFilter { + initial: Some("iNIt".to_owned()), + any: vec!["1".to_owned(), "2aA".to_owned()], + final_: Some("finAl".to_owned()), + }), + ])))) + .times(1) + .return_once(|_| { + Ok(vec![Group { + display_name: "group_1".into(), + id: GroupId(1), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + users: vec![], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), + attributes: Vec::new(), + modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + }]) + }); + let ldap_handler = setup_bound_admin_handler(mock).await; + let request = make_group_search_request( + LdapFilter::And(vec![ + LdapFilter::Equality("cN".to_string(), "Group_1".to_string()), + LdapFilter::Equality( + "uniqueMember".to_string(), + "uid=bob,ou=peopLe,Dc=eXample,dc=com".to_string(), + ), + LdapFilter::Equality( + "dn".to_string(), + "uid=rockstars,ou=groups,dc=example,dc=com".to_string(), + ), + LdapFilter::Equality( + "dn".to_string(), + "uid=rockstars,ou=people,dc=example,dc=com".to_string(), + ), + LdapFilter::Equality( + "uuid".to_string(), + "04ac75e0-2900-3e21-926c-2f732c26b3fc".to_string(), + ), + LdapFilter::Equality("obJEctclass".to_string(), "groupofUniqueNames".to_string()), + LdapFilter::Equality("objectclass".to_string(), "groupOfNames".to_string()), + LdapFilter::Present("objectclass".to_string()), + LdapFilter::Present("dn".to_string()), + LdapFilter::Not(Box::new(LdapFilter::Present( + "random_attribUte".to_string(), + ))), + LdapFilter::Equality("unknown_attribute".to_string(), "randomValue".to_string()), + LdapFilter::Substring( + "cn".to_owned(), + LdapSubstringFilter { + initial: Some("iNIt".to_owned()), + any: vec!["1".to_owned(), "2aA".to_owned()], + final_: Some("finAl".to_owned()), + }, + ), + ]), + vec!["1.1"], + ); + assert_eq!( + ldap_handler.do_search_or_dse(&request).await, + Ok(vec![ + LdapOp::SearchResultEntry(LdapSearchResultEntry { + dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(), + attributes: vec![], + }), + make_search_success(), + ]) + ); + } + + #[tokio::test] + async fn test_search_groups_filter_2() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_groups() + .with(eq(Some(GroupRequestFilter::Or(vec![ + GroupRequestFilter::DisplayName("group_1".into()), + GroupRequestFilter::Member(UserId::new("bob")), + ])))) + .times(1) + .return_once(|_| Ok(vec![])); + let ldap_handler = setup_bound_admin_handler(mock).await; + let request = make_group_search_request( + LdapFilter::Or(vec![ + LdapFilter::Equality("cn".to_string(), "group_1".to_string()), + LdapFilter::Equality( + "member".to_string(), + "uid=bob,ou=people,dc=example,dc=com".to_string(), + ), + ]), + vec!["cn"], + ); + assert_eq!( + ldap_handler.do_search_or_dse(&request).await, + Ok(vec![make_search_success()]) + ); + } + + #[tokio::test] + async fn test_search_groups_filter_3() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_groups() + .with(eq(Some(GroupRequestFilter::Not(Box::new( + GroupRequestFilter::DisplayName("group_1".into()), + ))))) + .times(1) + .return_once(|_| Ok(vec![])); + let ldap_handler = setup_bound_admin_handler(mock).await; + let request = make_group_search_request( + LdapFilter::Not(Box::new(LdapFilter::Equality( + "cn".to_string(), + "group_1".to_string(), + ))), + vec!["cn"], + ); + assert_eq!( + ldap_handler.do_search_or_dse(&request).await, + Ok(vec![make_search_success()]) + ); + } + + #[tokio::test] + async fn test_search_group_as_scope() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_groups() + .with(eq(Some(GroupRequestFilter::DisplayName("group_1".into())))) + .times(1) + .return_once(|_| Ok(vec![])); + let ldap_handler = setup_bound_admin_handler(mock).await; + let request = make_search_request( + "cn=group_1,ou=groups,dc=example,dc=com", + LdapFilter::And(vec![]), + vec!["objectClass"], + ); + assert_eq!( + ldap_handler.do_search_or_dse(&request).await, + Ok(vec![make_search_success()]), + ); + } +} diff --git a/crates/ldap/src/core/user.rs b/crates/ldap/src/core/user.rs index 7df2e65..563acfd 100644 --- a/crates/ldap/src/core/user.rs +++ b/crates/ldap/src/core/user.rs @@ -400,3 +400,374 @@ pub fn convert_users_to_ldap_op<'a>( )) }) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + handler::tests::{ + make_user_search_request, setup_bound_admin_handler, setup_bound_handler_with_group, + setup_bound_readonly_handler, + }, + search::{make_search_request, make_search_success}, + }; + use chrono::{DateTime, Duration, NaiveDateTime, TimeZone, Utc}; + use lldap_domain::types::{Attribute, GroupDetails, JpegPhoto}; + use lldap_test_utils::MockTestBackendHandler; + use mockall::predicate::eq; + use pretty_assertions::assert_eq; + + fn assert_timestamp_within_margin( + timestamp_bytes: &[u8], + base_timestamp_dt: DateTime, + time_margin: Duration, + ) { + let timestamp_str = + std::str::from_utf8(timestamp_bytes).expect("Invalid conversion from UTF-8 to string"); + let timestamp_naive = NaiveDateTime::parse_from_str(timestamp_str, "%Y%m%d%H%M%SZ") + .expect("Invalid timestamp format"); + let timestamp_dt: DateTime = Utc.from_utc_datetime(×tamp_naive); + + let within_range = (base_timestamp_dt - timestamp_dt).abs() <= time_margin; + + assert!( + within_range, + "Timestamp not within range: expected within [{} - {}], got [{}]", + base_timestamp_dt - time_margin, + base_timestamp_dt + time_margin, + timestamp_dt + ); + } + + #[tokio::test] + async fn test_search_regular_user() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users() + .with( + eq(Some(UserRequestFilter::And(vec![ + UserRequestFilter::True, + UserRequestFilter::UserId(UserId::new("test")), + ]))), + eq(false), + ) + .times(1) + .return_once(|_, _| { + Ok(vec![UserAndGroups { + user: User { + user_id: UserId::new("test"), + ..Default::default() + }, + groups: None, + }]) + }); + let ldap_handler = setup_bound_handler_with_group(mock, "regular").await; + + let request = + make_user_search_request::(LdapFilter::And(vec![]), vec!["1.1".to_string()]); + assert_eq!( + ldap_handler.do_search_or_dse(&request).await, + Ok(vec![ + LdapOp::SearchResultEntry(LdapSearchResultEntry { + dn: "uid=test,ou=people,dc=example,dc=com".to_string(), + attributes: vec![], + }), + make_search_success() + ]), + ); + } + + #[tokio::test] + async fn test_search_readonly_user() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users() + .with(eq(Some(UserRequestFilter::True)), eq(false)) + .times(1) + .return_once(|_, _| Ok(vec![])); + let ldap_handler = setup_bound_readonly_handler(mock).await; + let request = make_user_search_request(LdapFilter::And(vec![]), vec!["1.1"]); + assert_eq!( + ldap_handler.do_search_or_dse(&request).await, + Ok(vec![make_search_success()]), + ); + } + + #[tokio::test] + async fn test_search_member_of() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users() + .with(eq(Some(UserRequestFilter::True)), eq(true)) + .times(1) + .return_once(|_, _| { + Ok(vec![UserAndGroups { + user: User { + user_id: UserId::new("bob"), + ..Default::default() + }, + groups: Some(vec![GroupDetails { + group_id: lldap_domain::types::GroupId(42), + display_name: "rockstars".into(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + uuid: lldap_domain::uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), + attributes: Vec::new(), + modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + }]), + }]) + }); + let ldap_handler = setup_bound_readonly_handler(mock).await; + let request = make_user_search_request::( + LdapFilter::And(vec![]), + vec!["memberOf".to_string()], + ); + assert_eq!( + ldap_handler.do_search_or_dse(&request).await, + Ok(vec![ + LdapOp::SearchResultEntry(LdapSearchResultEntry { + dn: "uid=bob,ou=people,dc=example,dc=com".to_string(), + attributes: vec![LdapPartialAttribute { + atype: "memberOf".to_string(), + vals: vec![b"cn=rockstars,ou=groups,dc=example,dc=com".to_vec()] + }], + }), + make_search_success(), + ]), + ); + } + + #[tokio::test] + async fn test_search_user_as_scope() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users() + .with( + eq(Some(UserRequestFilter::UserId(UserId::new("bob")))), + eq(false), + ) + .times(1) + .return_once(|_, _| Ok(vec![])); + let ldap_handler = setup_bound_admin_handler(mock).await; + let request = make_search_request( + "uid=bob,ou=people,dc=example,dc=com", + LdapFilter::And(vec![]), + vec!["objectClass"], + ); + assert_eq!( + ldap_handler.do_search_or_dse(&request).await, + Ok(vec![make_search_success()]), + ); + } + + #[tokio::test] + async fn test_search_users() { + use chrono::prelude::*; + use lldap_domain::uuid; + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users().times(1).return_once(|_, _| { + Ok(vec![ + UserAndGroups { + user: User { + user_id: UserId::new("bob_1"), + email: "bob@bobmail.bob".into(), + display_name: Some("Bôb Böbberson".to_string()), + uuid: uuid!("698e1d5f-7a40-3151-8745-b9b8a37839da"), + attributes: vec![ + Attribute { + name: "first_name".into(), + value: "Bôb".to_string().into(), + }, + Attribute { + name: "last_name".into(), + value: "Böbberson".to_string().into(), + }, + ], + ..Default::default() + }, + groups: None, + }, + UserAndGroups { + user: User { + user_id: UserId::new("jim"), + email: "jim@cricket.jim".into(), + display_name: Some("Jimminy Cricket".to_string()), + attributes: vec![ + Attribute { + name: "avatar".into(), + value: JpegPhoto::for_tests().into(), + }, + Attribute { + name: "first_name".into(), + value: "Jim".to_string().into(), + }, + Attribute { + name: "last_name".into(), + value: "Cricket".to_string().into(), + }, + ], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), + creation_date: Utc + .with_ymd_and_hms(2014, 7, 8, 9, 10, 11) + .unwrap() + .naive_utc(), + modified_date: Utc + .with_ymd_and_hms(2014, 7, 8, 9, 10, 11) + .unwrap() + .naive_utc(), + password_modified_date: Utc + .with_ymd_and_hms(2014, 7, 8, 9, 10, 11) + .unwrap() + .naive_utc(), + }, + groups: None, + }, + ]) + }); + let ldap_handler = setup_bound_admin_handler(mock).await; + let request = make_user_search_request( + LdapFilter::And(vec![]), + vec![ + "objectClass", + "dn", + "uid", + "mail", + "givenName", + "sn", + "cn", + "createTimestamp", + "entryUuid", + "jpegPhoto", + ], + ); + assert_eq!( + ldap_handler.do_search_or_dse(&request).await, + Ok(vec![ + LdapOp::SearchResultEntry(LdapSearchResultEntry { + dn: "uid=bob_1,ou=people,dc=example,dc=com".to_string(), + attributes: vec![ + LdapPartialAttribute { + atype: "cn".to_string(), + vals: vec!["Bôb Böbberson".to_string().into_bytes()] + }, + LdapPartialAttribute { + atype: "createTimestamp".to_string(), + vals: vec![b"19700101000000Z".to_vec()] + }, + LdapPartialAttribute { + atype: "entryUuid".to_string(), + vals: vec![b"698e1d5f-7a40-3151-8745-b9b8a37839da".to_vec()] + }, + LdapPartialAttribute { + atype: "givenName".to_string(), + vals: vec!["Bôb".to_string().into_bytes()] + }, + LdapPartialAttribute { + atype: "mail".to_string(), + vals: vec![b"bob@bobmail.bob".to_vec()] + }, + LdapPartialAttribute { + atype: "objectClass".to_string(), + vals: vec![ + b"inetOrgPerson".to_vec(), + b"posixAccount".to_vec(), + b"mailAccount".to_vec(), + b"person".to_vec(), + b"customUserClass".to_vec(), + ] + }, + LdapPartialAttribute { + atype: "sn".to_string(), + vals: vec!["Böbberson".to_string().into_bytes()] + }, + LdapPartialAttribute { + atype: "uid".to_string(), + vals: vec![b"bob_1".to_vec()] + }, + ], + }), + LdapOp::SearchResultEntry(LdapSearchResultEntry { + dn: "uid=jim,ou=people,dc=example,dc=com".to_string(), + attributes: vec![ + LdapPartialAttribute { + atype: "cn".to_string(), + vals: vec![b"Jimminy Cricket".to_vec()] + }, + LdapPartialAttribute { + atype: "createTimestamp".to_string(), + vals: vec![b"20140708091011Z".to_vec()] + }, + LdapPartialAttribute { + atype: "entryUuid".to_string(), + vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()] + }, + LdapPartialAttribute { + atype: "givenName".to_string(), + vals: vec![b"Jim".to_vec()] + }, + LdapPartialAttribute { + atype: "jpegPhoto".to_string(), + vals: vec![JpegPhoto::for_tests().into_bytes()] + }, + LdapPartialAttribute { + atype: "mail".to_string(), + vals: vec![b"jim@cricket.jim".to_vec()] + }, + LdapPartialAttribute { + atype: "objectClass".to_string(), + vals: vec![ + b"inetOrgPerson".to_vec(), + b"posixAccount".to_vec(), + b"mailAccount".to_vec(), + b"person".to_vec(), + b"customUserClass".to_vec(), + ] + }, + LdapPartialAttribute { + atype: "sn".to_string(), + vals: vec![b"Cricket".to_vec()] + }, + LdapPartialAttribute { + atype: "uid".to_string(), + vals: vec![b"jim".to_vec()] + }, + ], + }), + make_search_success(), + ]) + ); + } + + #[tokio::test] + async fn test_pwd_changed_time_format() { + use lldap_domain::uuid; + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users().times(1).return_once(|_, _| { + Ok(vec![UserAndGroups { + user: User { + user_id: UserId::new("bob_1"), + email: "bob@bobmail.bob".into(), + uuid: uuid!("698e1d5f-7a40-3151-8745-b9b8a37839da"), + attributes: vec![], + password_modified_date: Utc + .with_ymd_and_hms(2014, 7, 8, 9, 10, 11) + .unwrap() + .naive_utc(), + ..Default::default() + }, + groups: None, + }]) + }); + let ldap_handler = setup_bound_admin_handler(mock).await; + let request = make_user_search_request(LdapFilter::And(vec![]), vec!["pwdChangedTime"]); + if let LdapOp::SearchResultEntry(entry) = + &ldap_handler.do_search_or_dse(&request).await.unwrap()[0] + { + assert_eq!(entry.attributes.len(), 1); + assert_eq!(entry.attributes[0].atype, "pwdChangedTime"); + assert_eq!(entry.attributes[0].vals.len(), 1); + assert_timestamp_within_margin( + &entry.attributes[0].vals[0], + Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap(), + Duration::seconds(1), + ); + } else { + panic!("Expected SearchResultEntry"); + } + } +} diff --git a/crates/ldap/src/search.rs b/crates/ldap/src/search.rs index d3fe278..c654ef1 100644 --- a/crates/ldap/src/search.rs +++ b/crates/ldap/src/search.rs @@ -434,16 +434,16 @@ mod tests { core::error::LdapError, handler::tests::{ make_group_search_request, make_user_search_request, setup_bound_admin_handler, - setup_bound_handler_with_group, setup_bound_readonly_handler, + setup_bound_readonly_handler, }, }; - use chrono::{DateTime, Duration, NaiveDateTime, TimeZone}; + use chrono::{DateTime, Duration, NaiveDateTime, TimeZone, Utc}; use ldap3_proto::proto::{LdapDerefAliases, LdapSearchScope, LdapSubstringFilter}; use lldap_domain::{ schema::{AttributeList, AttributeSchema, Schema}, types::{ - Attribute, AttributeName, AttributeType, GroupDetails, GroupId, JpegPhoto, - LdapObjectClass, User, UserId, + Attribute, AttributeName, AttributeType, GroupId, JpegPhoto, LdapObjectClass, User, + UserId, }, uuid, }; @@ -453,28 +453,6 @@ mod tests { use mockall::predicate::eq; use pretty_assertions::assert_eq; - #[tokio::test] - async fn test_search_root_dse() { - let ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await; - let request = LdapSearchRequest { - base: "".to_string(), - scope: LdapSearchScope::Base, - aliases: LdapDerefAliases::Never, - sizelimit: 0, - timelimit: 0, - typesonly: false, - filter: LdapFilter::Present("objectClass".to_string()), - attrs: vec!["supportedExtension".to_string()], - }; - assert_eq!( - ldap_handler.do_search_or_dse(&request).await, - Ok(vec![ - root_dse_response("dc=example,dc=com"), - make_search_success() - ]) - ); - } - fn assert_timestamp_within_margin( timestamp_bytes: &[u8], base_timestamp_dt: DateTime, @@ -497,6 +475,28 @@ mod tests { ); } + #[tokio::test] + async fn test_search_root_dse() { + let ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await; + let request = LdapSearchRequest { + base: "".to_string(), + scope: LdapSearchScope::Base, + aliases: LdapDerefAliases::Never, + sizelimit: 0, + timelimit: 0, + typesonly: false, + filter: LdapFilter::Present("objectClass".to_string()), + attrs: vec!["supportedExtension".to_string()], + }; + assert_eq!( + ldap_handler.do_search_or_dse(&request).await, + Ok(vec![ + root_dse_response("dc=example,dc=com"), + make_search_success() + ]) + ); + } + #[tokio::test] async fn test_subschema_response() { let ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await; @@ -659,661 +659,6 @@ mod tests { assert_eq!(actual_reponse[1], make_search_success()); } - #[tokio::test] - async fn test_search_regular_user() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users() - .with( - eq(Some(UserRequestFilter::And(vec![ - UserRequestFilter::True, - UserRequestFilter::UserId(UserId::new("test")), - ]))), - eq(false), - ) - .times(1) - .return_once(|_, _| { - Ok(vec![UserAndGroups { - user: User { - user_id: UserId::new("test"), - ..Default::default() - }, - groups: None, - }]) - }); - let ldap_handler = setup_bound_handler_with_group(mock, "regular").await; - - let request = - make_user_search_request::(LdapFilter::And(vec![]), vec!["1.1".to_string()]); - assert_eq!( - ldap_handler.do_search_or_dse(&request).await, - Ok(vec![ - LdapOp::SearchResultEntry(LdapSearchResultEntry { - dn: "uid=test,ou=people,dc=example,dc=com".to_string(), - attributes: vec![], - }), - make_search_success() - ]), - ); - } - - #[tokio::test] - async fn test_search_readonly_user() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users() - .with(eq(Some(UserRequestFilter::True)), eq(false)) - .times(1) - .return_once(|_, _| Ok(vec![])); - let ldap_handler = setup_bound_readonly_handler(mock).await; - - let request = - make_user_search_request::(LdapFilter::And(vec![]), vec!["1.1".to_string()]); - assert_eq!( - ldap_handler.do_search_or_dse(&request).await, - Ok(vec![make_search_success()]), - ); - } - - #[tokio::test] - async fn test_search_member_of() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users() - .with(eq(Some(UserRequestFilter::True)), eq(true)) - .times(1) - .return_once(|_, _| { - Ok(vec![UserAndGroups { - user: User { - user_id: UserId::new("bob"), - ..Default::default() - }, - groups: Some(vec![GroupDetails { - group_id: GroupId(42), - display_name: "rockstars".into(), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), - attributes: Vec::new(), - modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - }]), - }]) - }); - let ldap_handler = setup_bound_readonly_handler(mock).await; - - let request = make_user_search_request::( - LdapFilter::And(vec![]), - vec!["memberOf".to_string()], - ); - assert_eq!( - ldap_handler.do_search_or_dse(&request).await, - Ok(vec![ - LdapOp::SearchResultEntry(LdapSearchResultEntry { - dn: "uid=bob,ou=people,dc=example,dc=com".to_string(), - attributes: vec![LdapPartialAttribute { - atype: "memberOf".to_string(), - vals: vec![b"cn=rockstars,ou=groups,dc=example,dc=com".to_vec()] - }], - }), - make_search_success(), - ]), - ); - } - - #[tokio::test] - async fn test_search_user_as_scope() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users() - .with( - eq(Some(UserRequestFilter::UserId(UserId::new("bob")))), - eq(false), - ) - .times(1) - .return_once(|_, _| Ok(vec![])); - let ldap_handler = setup_bound_readonly_handler(mock).await; - - let request = LdapSearchRequest { - base: "uid=bob,ou=people,Dc=example,dc=com".to_string(), - scope: LdapSearchScope::Base, - aliases: LdapDerefAliases::Never, - sizelimit: 0, - timelimit: 0, - typesonly: false, - filter: LdapFilter::And(vec![]), - attrs: vec!["1.1".to_string()], - }; - assert_eq!( - ldap_handler.do_search_or_dse(&request).await, - Ok(vec![make_search_success()]), - ); - } - - #[tokio::test] - async fn test_search_users() { - use chrono::prelude::*; - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users().times(1).return_once(|_, _| { - Ok(vec![ - UserAndGroups { - user: User { - user_id: UserId::new("bob_1"), - email: "bob@bobmail.bob".into(), - display_name: Some("Bôb Böbberson".to_string()), - uuid: uuid!("698e1d5f-7a40-3151-8745-b9b8a37839da"), - attributes: vec![ - Attribute { - name: "first_name".into(), - value: "Bôb".to_string().into(), - }, - Attribute { - name: "last_name".into(), - value: "Böbberson".to_string().into(), - }, - ], - ..Default::default() - }, - groups: None, - }, - UserAndGroups { - user: User { - user_id: UserId::new("jim"), - email: "jim@cricket.jim".into(), - display_name: Some("Jimminy Cricket".to_string()), - attributes: vec![ - Attribute { - name: "avatar".into(), - value: JpegPhoto::for_tests().into(), - }, - Attribute { - name: "first_name".into(), - value: "Jim".to_string().into(), - }, - Attribute { - name: "last_name".into(), - value: "Cricket".to_string().into(), - }, - ], - uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), - creation_date: Utc - .with_ymd_and_hms(2014, 7, 8, 9, 10, 11) - .unwrap() - .naive_utc(), - modified_date: Utc - .with_ymd_and_hms(2014, 7, 8, 9, 10, 11) - .unwrap() - .naive_utc(), - password_modified_date: Utc - .with_ymd_and_hms(2014, 7, 8, 9, 10, 11) - .unwrap() - .naive_utc(), - }, - groups: None, - }, - ]) - }); - let ldap_handler = setup_bound_admin_handler(mock).await; - let request = make_user_search_request( - LdapFilter::And(vec![]), - vec![ - "objectClass", - "dn", - "uid", - "mail", - "givenName", - "sn", - "cn", - "createTimestamp", - "entryUuid", - "jpegPhoto", - ], - ); - assert_eq!( - ldap_handler.do_search_or_dse(&request).await, - Ok(vec![ - LdapOp::SearchResultEntry(LdapSearchResultEntry { - dn: "uid=bob_1,ou=people,dc=example,dc=com".to_string(), - attributes: vec![ - LdapPartialAttribute { - atype: "cn".to_string(), - vals: vec!["Bôb Böbberson".to_string().into_bytes()] - }, - LdapPartialAttribute { - atype: "createTimestamp".to_string(), - vals: vec![b"19700101000000Z".to_vec()] - }, - LdapPartialAttribute { - atype: "entryUuid".to_string(), - vals: vec![b"698e1d5f-7a40-3151-8745-b9b8a37839da".to_vec()] - }, - LdapPartialAttribute { - atype: "givenName".to_string(), - vals: vec!["Bôb".to_string().into_bytes()] - }, - LdapPartialAttribute { - atype: "mail".to_string(), - vals: vec![b"bob@bobmail.bob".to_vec()] - }, - LdapPartialAttribute { - atype: "objectClass".to_string(), - vals: vec![ - b"inetOrgPerson".to_vec(), - b"posixAccount".to_vec(), - b"mailAccount".to_vec(), - b"person".to_vec(), - b"customUserClass".to_vec(), - ] - }, - LdapPartialAttribute { - atype: "sn".to_string(), - vals: vec!["Böbberson".to_string().into_bytes()] - }, - LdapPartialAttribute { - atype: "uid".to_string(), - vals: vec![b"bob_1".to_vec()] - }, - ], - }), - LdapOp::SearchResultEntry(LdapSearchResultEntry { - dn: "uid=jim,ou=people,dc=example,dc=com".to_string(), - attributes: vec![ - LdapPartialAttribute { - atype: "cn".to_string(), - vals: vec![b"Jimminy Cricket".to_vec()] - }, - LdapPartialAttribute { - atype: "createTimestamp".to_string(), - vals: vec![b"20140708091011Z".to_vec()] - }, - LdapPartialAttribute { - atype: "entryUuid".to_string(), - vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()] - }, - LdapPartialAttribute { - atype: "givenName".to_string(), - vals: vec![b"Jim".to_vec()] - }, - LdapPartialAttribute { - atype: "jpegPhoto".to_string(), - vals: vec![JpegPhoto::for_tests().into_bytes()] - }, - LdapPartialAttribute { - atype: "mail".to_string(), - vals: vec![b"jim@cricket.jim".to_vec()] - }, - LdapPartialAttribute { - atype: "objectClass".to_string(), - vals: vec![ - b"inetOrgPerson".to_vec(), - b"posixAccount".to_vec(), - b"mailAccount".to_vec(), - b"person".to_vec(), - b"customUserClass".to_vec(), - ] - }, - LdapPartialAttribute { - atype: "sn".to_string(), - vals: vec![b"Cricket".to_vec()] - }, - LdapPartialAttribute { - atype: "uid".to_string(), - vals: vec![b"jim".to_vec()] - }, - ], - }), - make_search_success(), - ]) - ); - } - - #[tokio::test] - async fn test_search_groups() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_groups() - .with(eq(Some(GroupRequestFilter::True))) - .times(1) - .return_once(|_| { - Ok(vec![ - Group { - id: GroupId(1), - display_name: "group_1".into(), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - users: vec![UserId::new("bob"), UserId::new("john")], - uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), - attributes: Vec::new(), - modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - }, - Group { - id: GroupId(3), - display_name: "BestGroup".into(), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - users: vec![UserId::new("john")], - uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), - attributes: Vec::new(), - modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - }, - ]) - }); - let ldap_handler = setup_bound_admin_handler(mock).await; - let request = make_group_search_request( - LdapFilter::And(vec![]), - vec![ - "objectClass", - "dn", - "cn", - "uniqueMember", - "entryUuid", - "entryDN", - ], - ); - assert_eq!( - ldap_handler.do_search_or_dse(&request).await, - Ok(vec![ - LdapOp::SearchResultEntry(LdapSearchResultEntry { - dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(), - attributes: vec![ - LdapPartialAttribute { - atype: "cn".to_string(), - vals: vec![b"group_1".to_vec()] - }, - LdapPartialAttribute { - atype: "entryDN".to_string(), - vals: vec![b"uid=group_1,ou=groups,dc=example,dc=com".to_vec()], - }, - LdapPartialAttribute { - atype: "entryUuid".to_string(), - vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()], - }, - LdapPartialAttribute { - atype: "objectClass".to_string(), - vals: vec![b"groupOfUniqueNames".to_vec(), b"groupOfNames".to_vec(),], - }, - LdapPartialAttribute { - atype: "uniqueMember".to_string(), - vals: vec![ - b"uid=bob,ou=people,dc=example,dc=com".to_vec(), - b"uid=john,ou=people,dc=example,dc=com".to_vec(), - ] - }, - ], - }), - LdapOp::SearchResultEntry(LdapSearchResultEntry { - dn: "cn=BestGroup,ou=groups,dc=example,dc=com".to_string(), - attributes: vec![ - LdapPartialAttribute { - atype: "cn".to_string(), - vals: vec![b"BestGroup".to_vec()] - }, - LdapPartialAttribute { - atype: "entryDN".to_string(), - vals: vec![b"uid=BestGroup,ou=groups,dc=example,dc=com".to_vec()], - }, - LdapPartialAttribute { - atype: "entryUuid".to_string(), - vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()], - }, - LdapPartialAttribute { - atype: "objectClass".to_string(), - vals: vec![b"groupOfUniqueNames".to_vec(), b"groupOfNames".to_vec(),], - }, - LdapPartialAttribute { - atype: "uniqueMember".to_string(), - vals: vec![b"uid=john,ou=people,dc=example,dc=com".to_vec()] - }, - ], - }), - make_search_success(), - ]) - ); - } - - #[tokio::test] - async fn test_search_groups_by_groupid() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_groups() - .with(eq(Some(GroupRequestFilter::GroupId(GroupId(1))))) - .times(1) - .return_once(|_| { - Ok(vec![Group { - id: GroupId(1), - display_name: "group_1".into(), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - users: vec![UserId::new("bob"), UserId::new("john")], - uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), - attributes: Vec::new(), - modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - }]) - }); - let ldap_handler = setup_bound_admin_handler(mock).await; - let request = make_group_search_request( - LdapFilter::Equality("groupid".to_string(), "1".to_string()), - vec!["dn"], - ); - assert_eq!( - ldap_handler.do_search_or_dse(&request).await, - Ok(vec![ - LdapOp::SearchResultEntry(LdapSearchResultEntry { - dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(), - attributes: vec![], - }), - make_search_success(), - ]) - ); - } - - #[tokio::test] - async fn test_search_groups_filter() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_groups() - .with(eq(Some(GroupRequestFilter::And(vec![ - GroupRequestFilter::DisplayName("group_1".into()), - GroupRequestFilter::Member(UserId::new("bob")), - GroupRequestFilter::DisplayName("rockstars".into()), - false.into(), - GroupRequestFilter::Uuid(uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc")), - false.into(), - GroupRequestFilter::DisplayNameSubString(SubStringFilter { - initial: Some("iNIt".to_owned()), - any: vec!["1".to_owned(), "2aA".to_owned()], - final_: Some("finAl".to_owned()), - }), - ])))) - .times(1) - .return_once(|_| { - Ok(vec![Group { - display_name: "group_1".into(), - id: GroupId(1), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - users: vec![], - uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), - attributes: Vec::new(), - modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - }]) - }); - let ldap_handler = setup_bound_admin_handler(mock).await; - let request = make_group_search_request( - LdapFilter::And(vec![ - LdapFilter::Equality("cN".to_string(), "Group_1".to_string()), - LdapFilter::Equality( - "uniqueMember".to_string(), - "uid=bob,ou=peopLe,Dc=eXample,dc=com".to_string(), - ), - LdapFilter::Equality( - "dn".to_string(), - "uid=rockstars,ou=groups,dc=example,dc=com".to_string(), - ), - LdapFilter::Equality( - "dn".to_string(), - "uid=rockstars,ou=people,dc=example,dc=com".to_string(), - ), - LdapFilter::Equality( - "uuid".to_string(), - "04ac75e0-2900-3e21-926c-2f732c26b3fc".to_string(), - ), - LdapFilter::Equality("obJEctclass".to_string(), "groupofUniqueNames".to_string()), - LdapFilter::Equality("objectclass".to_string(), "groupOfNames".to_string()), - LdapFilter::Present("objectclass".to_string()), - LdapFilter::Present("dn".to_string()), - LdapFilter::Not(Box::new(LdapFilter::Present( - "random_attribUte".to_string(), - ))), - LdapFilter::Equality("unknown_attribute".to_string(), "randomValue".to_string()), - LdapFilter::Substring( - "cn".to_owned(), - LdapSubstringFilter { - initial: Some("iNIt".to_owned()), - any: vec!["1".to_owned(), "2aA".to_owned()], - final_: Some("finAl".to_owned()), - }, - ), - ]), - vec!["1.1"], - ); - assert_eq!( - ldap_handler.do_search_or_dse(&request).await, - Ok(vec![ - LdapOp::SearchResultEntry(LdapSearchResultEntry { - dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(), - attributes: vec![], - }), - make_search_success(), - ]) - ); - } - - #[tokio::test] - async fn test_search_groups_filter_2() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_groups() - .with(eq(Some(GroupRequestFilter::Not(Box::new( - GroupRequestFilter::DisplayName("group_2".into()), - ))))) - .times(1) - .return_once(|_| { - Ok(vec![Group { - display_name: "group_1".into(), - id: GroupId(1), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - users: vec![], - uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), - attributes: Vec::new(), - modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - }]) - }); - let ldap_handler = setup_bound_admin_handler(mock).await; - let request = make_group_search_request( - LdapFilter::Or(vec![LdapFilter::Not(Box::new(LdapFilter::Equality( - "displayname".to_string(), - "group_2".to_string(), - )))]), - vec!["cn"], - ); - assert_eq!( - ldap_handler.do_search_or_dse(&request).await, - Ok(vec![ - LdapOp::SearchResultEntry(LdapSearchResultEntry { - dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(), - attributes: vec![LdapPartialAttribute { - atype: "cn".to_string(), - vals: vec![b"group_1".to_vec()] - },], - }), - make_search_success(), - ]) - ); - } - - #[tokio::test] - async fn test_search_groups_filter_3() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_groups() - .with(eq(Some(GroupRequestFilter::Or(vec![ - GroupRequestFilter::AttributeEquality( - AttributeName::from("attr"), - "TEST".to_string().into(), - ), - GroupRequestFilter::AttributeEquality( - AttributeName::from("attr"), - "test".to_string().into(), - ), - ])))) - .times(1) - .return_once(|_| { - Ok(vec![Group { - display_name: "group_1".into(), - id: GroupId(1), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - users: vec![], - uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), - attributes: vec![Attribute { - name: "Attr".into(), - value: "TEST".to_string().into(), - }], - modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - }]) - }); - mock.expect_get_schema().returning(|| { - Ok(Schema { - user_attributes: AttributeList { - attributes: Vec::new(), - }, - group_attributes: AttributeList { - attributes: vec![AttributeSchema { - name: "Attr".into(), - attribute_type: AttributeType::String, - is_list: false, - is_visible: true, - is_editable: true, - is_hardcoded: false, - is_readonly: false, - }], - }, - extra_user_object_classes: Vec::new(), - extra_group_object_classes: Vec::new(), - }) - }); - let ldap_handler = setup_bound_admin_handler(mock).await; - let request = make_group_search_request( - LdapFilter::Equality("Attr".to_string(), "TEST".to_string()), - vec!["cn"], - ); - assert_eq!( - ldap_handler.do_search_or_dse(&request).await, - Ok(vec![ - LdapOp::SearchResultEntry(LdapSearchResultEntry { - dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(), - attributes: vec![LdapPartialAttribute { - atype: "cn".to_string(), - vals: vec![b"group_1".to_vec()] - },], - }), - make_search_success(), - ]) - ); - } - - #[tokio::test] - async fn test_search_group_as_scope() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_groups() - .with(eq(Some(GroupRequestFilter::DisplayName( - "rockstars".into(), - )))) - .times(1) - .return_once(|_| Ok(vec![])); - let ldap_handler = setup_bound_readonly_handler(mock).await; - - let request = LdapSearchRequest { - base: "uid=rockstars,ou=groups,Dc=example,dc=com".to_string(), - scope: LdapSearchScope::Base, - aliases: LdapDerefAliases::Never, - sizelimit: 0, - timelimit: 0, - typesonly: false, - filter: LdapFilter::And(vec![]), - attrs: vec!["1.1".to_string()], - }; - assert_eq!( - ldap_handler.do_search_or_dse(&request).await, - Ok(vec![make_search_success()]), - ); - } - #[tokio::test] async fn test_search_groups_unsupported_substring() { let ldap_handler = setup_bound_readonly_handler(MockTestBackendHandler::new()).await; @@ -1541,6 +886,7 @@ mod tests { Ok(vec![make_search_success()]) ); } + #[tokio::test] async fn test_search_member_of_filter_error() { let mut mock = MockTestBackendHandler::new(); @@ -2101,59 +1447,4 @@ mod tests { ]), ); } - - #[tokio::test] - async fn test_pwd_changed_time_format() { - use chrono::prelude::*; - let mut mock = MockTestBackendHandler::new(); - let test_date = chrono::Utc - .with_ymd_and_hms(2024, 12, 31, 14, 30, 45) - .unwrap() - .naive_utc(); - - mock.expect_list_users().times(1).return_once(move |_, _| { - Ok(vec![UserAndGroups { - user: User { - user_id: UserId::new("testuser"), - email: "test@example.com".into(), - display_name: Some("Test User".to_string()), - uuid: uuid!("12345678-9abc-def0-1234-56789abcdef0"), - password_modified_date: test_date, - ..Default::default() - }, - groups: None, - }]) - }); - let ldap_handler = setup_bound_admin_handler(mock).await; - - let request = make_search_request( - "ou=people,dc=example,dc=com", - LdapFilter::Equality("uid".into(), "testuser".into()), - vec!["pwdChangedTime"], - ); - - let result = ldap_handler.do_search_or_dse(&request).await.unwrap(); - - if let LdapOp::SearchResultEntry(entry) = &result[0] { - assert_eq!(entry.dn, "uid=testuser,ou=people,dc=example,dc=com"); - assert_eq!(entry.attributes.len(), 1); - - let pwd_changed_time_attr = &entry.attributes[0]; - assert_eq!(pwd_changed_time_attr.atype, "pwdChangedTime"); - assert_eq!(pwd_changed_time_attr.vals.len(), 1); - - let timestamp_str = std::str::from_utf8(&pwd_changed_time_attr.vals[0]) - .expect("Invalid UTF-8 in timestamp"); - - // Verify it's in GeneralizedTime format (YYYYMMDDHHMMSSZ) - assert_eq!(timestamp_str, "20241231143045Z"); - - // Verify the format can be parsed back correctly - let parsed_time = chrono::NaiveDateTime::parse_from_str(timestamp_str, "%Y%m%d%H%M%SZ") - .expect("Invalid GeneralizedTime format"); - assert_eq!(parsed_time, test_date); - } else { - panic!("Expected SearchResultEntry"); - } - } }