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