mirror of
https://github.com/nitnelave/lldap.git
synced 2023-04-12 14:25:13 +00:00
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:
parent
da186fab38
commit
733d363e25
@ -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);
|
||||||
|
let get_user_list = || async {
|
||||||
|
self.get_user_list(&request.filter, &request.attrs, &request.base, &user_filter)
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
let get_group_list = || async {
|
||||||
|
self.get_groups_list(&request.filter, &request.attrs, &request.base, &user_filter)
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
let mut results = match scope {
|
||||||
|
SearchScope::Global => {
|
||||||
|
let mut results = Vec::new();
|
||||||
|
results.extend(get_user_list().await);
|
||||||
|
results.extend(get_group_list().await);
|
||||||
|
results
|
||||||
|
}
|
||||||
|
SearchScope::Users => get_user_list().await,
|
||||||
|
SearchScope::Groups => get_group_list().await,
|
||||||
|
SearchScope::User(filter) => {
|
||||||
|
let filter = LdapFilter::And(vec![request.filter.clone(), filter]);
|
||||||
|
self.get_user_list(&filter, &request.attrs, &request.base, &user_filter)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
SearchScope::Group(filter) => {
|
||||||
|
let filter = LdapFilter::And(vec![request.filter.clone(), filter]);
|
||||||
|
self.get_groups_list(&filter, &request.attrs, &request.base, &user_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.base_dn_str, &self.base_dn_str
|
||||||
|
);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
SearchScope::Invalid => {
|
||||||
// Search path is not in our tree, just return an empty success.
|
// Search path is not in our tree, just return an empty success.
|
||||||
warn!(
|
warn!(
|
||||||
"The specified search tree {:?} is not under the common subtree {:?}",
|
"The specified search tree {:?} is not under the common subtree {:?}",
|
||||||
&dn_parts, &self.base_dn
|
&dn_parts, &self.base_dn
|
||||||
);
|
);
|
||||||
return vec![make_search_success()];
|
Vec::new()
|
||||||
}
|
|
||||||
let mut results = Vec::new();
|
|
||||||
let mut got_match = false;
|
|
||||||
if dn_parts.len() == self.base_dn.len()
|
|
||||||
|| (dn_parts.len() == self.base_dn.len() + 1
|
|
||||||
&& dn_parts[0] == ("ou".to_string(), "people".to_string()))
|
|
||||||
{
|
|
||||||
got_match = true;
|
|
||||||
results.extend(self.get_user_list(request, &user_filter).await);
|
|
||||||
}
|
|
||||||
if dn_parts.len() == self.base_dn.len()
|
|
||||||
|| (dn_parts.len() == self.base_dn.len() + 1
|
|
||||||
&& dn_parts[0] == ("ou".to_string(), "groups".to_string()))
|
|
||||||
{
|
|
||||||
got_match = true;
|
|
||||||
results.extend(self.get_groups_list(request, &user_filter).await);
|
|
||||||
}
|
|
||||||
if !got_match {
|
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
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();
|
||||||
|
Loading…
Reference in New Issue
Block a user