Search code examples
javaldap

Java LDAP query with paging not working, PagedResultsControl not honored


Here's my code, basically according to the sample in https://docs.oracle.com/javase/7/docs/api/javax/naming/ldap/PagedResultsControl.html:

package com.igsl.ldapuserattributes;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl;

import org.apache.log4j.Logger;
import org.codehaus.jackson.map.ObjectMapper;

public class LDAPUserAttributes {
    
    private static final Logger LOGGER = Logger.getLogger(LDAPUserAttributes.class);
    private static final ObjectMapper OM = new ObjectMapper();
    private static final String AUTH_METHOD = "simple";
    
    public static void main(String[] args) throws Exception {
        String[] readAttrs = new String[] {
            "distinguishedName",
            "sAMAccountName",
            "displayName",
            "mail",
            "telephone",
            "memberOf",
            "createTimestamp",
            "modifyTimestamp",
            "objectClass",
            "dn"
        };
        Map<String, Map<String, List<String>>> ad = getLDAPUsers(
                "ldap://192.168.56.120:389", 
                "CN=Administrator,CN=Users,DC=win2022,DC=kcwong,DC=igsl", 
                "P@ssw0rd", 
                "CN=Users,DC=win2022,DC=kcwong,DC=igsl", 
                "(&(objectClass=user)(objectClass=user)(|(sAMAccountName=t*)(sAMAccountName=a*)))", 
                SearchControls.SUBTREE_SCOPE,
                readAttrs);
        System.out.println(OM.writeValueAsString(ad));

    Map<String, Map<String, List<String>>> apacheDS = getLDAPUsers(
                "ldap://127.0.0.1:10389", 
                "uid=admin,ou=system", 
                "admin", 
                "ou=users,ou=system", 
                "(&(objectClass=person))", 
                SearchControls.SUBTREE_SCOPE,
                readAttrs);
        System.out.println(OM.writeValueAsString(apacheDS));
    }
        
    public static Map<String, Map<String, List<String>>> getLDAPUsers(String url, String principal, String credential, String baseDN, String filter, int scope, String[] readAttrs) throws Exception {
        Map<String, Map<String, List<String>>> output = new HashMap<String, Map<String, List<String>>>();
        // Note: Jira uses OSGi and does not export com.sun.* classes. 
        // So LdapCtxFactory is not available when using a JobRunner's classloader.
        // We need to switch class loader for this thread.
        final Thread currentThread = Thread.currentThread();
        final ClassLoader originalClassLoader = currentThread.getContextClassLoader();
        try {
            ClassLoader rootClassLoader = ClassLoader.getSystemClassLoader();
            currentThread.setContextClassLoader(rootClassLoader);
            LdapContext ctx = null;
            try {
                final int PAGE_SIZE = 500; // TODO Move to config
                Hashtable env = new Hashtable();
                env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
                env.put(Context.PROVIDER_URL, url);
                env.put(Context.SECURITY_AUTHENTICATION, AUTH_METHOD);
                env.put(Context.SECURITY_PRINCIPAL, principal);
                env.put(Context.SECURITY_CREDENTIALS, credential);
                List<Control> controls = new ArrayList<Control>();
                controls.add(new PagedResultsControl(PAGE_SIZE, Control.CRITICAL)); 
                ctx = new InitialLdapContext(env, null);
                ctx.setRequestControls(new Control[] {
                    new PagedResultsControl(PAGE_SIZE, Control.CRITICAL)
                });
                byte[] pageCookie = null;
                int total = 0;
                System.out.println("Start of LDAP query");
                do {
                    NamingEnumeration<SearchResult> results = ctx.search(
                            baseDN, 
                            filter, 
                            new SearchControls());
                            //new SearchControls(SearchControls.SUBTREE_SCOPE, 0, 0, readAttrs, false, false));
                    System.out.println("results: " + results);
                    int count = 0;
                    Control[] ctrls;
                    ctrls = ctx.getResponseControls();
                    if (ctrls != null) {
                        System.out.println("Before loop Response controls: " + ctrls.length);
                        for (int i = 0; i < ctrls.length; i++) {
                            System.out.println("Response control: " + ctrls[i].getID() + " - " + ctrls[i].getClass().getCanonicalName());
                            if (ctrls[i] instanceof PagedResultsResponseControl) {
                                PagedResultsResponseControl prrc = (PagedResultsResponseControl) ctrls[i];
                                total = prrc.getResultSize();
                                pageCookie = prrc.getCookie();
                                System.out.println("New page cookie: " + OM.writeValueAsString(pageCookie));
                            }
                        }
                    } else {
                        System.out.println("Before loop Response controls is null");
                    }
                    while (results != null && results.hasMore()) {
                        count++;
                        SearchResult result = results.next();
                        Map<String, List<String>> userData = new HashMap<String, List<String>>();
                        Attributes attrs = result.getAttributes();
                        for (int i = 0; i < readAttrs.length; i++) {
                            Attribute attr = attrs.get(readAttrs[i]);
                            if (attr != null) {
                                NamingEnumeration<?> values = attr.getAll();
                                List<String> valueList = new ArrayList<String>();
                                while (values.hasMore()) {
                                    Object value = values.next();
                                    valueList.add(String.valueOf(value));
                                }
                                userData.put(attr.getID(), valueList);
                            }
                        }
                        output.put(result.getName(), userData);
                        System.out.println("Processed user #" + count + ": " + result.getName());
                    }
                    ctrls = ctx.getResponseControls();
                    if (ctrls != null) {
                        System.out.println("After loop Response controls: " + ctrls.length);
                        for (int i = 0; i < ctrls.length; i++) {
                            System.out.println("Response control: " + ctrls[i].getID() + " - " + ctrls[i].getClass().getCanonicalName());
                            if (ctrls[i] instanceof PagedResultsResponseControl) {
                                PagedResultsResponseControl prrc = (PagedResultsResponseControl) ctrls[i];
                                total = prrc.getResultSize();
                                pageCookie = prrc.getCookie();
                                System.out.println("New page cookie: " + OM.writeValueAsString(pageCookie));
                            }
                        }
                    } else {
                        System.out.println("After loop Response controls is null");
                    }
                    ctx.setRequestControls(new Control[] {
                             new PagedResultsControl(PAGE_SIZE, pageCookie, Control.CRITICAL) 
                        });
                } while (pageCookie != null);
                System.out.println("All pages completed");
            } finally {
                if (ctx != null) {
                    ctx.close();
                }
            }
        } catch (Exception ex) {
            System.out.println("LDAP query error: " + ex);
            throw ex;
        } finally {
            currentThread.setContextClassLoader(originalClassLoader);
        }
        return output;
    }
    
}

