From 21e51c3d388b140a750c125be1c10697de7155c9 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Mon, 13 Feb 2023 12:03:14 +0100 Subject: [PATCH] server: Add support for LdapCompare op --- Cargo.lock | 34 ++- Cargo.toml | 5 - server/Cargo.toml | 2 +- server/src/domain/ldap/group.rs | 47 ++-- server/src/domain/ldap/user.rs | 77 +++--- server/src/domain/ldap/utils.rs | 2 +- server/src/infra/ldap_handler.rs | 427 ++++++++++++++++++++++++------- 7 files changed, 439 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 56eb8f9..733a60a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2185,13 +2185,16 @@ dependencies = [ [[package]] name = "ldap3_proto" -version = "0.2.3" -source = "git+https://github.com/nitnelave/ldap3_server/?rev=7b50b2b82c383f5f70e02e11072bb916629ed2bc#7b50b2b82c383f5f70e02e11072bb916629ed2bc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4162706b6f3b3d58f577990e22e9a0e03e2f9bedc2b8181d8abab2498da32003" dependencies = [ "bytes", "lber", + "peg", "tokio-util 0.7.3", "tracing", + "uuid 1.2.2", ] [[package]] @@ -2864,6 +2867,33 @@ dependencies = [ "syn", ] +[[package]] +name = "peg" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07f2cafdc3babeebc087e499118343442b742cc7c31b4d054682cc598508554" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a90084dc05cf0428428e3d12399f39faad19b0909f64fb9170c9fdd6d9cd49b" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa00462b37ead6d11a82c9d568b26682d78e0477dc02d1966c013af80969739" + [[package]] name = "pem-rfc7468" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index f2283cd..85e1dd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,11 +8,6 @@ members = [ default-members = ["server"] -# Remove once https://github.com/kanidm/ldap3_proto/pull/8 is merged. -[patch.crates-io.ldap3_proto] -git = 'https://github.com/nitnelave/ldap3_server/' -rev = '7b50b2b82c383f5f70e02e11072bb916629ed2bc' - [patch.crates-io.opaque-ke] git = 'https://github.com/nitnelave/opaque-ke/' branch = 'zeroize_1.5' diff --git a/server/Cargo.toml b/server/Cargo.toml index 04d212f..824b569 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -28,7 +28,7 @@ itertools = "0.10.1" juniper = "0.15.10" juniper_actix = "0.4.0" jwt = "0.13" -ldap3_proto = "*" +ldap3_proto = ">=0.3.1" log = "*" orion = "0.16" rustls = "0.20" diff --git a/server/src/domain/ldap/group.rs b/server/src/domain/ldap/group.rs index 4555e67..a2cd2a9 100644 --- a/server/src/domain/ldap/group.rs +++ b/server/src/domain/ldap/group.rs @@ -17,7 +17,7 @@ use super::{ }, }; -fn get_group_attribute( +pub fn get_group_attribute( group: &Group, base_dn_str: &str, attribute: &str, @@ -29,8 +29,8 @@ fn get_group_attribute( "objectclass" => vec![b"groupOfUniqueNames".to_vec()], // Always returned as part of the base response. "dn" | "distinguishedname" => return None, - "cn" | "uid" => vec![group.display_name.clone().into_bytes()], - "entryuuid" => vec![group.uuid.to_string().into_bytes()], + "cn" | "uid" | "id" => vec![group.display_name.clone().into_bytes()], + "entryuuid" | "uuid" => vec![group.uuid.to_string().into_bytes()], "member" | "uniquemember" => group .users .iter() @@ -73,6 +73,10 @@ const ALL_GROUP_ATTRIBUTE_KEYS: &[&str] = &[ "entryuuid", ]; +fn expand_group_attribute_wildcards(attributes: &[String]) -> Vec<&str> { + expand_attribute_wildcards(attributes, ALL_GROUP_ATTRIBUTE_KEYS) +} + fn make_ldap_search_group_result_entry( group: Group, base_dn_str: &str, @@ -80,7 +84,7 @@ fn make_ldap_search_group_result_entry( user_filter: &Option<&UserId>, ignored_group_attributes: &[String], ) -> LdapSearchResultEntry { - let expanded_attributes = expand_attribute_wildcards(attributes, ALL_GROUP_ATTRIBUTE_KEYS); + let expanded_attributes = expand_group_attribute_wildcards(attributes); LdapSearchResultEntry { dn: format!("cn={},ou=groups,{}", group.display_name, base_dn_str), @@ -185,11 +189,10 @@ fn convert_group_filter( pub async fn get_groups_list( ldap_info: &LdapInfo, ldap_filter: &LdapFilter, - attributes: &[String], base: &str, user_filter: &Option<&UserId>, backend: &mut Backend, -) -> LdapResult> { +) -> LdapResult> { debug!(?ldap_filter); let filter = convert_group_filter(ldap_info, ldap_filter)?; let parsed_filters = match user_filter { @@ -200,24 +203,28 @@ pub async fn get_groups_list( } }; debug!(?parsed_filters); - let groups = backend + backend .list_groups(Some(parsed_filters)) .await .map_err(|e| LdapError { code: LdapResultCode::Other, message: format!(r#"Error while listing groups "{}": {:#}"#, base, e), - })?; - - Ok(groups - .into_iter() - .map(|u| { - LdapOp::SearchResultEntry(make_ldap_search_group_result_entry( - u, - &ldap_info.base_dn_str, - attributes, - user_filter, - &ldap_info.ignored_group_attributes, - )) }) - .collect::>()) +} + +pub fn convert_groups_to_ldap_op<'a>( + groups: Vec, + attributes: &'a [String], + ldap_info: &'a LdapInfo, + user_filter: &'a Option<&'a UserId>, +) -> impl Iterator + 'a { + groups.into_iter().map(move |g| { + LdapOp::SearchResultEntry(make_ldap_search_group_result_entry( + g, + &ldap_info.base_dn_str, + attributes, + user_filter, + &ldap_info.ignored_group_attributes, + )) + }) } diff --git a/server/src/domain/ldap/user.rs b/server/src/domain/ldap/user.rs index caddb6d..ab27a07 100644 --- a/server/src/domain/ldap/user.rs +++ b/server/src/domain/ldap/user.rs @@ -10,7 +10,7 @@ use crate::domain::{ error::LdapError, utils::{expand_attribute_wildcards, get_user_id_from_distinguished_name}, }, - types::{GroupDetails, User, UserColumn, UserId}, + types::{GroupDetails, User, UserAndGroups, UserColumn, UserId}, }; use super::{ @@ -18,7 +18,7 @@ use super::{ utils::{get_group_id_from_distinguished_name, map_user_field, LdapInfo}, }; -fn get_user_attribute( +pub fn get_user_attribute( user: &User, attribute: &str, base_dn_str: &str, @@ -35,12 +35,12 @@ fn get_user_attribute( ], // dn is always returned as part of the base response. "dn" | "distinguishedname" => return None, - "uid" => vec![user.user_id.to_string().into_bytes()], - "entryuuid" => vec![user.uuid.to_string().into_bytes()], - "mail" => vec![user.email.clone().into_bytes()], - "givenname" => vec![user.first_name.clone()?.into_bytes()], - "sn" => vec![user.last_name.clone()?.into_bytes()], - "jpegphoto" => vec![user.avatar.clone()?.into_bytes()], + "uid" | "user_id" | "id" => vec![user.user_id.to_string().into_bytes()], + "entryuuid" | "uuid" => vec![user.uuid.to_string().into_bytes()], + "mail" | "email" => vec![user.email.clone().into_bytes()], + "givenname" | "first_name" | "firstname" => vec![user.first_name.clone()?.into_bytes()], + "sn" | "last_name" | "lastname" => vec![user.last_name.clone()?.into_bytes()], + "jpegphoto" | "avatar" => vec![user.avatar.clone()?.into_bytes()], "memberof" => groups .into_iter() .flatten() @@ -53,10 +53,12 @@ fn get_user_attribute( }) .collect(), "cn" | "displayname" => vec![user.display_name.clone()?.into_bytes()], - "createtimestamp" | "modifytimestamp" => vec![chrono::Utc - .from_utc_datetime(&user.creation_date) - .to_rfc3339() - .into_bytes()], + "creationdate" | "creation_date" | "createtimestamp" | "modifytimestamp" => { + vec![chrono::Utc + .from_utc_datetime(&user.creation_date) + .to_rfc3339() + .into_bytes()] + } "1.1" => return None, // We ignore the operational attribute wildcard. "+" => return None, @@ -99,15 +101,17 @@ const ALL_USER_ATTRIBUTE_KEYS: &[&str] = &[ fn make_ldap_search_user_result_entry( user: User, base_dn_str: &str, - attributes: &[&str], + attributes: &[String], groups: Option<&[GroupDetails]>, ignored_user_attributes: &[String], ) -> LdapSearchResultEntry { + let expanded_attributes = expand_user_attribute_wildcards(attributes); let dn = format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str); + dbg!(&attributes, &expanded_attributes, &user); LdapSearchResultEntry { dn, - attributes: attributes + attributes: expanded_attributes .iter() .filter_map(|a| { let values = @@ -188,15 +192,19 @@ fn convert_user_filter(ldap_info: &LdapInfo, filter: &LdapFilter) -> LdapResult< } } +fn expand_user_attribute_wildcards(attributes: &[String]) -> Vec<&str> { + expand_attribute_wildcards(attributes, ALL_USER_ATTRIBUTE_KEYS) +} + #[instrument(skip_all, level = "debug")] pub async fn get_user_list( ldap_info: &LdapInfo, ldap_filter: &LdapFilter, - attributes: &[String], + request_groups: bool, base: &str, user_filter: &Option<&UserId>, backend: &mut Backend, -) -> LdapResult> { +) -> LdapResult> { debug!(?ldap_filter); let filters = convert_user_filter(ldap_info, ldap_filter)?; let parsed_filters = match user_filter { @@ -207,28 +215,27 @@ pub async fn get_user_list( } }; debug!(?parsed_filters); - let expanded_attributes = expand_attribute_wildcards(attributes, ALL_USER_ATTRIBUTE_KEYS); - let need_groups = expanded_attributes - .iter() - .any(|s| s.to_ascii_lowercase() == "memberof"); - let users = backend - .list_users(Some(parsed_filters), need_groups) + backend + .list_users(Some(parsed_filters), request_groups) .await .map_err(|e| LdapError { code: LdapResultCode::Other, message: format!(r#"Error while searching user "{}": {:#}"#, base, e), - })?; - - Ok(users - .into_iter() - .map(|u| { - LdapOp::SearchResultEntry(make_ldap_search_user_result_entry( - u.user, - &ldap_info.base_dn_str, - &expanded_attributes, - u.groups.as_deref(), - &ldap_info.ignored_user_attributes, - )) }) - .collect::>()) +} + +pub fn convert_users_to_ldap_op<'a>( + users: Vec, + attributes: &'a [String], + ldap_info: &'a LdapInfo, +) -> impl Iterator + 'a { + users.into_iter().map(move |u| { + LdapOp::SearchResultEntry(make_ldap_search_user_result_entry( + u.user, + &ldap_info.base_dn_str, + attributes, + u.groups.as_deref(), + &ldap_info.ignored_user_attributes, + )) + }) } diff --git a/server/src/domain/ldap/utils.rs b/server/src/domain/ldap/utils.rs index e2cbec4..d84c430 100644 --- a/server/src/domain/ldap/utils.rs +++ b/server/src/domain/ldap/utils.rs @@ -143,7 +143,7 @@ pub fn map_user_field(field: &str) -> Option { "cn" | "displayname" | "display_name" => UserColumn::DisplayName, "givenname" | "first_name" => UserColumn::FirstName, "sn" | "last_name" => UserColumn::LastName, - "avatar" => UserColumn::Avatar, + "avatar" | "jpegphoto" => UserColumn::Avatar, "creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => { UserColumn::CreationDate } diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index 2c5e6ce..3527900 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -3,23 +3,23 @@ use crate::{ handler::{BackendHandler, BindRequest, CreateUserRequest, LoginHandler}, ldap::{ error::{LdapError, LdapResult}, - group::get_groups_list, - user::get_user_list, + group::{convert_groups_to_ldap_op, get_groups_list}, + user::{convert_users_to_ldap_op, get_user_list}, utils::{ get_user_id_from_distinguished_name, is_subtree, parse_distinguished_name, LdapInfo, }, }, opaque_handler::OpaqueHandler, - types::{JpegPhoto, UserId}, + types::{Group, JpegPhoto, UserAndGroups, UserId}, }, infra::auth_service::{Permission, ValidationResults}, }; use anyhow::Result; use ldap3_proto::proto::{ - LdapAddRequest, LdapBindCred, LdapBindRequest, LdapBindResponse, LdapExtendedRequest, - LdapExtendedResponse, LdapFilter, LdapOp, LdapPartialAttribute, LdapPasswordModifyRequest, - LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, LdapSearchResultEntry, - LdapSearchScope, + LdapAddRequest, LdapBindCred, LdapBindRequest, LdapBindResponse, LdapCompareRequest, + LdapDerefAliases, LdapExtendedRequest, LdapExtendedResponse, LdapFilter, LdapOp, + LdapPartialAttribute, LdapPasswordModifyRequest, LdapResult as LdapResultOp, LdapResultCode, + LdapSearchRequest, LdapSearchResultEntry, LdapSearchScope, }; use std::collections::HashMap; use tracing::{debug, instrument, warn}; @@ -71,6 +71,23 @@ fn get_search_scope(base_dn: &[(String, String)], dn_parts: &[(String, String)]) } } +fn make_search_request>( + base: &str, + filter: LdapFilter, + attrs: Vec, +) -> LdapSearchRequest { + LdapSearchRequest { + base: base.to_string(), + scope: LdapSearchScope::Base, + aliases: LdapDerefAliases::Never, + sizelimit: 0, + timelimit: 0, + typesonly: false, + filter, + attrs: attrs.into_iter().map(Into::into).collect(), + } +} + fn make_search_success() -> LdapOp { make_search_error(LdapResultCode::Success, "".to_string()) } @@ -334,6 +351,18 @@ impl LdapHandler LdapResult> { + let user_info = self.user_info.as_ref().ok_or_else(|| LdapError { + code: LdapResultCode::InsufficentAccessRights, + message: "No user currently bound".to_string(), + })?; + Ok(if user_info.is_admin_or_readonly() { + None + } else { + Some(user_info.user.clone()) + }) + } + pub async fn do_search_or_dse( &mut self, request: &LdapSearchRequest, @@ -349,30 +378,19 @@ impl LdapHandler, - ) -> LdapResult> { - let user_filter = user_filter.as_ref(); + user_filter: &Option<&UserId>, + ) -> LdapResult<(Option>, Option>)> { let dn_parts = parse_distinguished_name(&request.base.to_ascii_lowercase())?; let scope = get_search_scope(&self.ldap_info.base_dn, &dn_parts); debug!(?request.base, ?scope); // Disambiguate the lifetimes. - fn cast(x: T) -> T + fn cast<'a, T, R, B: 'a>(x: T) -> T where T: Fn(&'a mut B, &'a LdapFilter) -> R + 'a, { @@ -380,12 +398,16 @@ impl LdapHandler LdapHandler = match scope { - SearchScope::Global => { - let mut results = Vec::new(); - results.extend(get_user_list(&mut self.backend_handler, &request.filter).await?); - results.extend(get_group_list(&mut self.backend_handler, &request.filter).await?); - results - } - SearchScope::Users => get_user_list(&mut self.backend_handler, &request.filter).await?, - SearchScope::Groups => { - get_group_list(&mut self.backend_handler, &request.filter).await? - } + Ok(match scope { + SearchScope::Global => ( + Some(get_user_list(&mut self.backend_handler, &request.filter).await?), + Some(get_group_list(&mut self.backend_handler, &request.filter).await?), + ), + SearchScope::Users => ( + Some(get_user_list(&mut self.backend_handler, &request.filter).await?), + None, + ), + SearchScope::Groups => ( + None, + Some(get_group_list(&mut self.backend_handler, &request.filter).await?), + ), SearchScope::User(filter) => { let filter = LdapFilter::And(vec![request.filter.clone(), filter]); - get_user_list(&mut self.backend_handler, &filter).await? + ( + Some(get_user_list(&mut self.backend_handler, &filter).await?), + None, + ) } SearchScope::Group(filter) => { let filter = LdapFilter::And(vec![request.filter.clone(), filter]); - get_group_list(&mut self.backend_handler, &filter).await? + ( + None, + Some(get_group_list(&mut self.backend_handler, &filter).await?), + ) } SearchScope::Unknown => { warn!( r#"The requested search tree "{}" matches neither the user subtree "ou=people,{}" nor the group subtree "ou=groups,{}""#, &request.base, &self.ldap_info.base_dn_str, &self.ldap_info.base_dn_str ); - Vec::new() + (None, None) } SearchScope::Invalid => { // Search path is not in our tree, just return an empty success. @@ -433,9 +462,33 @@ impl LdapHandler LdapResult> { + let user_filter = self.get_user_permission_filter()?; + let user_filter = user_filter.as_ref(); + let (users, groups) = self.do_search_internal(request, &user_filter).await?; + + let mut results = Vec::new(); + if let Some(users) = users { + results.extend(convert_users_to_ldap_op( + users, + &request.attrs, + &self.ldap_info, + )); + } + if let Some(groups) = groups { + results.extend(convert_groups_to_ldap_op( + groups, + &request.attrs, + &self.ldap_info, + &user_filter, + )); + } if results.is_empty() || matches!(results[results.len() - 1], LdapOp::SearchResultEntry(_)) { results.push(make_search_success()); @@ -527,6 +580,57 @@ impl LdapHandler LdapResult> { + dbg!(&request); + let req = make_search_request::( + &self.ldap_info.base_dn_str, + LdapFilter::Equality("dn".to_string(), request.dn.to_string()), + vec![request.atype.clone()], + ); + let entries = self.do_search(&req).await?; + if entries.len() > 2 { + // SearchResultEntry + SearchResultDone + return Err(LdapError { + code: LdapResultCode::OperationsError, + message: "Too many search results".to_string(), + }); + } + + match entries.first() { + Some(LdapOp::SearchResultEntry(entry)) => { + dbg!(&entry.attributes); + let available = entry + .attributes + .iter() + .any(|attr| attr.atype == request.atype && 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(), + }), + } + } + pub async fn handle_ldap_message(&mut self, ldap_op: LdapOp) -> Option> { Some(match ldap_op { LdapOp::BindRequest(request) => { @@ -555,6 +659,10 @@ impl LdapHandler self + .do_compare(request) + .await + .unwrap_or_else(|e: LdapError| vec![make_search_error(e.code, e.message)]), op => vec![make_extended_response( LdapResultCode::UnwillingToPerform, format!("Unsupported operation: {:#?}", op), @@ -625,23 +733,6 @@ mod tests { } } - fn make_search_request>( - base: &str, - filter: LdapFilter, - attrs: Vec, - ) -> LdapSearchRequest { - LdapSearchRequest { - base: base.to_string(), - scope: LdapSearchScope::Base, - aliases: LdapDerefAliases::Never, - sizelimit: 0, - timelimit: 0, - typesonly: false, - filter, - attrs: attrs.into_iter().map(Into::into).collect(), - } - } - fn make_user_search_request>( filter: LdapFilter, attrs: Vec, @@ -649,6 +740,13 @@ mod tests { make_search_request::("ou=people,Dc=example,dc=com", filter, attrs) } + fn make_group_search_request>( + filter: LdapFilter, + attrs: Vec, + ) -> LdapSearchRequest { + make_search_request::("ou=groups,dc=example,dc=com", filter, attrs) + } + async fn setup_bound_handler_with_group( mut mock: MockTestBackendHandler, group: &str, @@ -778,7 +876,7 @@ mod tests { mock.expect_list_users() .with( eq(Some(UserRequestFilter::And(vec![ - UserRequestFilter::from(true), + true.into(), UserRequestFilter::UserId(UserId::new("test")), ]))), eq(false), @@ -813,7 +911,7 @@ mod tests { async fn test_search_readonly_user() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_users() - .with(eq(Some(UserRequestFilter::from(true))), eq(false)) + .with(eq(Some(true.into())), eq(false)) .times(1) .return_once(|_, _| Ok(vec![])); let mut ldap_handler = setup_bound_readonly_handler(mock).await; @@ -830,7 +928,7 @@ mod tests { async fn test_search_member_of() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_users() - .with(eq(Some(UserRequestFilter::from(true))), eq(true)) + .with(eq(Some(true.into())), eq(true)) .times(1) .return_once(|_, _| { Ok(vec![UserAndGroups { @@ -873,7 +971,7 @@ mod tests { mock.expect_list_users() .with( eq(Some(UserRequestFilter::And(vec![ - UserRequestFilter::from(true), + true.into(), UserRequestFilter::UserId(UserId::new("bob")), ]))), eq(false), @@ -1131,7 +1229,7 @@ mod tests { async fn test_search_groups() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_groups() - .with(eq(Some(GroupRequestFilter::from(true)))) + .with(eq(Some(true.into()))) .times(1) .return_once(|_| { Ok(vec![ @@ -1152,8 +1250,7 @@ mod tests { ]) }); let mut ldap_handler = setup_bound_admin_handler(mock).await; - let request = make_search_request( - "ou=groups,dc=example,dc=cOm", + let request = make_group_search_request( LdapFilter::And(vec![]), vec!["objectClass", "dn", "cn", "uniqueMember", "entryUuid"], ); @@ -1218,12 +1315,13 @@ mod tests { GroupRequestFilter::DisplayName("group_1".to_string()), GroupRequestFilter::Member(UserId::new("bob")), GroupRequestFilter::DisplayName("rockstars".to_string()), - GroupRequestFilter::from(true), - GroupRequestFilter::from(true), - GroupRequestFilter::from(true), - GroupRequestFilter::from(true), - GroupRequestFilter::Not(Box::new(GroupRequestFilter::from(false))), - GroupRequestFilter::from(false), + false.into(), + true.into(), + true.into(), + true.into(), + true.into(), + GroupRequestFilter::Not(Box::new(false.into())), + false.into(), ])))) .times(1) .return_once(|_| { @@ -1236,8 +1334,7 @@ mod tests { }]) }); let mut ldap_handler = setup_bound_admin_handler(mock).await; - let request = make_search_request( - "ou=groups,dc=example,dc=com", + let request = make_group_search_request( LdapFilter::And(vec![ LdapFilter::Equality("cN".to_string(), "Group_1".to_string()), LdapFilter::Equality( @@ -1248,6 +1345,10 @@ mod tests { "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("obJEctclass".to_string(), "groupofUniqueNames".to_string()), LdapFilter::Equality("objectclass".to_string(), "groupOfNames".to_string()), LdapFilter::Present("objectclass".to_string()), @@ -1291,8 +1392,7 @@ mod tests { }]) }); let mut ldap_handler = setup_bound_admin_handler(mock).await; - let request = make_search_request( - "ou=groups,dc=example,dc=com", + let request = make_group_search_request( LdapFilter::Or(vec![LdapFilter::Not(Box::new(LdapFilter::Equality( "displayname".to_string(), "group_2".to_string(), @@ -1319,7 +1419,7 @@ mod tests { let mut mock = MockTestBackendHandler::new(); mock.expect_list_groups() .with(eq(Some(GroupRequestFilter::And(vec![ - GroupRequestFilter::from(true), + true.into(), GroupRequestFilter::DisplayName("rockstars".to_string()), ])))) .times(1) @@ -1358,8 +1458,7 @@ mod tests { )) }); let mut ldap_handler = setup_bound_admin_handler(mock).await; - let request = make_search_request( - "ou=groups,dc=example,dc=com", + let request = make_group_search_request( LdapFilter::Or(vec![LdapFilter::Not(Box::new(LdapFilter::Equality( "displayname".to_string(), "group_2".to_string(), @@ -1378,8 +1477,7 @@ mod tests { #[tokio::test] async fn test_search_groups_filter_error() { let mut ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await; - let request = make_search_request( - "ou=groups,dc=example,dc=com", + let request = make_group_search_request( LdapFilter::And(vec![LdapFilter::Substring( "whatever".to_string(), ldap3_proto::proto::LdapSubstringFilter::default(), @@ -1407,12 +1505,13 @@ mod tests { "bob", )))), UserRequestFilter::UserId("bob_1".to_string().into()), - UserRequestFilter::from(true), - UserRequestFilter::from(false), - UserRequestFilter::from(true), - UserRequestFilter::from(true), - UserRequestFilter::from(false), - UserRequestFilter::from(false), + false.into(), + true.into(), + false.into(), + true.into(), + true.into(), + false.into(), + false.into(), ], )]))), eq(false), @@ -1430,6 +1529,10 @@ mod tests { "dn".to_string(), "uid=bob_1,ou=people,dc=example,dc=com".to_string(), ), + LdapFilter::Equality( + "dn".to_string(), + "uid=bob_1,ou=groups,dc=example,dc=com".to_string(), + ), LdapFilter::Equality("objectclass".to_string(), "persOn".to_string()), LdapFilter::Equality("objectclass".to_string(), "other".to_string()), LdapFilter::Present("objectClass".to_string()), @@ -1560,7 +1663,7 @@ mod tests { }]) }); mock.expect_list_groups() - .with(eq(Some(GroupRequestFilter::from(true)))) + .with(eq(Some(true.into()))) .times(1) .return_once(|_| { Ok(vec![Group { @@ -1635,7 +1738,7 @@ mod tests { }]) }); mock.expect_list_groups() - .with(eq(Some(GroupRequestFilter::from(true)))) + .with(eq(Some(true.into()))) .returning(|_| { Ok(vec![Group { id: GroupId(1), @@ -2091,7 +2194,7 @@ mod tests { async fn test_search_filter_non_attribute() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_users() - .with(eq(Some(UserRequestFilter::from(true))), eq(false)) + .with(eq(Some(true.into())), eq(false)) .times(1) .return_once(|_, _| Ok(vec![])); let mut ldap_handler = setup_bound_admin_handler(mock).await; @@ -2104,4 +2207,146 @@ mod tests { Ok(vec![make_search_success()]) ); } + + #[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".to_string(), + ..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".to_owned()))); + Ok(vec![Group { + id: GroupId(1), + display_name: "group".to_string(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + users: vec![UserId::new("bob")], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), + }]) + }); + 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".to_string(), + ..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![], + })]) + ); + } }