ldap: handle full scope searches

Nextcloud searches for users by specifying the entire user DN as the
scope. This commit adds support for these specific scopes.
This commit is contained in:
Valentin Tolmer 2022-06-10 17:02:06 +02:00 committed by nitnelave
parent da186fab38
commit 733d363e25

View File

@ -48,6 +48,49 @@ fn parse_distinguished_name(dn: &str) -> Result<Vec<(String, String)>> {
.collect() .collect()
} }
#[derive(Debug)]
enum SearchScope {
Global,
Users,
Groups,
User(LdapFilter),
Group(LdapFilter),
Unknown,
Invalid,
}
fn get_search_scope(base_dn: &[(String, String)], dn_parts: &[(String, String)]) -> SearchScope {
let base_dn_len = base_dn.len();
if !is_subtree(dn_parts, base_dn) {
SearchScope::Invalid
} else if dn_parts.len() == base_dn_len {
SearchScope::Global
} else if dn_parts.len() == base_dn_len + 1
&& dn_parts[0] == ("ou".to_string(), "people".to_string())
{
SearchScope::Users
} else if dn_parts.len() == base_dn_len + 1
&& dn_parts[0] == ("ou".to_string(), "groups".to_string())
{
SearchScope::Groups
} else if dn_parts.len() == base_dn_len + 2
&& dn_parts[1] == ("ou".to_string(), "people".to_string())
{
SearchScope::User(LdapFilter::Equality(
dn_parts[0].0.clone(),
dn_parts[0].1.clone(),
))
} else if dn_parts.len() == base_dn_len + 2
&& dn_parts[1] == ("ou".to_string(), "groups".to_string())
{
SearchScope::Group(LdapFilter::Equality(
dn_parts[0].0.clone(),
dn_parts[0].1.clone(),
))
} else {
SearchScope::Unknown
}
}
fn get_group_id_from_distinguished_name( fn get_group_id_from_distinguished_name(
dn: &str, dn: &str,
base_tree: &[(String, String)], base_tree: &[(String, String)],
@ -582,36 +625,50 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
)] )]
} }
}; };
if !is_subtree(&dn_parts, &self.base_dn) { let scope = get_search_scope(&self.base_dn, &dn_parts);
// Search path is not in our tree, just return an empty success. let get_user_list = || async {
warn!( self.get_user_list(&request.filter, &request.attrs, &request.base, &user_filter)
"The specified search tree {:?} is not under the common subtree {:?}", .await
&dn_parts, &self.base_dn };
); let get_group_list = || async {
return vec![make_search_success()]; self.get_groups_list(&request.filter, &request.attrs, &request.base, &user_filter)
} .await
let mut results = Vec::new(); };
let mut got_match = false; let mut results = match scope {
if dn_parts.len() == self.base_dn.len() SearchScope::Global => {
|| (dn_parts.len() == self.base_dn.len() + 1 let mut results = Vec::new();
&& dn_parts[0] == ("ou".to_string(), "people".to_string())) results.extend(get_user_list().await);
{ results.extend(get_group_list().await);
got_match = true; results
results.extend(self.get_user_list(request, &user_filter).await); }
} SearchScope::Users => get_user_list().await,
if dn_parts.len() == self.base_dn.len() SearchScope::Groups => get_group_list().await,
|| (dn_parts.len() == self.base_dn.len() + 1 SearchScope::User(filter) => {
&& dn_parts[0] == ("ou".to_string(), "groups".to_string())) let filter = LdapFilter::And(vec![request.filter.clone(), filter]);
{ self.get_user_list(&filter, &request.attrs, &request.base, &user_filter)
got_match = true; .await
results.extend(self.get_groups_list(request, &user_filter).await); }
} SearchScope::Group(filter) => {
if !got_match { let filter = LdapFilter::And(vec![request.filter.clone(), filter]);
warn!( self.get_groups_list(&filter, &request.attrs, &request.base, &user_filter)
r#"The requested search tree "{}" matches neither the user subtree "ou=people,{}" nor the group subtree "ou=groups,{}""#, .await
&request.base, &self.base_dn_str, &self.base_dn_str }
); SearchScope::Unknown => {
} warn!(
r#"The requested search tree "{}" matches neither the user subtree "ou=people,{}" nor the group subtree "ou=groups,{}""#,
&request.base, &self.base_dn_str, &self.base_dn_str
);
Vec::new()
}
SearchScope::Invalid => {
// Search path is not in our tree, just return an empty success.
warn!(
"The specified search tree {:?} is not under the common subtree {:?}",
&dn_parts, &self.base_dn
);
Vec::new()
}
};
if results.is_empty() || matches!(results[results.len() - 1], LdapOp::SearchResultEntry(_)) if results.is_empty() || matches!(results[results.len() - 1], LdapOp::SearchResultEntry(_))
{ {
results.push(make_search_success()); results.push(make_search_success());
@ -621,10 +678,12 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
async fn get_user_list( async fn get_user_list(
&self, &self,
request: &LdapSearchRequest, filter: &LdapFilter,
attributes: &[String],
base: &str,
user_filter: &Option<&UserId>, user_filter: &Option<&UserId>,
) -> Vec<LdapOp> { ) -> Vec<LdapOp> {
let filters = match self.convert_user_filter(&request.filter) { let filters = match self.convert_user_filter(filter) {
Ok(f) => f, Ok(f) => f,
Err(e) => { Err(e) => {
return vec![make_search_error( return vec![make_search_error(
@ -639,8 +698,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
UserRequestFilter::And(vec![filters, UserRequestFilter::UserId((*u).clone())]) UserRequestFilter::And(vec![filters, UserRequestFilter::UserId((*u).clone())])
} }
}; };
let expanded_attributes = let expanded_attributes = expand_attribute_wildcards(attributes, ALL_USER_ATTRIBUTE_KEYS);
expand_attribute_wildcards(&request.attrs, ALL_USER_ATTRIBUTE_KEYS);
let need_groups = expanded_attributes let need_groups = expanded_attributes
.iter() .iter()
.any(|s| s.to_ascii_lowercase() == "memberof"); .any(|s| s.to_ascii_lowercase() == "memberof");
@ -653,7 +711,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
Err(e) => { Err(e) => {
return vec![make_search_error( return vec![make_search_error(
LdapResultCode::Other, LdapResultCode::Other,
format!(r#"Error during searching user "{}": {:#}"#, request.base, e), format!(r#"Error while searching user "{}": {:#}"#, base, e),
)] )]
} }
}; };
@ -681,10 +739,12 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
async fn get_groups_list( async fn get_groups_list(
&self, &self,
request: &LdapSearchRequest, filter: &LdapFilter,
attributes: &[String],
base: &str,
user_filter: &Option<&UserId>, user_filter: &Option<&UserId>,
) -> Vec<LdapOp> { ) -> Vec<LdapOp> {
let filter = match self.convert_group_filter(&request.filter) { let filter = match self.convert_group_filter(filter) {
Ok(f) => f, Ok(f) => f,
Err(e) => { Err(e) => {
return vec![make_search_error( return vec![make_search_error(
@ -705,7 +765,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
Err(e) => { Err(e) => {
return vec![make_search_error( return vec![make_search_error(
LdapResultCode::Other, LdapResultCode::Other,
format!(r#"Error while listing groups "{}": {:#}"#, request.base, e), format!(r#"Error while listing groups "{}": {:#}"#, base, e),
)] )]
} }
}; };
@ -716,7 +776,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
make_ldap_search_group_result_entry( make_ldap_search_group_result_entry(
u, u,
&self.base_dn_str, &self.base_dn_str,
&request.attrs, attributes,
user_filter, user_filter,
&self.ignored_group_attributes, &self.ignored_group_attributes,
) )
@ -1180,6 +1240,37 @@ mod tests {
); );
} }
#[tokio::test]
async fn test_search_user_as_scope() {
let mut mock = MockTestBackendHandler::new();
mock.expect_list_users()
.with(
eq(Some(UserRequestFilter::And(vec![
UserRequestFilter::And(vec![]),
UserRequestFilter::UserId(UserId::new("bob")),
]))),
eq(false),
)
.times(1)
.return_once(|_, _| Ok(vec![]));
let mut 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(&request).await,
vec![make_search_success()],
);
}
#[tokio::test] #[tokio::test]
async fn test_bind_invalid_dn() { async fn test_bind_invalid_dn() {
let mock = MockTestBackendHandler::new(); let mock = MockTestBackendHandler::new();
@ -1573,6 +1664,34 @@ mod tests {
); );
} }
#[tokio::test]
async fn test_search_group_as_scope() {
let mut mock = MockTestBackendHandler::new();
mock.expect_list_groups()
.with(eq(Some(GroupRequestFilter::And(vec![
GroupRequestFilter::And(vec![]),
GroupRequestFilter::DisplayName("rockstars".to_string()),
]))))
.times(1)
.return_once(|_| Ok(vec![]));
let mut 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(&request).await,
vec![make_search_success()],
);
}
#[tokio::test] #[tokio::test]
async fn test_search_groups_error() { async fn test_search_groups_error() {
let mut mock = MockTestBackendHandler::new(); let mut mock = MockTestBackendHandler::new();