I have an Windows 2012 with Active Directory server inside a virtual machine. I filled it with 5000 users.

Calling the code (the first segment in main()) with page size set to 500, AD returns no response control (the array is null), and the code will throw exception on the 1001st attempt to call result.hasMore():

LDAP query error: javax.naming.SizeLimitExceededException: [LDAP: error code 4 - Sizelimit Exceeded]; remaining name 'CN=Users,DC=win2022,DC=kcwong,DC=igsl'
Exception in thread "main" javax.naming.SizeLimitExceededException: [LDAP: error code 4 - Sizelimit Exceeded]; remaining name 'CN=Users,DC=win2022,DC=kcwong,DC=igsl'
    at com.sun.jndi.ldap.LdapCtx.mapErrorCode(LdapCtx.java:3311)
    at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:3205)
    at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:2996)
    at com.sun.jndi.ldap.AbstractLdapNamingEnumeration.getNextBatch(AbstractLdapNamingEnumeration.java:148)
    at com.sun.jndi.ldap.AbstractLdapNamingEnumeration.hasMoreImpl(AbstractLdapNamingEnumeration.java:217)
    at com.sun.jndi.ldap.AbstractLdapNamingEnumeration.hasMore(AbstractLdapNamingEnumeration.java:189)
    at com.igsl.ldapuserattributes.LDAPUserAttributes.getLDAPUsers(LDAPUserAttributes.java:112)
    at com.igsl.ldapuserattributes.LDAPUserAttributes.main(LDAPUserAttributes.java:43)

I tried to add a condition to the while loop to stop calling .hasMore() after PAGE_SIZE, then the after loop response control is also null, so without a cookie, the query ended there.

I have a Apache DS (2.0.0.v20210717-M17) as well, again with 5000 users. Calling it (the second segment in main()) with page size set to 500, it also returns no response control (array is null) but it happily allows me to list all 5000 users without issues. If I use the commented SearchControl() with parameters instead of default, then I get the size limit exceeded exception after 500.

It seems both ApacheDS and ActiveDirectory do not honor PagedResultsControl... I recall I used to be able to page many years ago.

The closest question I can find is this: Why doesn't Active Directory return me a PagedResultsResponseControl? And it does not have an answer. The comment about disabling referral is not applicable as I have already tried not using it (the commented line about SearchControls).

Is the code sample now out of date and needs changes? How can I page my query with AD?


Solution

  • Turns out it is the SearchControls parameter in ctx.search().

    I need to avoid using the version with all parameters (which includes max results), that will override PagedResultsControl.

    Instead I need to create one using the default constructor, then override the things I need:

    SearchControls sc = new SearchControls();
    sc.setScope(SearchControls.SUBTREE);
    

    Then it will work properly.