This is an automated email from the ASF dual-hosted git repository.

smolnar82 pushed a commit to branch knox_idf
in repository https://gitbox.apache.org/repos/asf/knox.git

commit 6957e3e0cb38d84c9811200fe703c8b9e1fb60e1
Author: David Han <[email protected]>
AuthorDate: Thu Jun 25 04:56:36 2026 -0500

    KNOX-3341: LDAP proxy backend handles general searches (#1274)
    
    This commit improves the behavior of the LdapProxyBackend to handle broader 
search requests
    such as retrieving all users, all groups, or filtering by attributes other 
than uid.
    
    A "search" method was added to the LdapBackend interface to support broader 
search requests.
    
    LdapProxyBackend implements this method by converting the search base, 
objectclass, and user
    identifier attribute from proxy values to values recognized by the remote 
LDAP backend.
    LdapProxyBackend.createProxyEntry was factored out into a new 
RemoteSchemaConverter class. This class
    also contains methods for converting the search filter and DNs. The 
conversions in the search filter
    are further supported by a new FilterMappingVisitor class which will 
traverse the ExprNode tree and replace
    values as needed.
    Result entries are likewise converted from the remote values to the proxy 
values. As part of the mapping,
    the AD 'userAccountControl' attribute type is mapped into the 
'nsAccountLock' attribute type. A new
    DisabledUserInterceptor is implemented to perform this conversion. This 
interceptor can also be
    configured to remove disabled entries from the results.
    
    Group membership retrieval is modified to use 'getUserGroupEntries' as a 
common starting point
    then branch into separate codepaths for using 'memberOf' or not.
    
    FileBackend simply maps the filter back into user search.
    
    (cherry picked from commit 8a1332bdfdaaa75a1fa084262b020c9247a31a95)
---
 .../knox/gateway/services/ldap/LdapMessages.java   |  27 +-
 .../services/ldap/SchemaManagerFactory.java        |  83 +++-
 .../gateway/services/ldap/backend/FileBackend.java |  40 +-
 .../ldap/backend/FilterMappingVisitor.java         | 117 +++++
 .../gateway/services/ldap/backend/LdapBackend.java |  16 +-
 .../services/ldap/backend/LdapProxyBackend.java    | 473 ++++++++++++---------
 .../ldap/backend/RemoteSchemaConverter.java        | 177 ++++++++
 ...terceptor.java => DisabledUserInterceptor.java} |  74 +++-
 .../DisabledUserInterceptorFactory.java            |  37 ++
 .../DuplicateUserFilteringInterceptor.java         |   3 +-
 .../ldap/interceptor/UserSearchInterceptor.java    | 104 +----
 ...ces.ldap.interceptor.KnoxLdapInterceptorFactory |   3 +-
 .../ldap/backend/LdapProxyBackendTest.java         | 397 ++++++++++++++---
 ...rTest.java => DisabledUserInterceptorTest.java} |  93 ++--
 .../DuplicateUserFilteringInterceptorTest.java     |  19 +-
 .../ldap/interceptor/InterceptorTestUtils.java     |  39 ++
 .../test/resources/ldap-proxy-backend-test.ldif    |  19 +
 .../src/test/resources/ldap-recursive-test.ldif    |  86 ++++
 knox-site/docs/service_ldap_server.md              |  46 +-
 19 files changed, 1399 insertions(+), 454 deletions(-)

diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapMessages.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapMessages.java
index 313624e20..a69ad7cd8 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapMessages.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapMessages.java
@@ -105,6 +105,14 @@ public interface LdapMessages {
             text = "Loaded user from backend: {0}")
     void ldapUserLoaded(String username);
 
+    @Message(level = MessageLevel.ERROR,
+            text = "LDAP Lookup failed: {0}, {1}")
+    void ldapLookupFailed(String dn, @StackTrace(level = MessageLevel.DEBUG) 
Exception e);
+
+    @Message(level = MessageLevel.INFO,
+            text = "AttributeType not found: {0}")
+    void ldapAttributeTypeNotFound(String attribute);
+
     @Message(level = MessageLevel.INFO,
             text = "Cleaning up old lock file: {0}")
     void ldapCleaningLockFile(String lockFile);
@@ -138,17 +146,20 @@ public interface LdapMessages {
     @Message(level = MessageLevel.DEBUG, text = "Recursive group search 
enabled: {0}, max depth: {1}")
     void ldapRecursiveGroupSearchConfig(boolean enabled, int maxDepth);
 
-    @Message(level = MessageLevel.DEBUG, text = "Recursive group search for 
user {0} found {1} group(s) ({2}) at depth {3}")
-    void ldapRecursiveGroupSearchProgress(String user, int count, String 
groups, int depth);
+    @Message(level = MessageLevel.DEBUG, text = "Recursive group search for 
entry {0} found {1} group(s) ({2}) at depth {3}")
+    void ldapRecursiveGroupSearchProgress(String entryName, int count, String 
groups, int depth);
+
+    @Message(level = MessageLevel.DEBUG, text = "Recursive group search for 
entry {0} completed. Total groups found: {1}")
+    void ldapRecursiveGroupSearchFinished(String entryName, int count);
 
-    @Message(level = MessageLevel.DEBUG, text = "Recursive group search for 
user {0} completed. Total groups found: {1}")
-    void ldapRecursiveGroupSearchFinished(String user, int count);
+    @Message(level = MessageLevel.WARN, text = "Recursive group search for 
entry {0} reached max depth {1}")
+    void ldapRecursiveGroupSearchMaxDepthReached(String entryName, int 
maxDepth);
 
-    @Message(level = MessageLevel.WARN, text = "Recursive group search for 
user {0} reached max depth {1}")
-    void ldapRecursiveGroupSearchMaxDepthReached(String user, int maxDepth);
+    @Message(level = MessageLevel.DEBUG, text = "Cycle detected in recursive 
group search for entry {0} at group {1}")
+    void ldapRecursiveGroupSearchCycleDetected(String entryName, String 
groupDn);
 
-    @Message(level = MessageLevel.DEBUG, text = "Cycle detected in recursive 
group search for user {0} at group {1}")
-    void ldapRecursiveGroupSearchCycleDetected(String user, String groupDn);
+    @Message(level = MessageLevel.WARN, text = "Expected entry for group {0} 
not found in entry cache")
+    void ldapRecursiveGroupSearchExpectedGroupNotInCache(String groupDn);
 
     @Message(level = MessageLevel.DEBUG, text = "Created skeleton group entry 
for {0} as actual group entry was not found in the backend")
     void ldapSkeletonGroupEntryCreated(String groupDn);
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/SchemaManagerFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/SchemaManagerFactory.java
index 611189389..67a04c726 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/SchemaManagerFactory.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/SchemaManagerFactory.java
@@ -19,14 +19,16 @@ package org.apache.knox.gateway.services.ldap;
 
 import org.apache.directory.api.ldap.model.exception.LdapException;
 import org.apache.directory.api.ldap.model.schema.AttributeType;
-import org.apache.directory.api.ldap.model.schema.LdapSyntax;
-import org.apache.directory.api.ldap.model.schema.MatchingRule;
 import org.apache.directory.api.ldap.model.schema.ObjectClass;
 import org.apache.directory.api.ldap.model.schema.SchemaManager;
 import org.apache.directory.api.ldap.schema.loader.JarLdifSchemaLoader;
 import org.apache.directory.api.ldap.schema.manager.impl.DefaultSchemaManager;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 
 /**
  * Factory class for creating SchemaManager instances.
@@ -39,29 +41,74 @@ public class SchemaManagerFactory {
         SchemaManager schemaManager = new DefaultSchemaManager(loader);
         schemaManager.loadAllEnabled();
 
-        // Add Custom schemas
+        List<AttributeType> userAttributeTypes = new ArrayList<>();
+        List<AttributeType> groupAttributeTypes = new ArrayList<>();
+        getCustomAttributeTypes(userAttributeTypes, groupAttributeTypes);
+        getActiveDirectoryAttributeTypes(userAttributeTypes);
+
+        Set<AttributeType> allAttributeTypes = new HashSet<>();
+        allAttributeTypes.addAll(userAttributeTypes);
+        allAttributeTypes.addAll(groupAttributeTypes);
+        for (AttributeType attributeType : allAttributeTypes) {
+            schemaManager.add(attributeType);
+        }
+
+        ObjectClass personObjectClass = 
schemaManager.lookupObjectClassRegistry("person");
+        personObjectClass.unlock();
+        for (AttributeType attributeType : userAttributeTypes) {
+            personObjectClass.addMayAttributeTypes(attributeType);
+        }
+        personObjectClass.lock();
+
+        ObjectClass groupOfNamesObjectClass = 
schemaManager.lookupObjectClassRegistry("groupofnames");
+        groupOfNamesObjectClass.unlock();
+        for (AttributeType attributeType : groupAttributeTypes) {
+            groupOfNamesObjectClass.addMayAttributeTypes(attributeType);
+        }
+        groupOfNamesObjectClass.lock();
+
+        return schemaManager;
+    }
+
+    private static void getCustomAttributeTypes(List<AttributeType> 
userAttributes, List<AttributeType> groupAttributes) {
         AttributeType memberOfAttrType = new 
AttributeType("1.2.840.113556.1.2.102");
-        memberOfAttrType.setNames(new String[]{"memberOf"});
+        memberOfAttrType.setNames("memberOf");
         memberOfAttrType.setSchemaName("other");
-        memberOfAttrType.setSyntax(new 
LdapSyntax("1.3.6.1.4.1.1466.115.121.1.12"));
+        memberOfAttrType.setSyntaxOid("1.3.6.1.4.1.1466.115.121.1.12");
         memberOfAttrType.setDescription("attribute specifies the distinguished 
names of the groups to which this object belongs");
         memberOfAttrType.setSingleValued(false);
-        schemaManager.add(memberOfAttrType);
+        userAttributes.add(memberOfAttrType);
+        groupAttributes.add(memberOfAttrType);
+
+        AttributeType nsAccountLockAttrType = new 
AttributeType("2.16.840.1.113730.3.1.610");
+        nsAccountLockAttrType.setNames("nsAccountLock");
+        nsAccountLockAttrType.setSchemaName("other");
+        nsAccountLockAttrType.setSyntaxOid("1.3.6.1.4.1.1466.115.121.1.15");
+        nsAccountLockAttrType.setEqualityOid("caseIgnoreMatch");
+        nsAccountLockAttrType.setDescription("Operational attribute to 
administratively lock/inactivate accounts");
+        nsAccountLockAttrType.setSingleValued(true);
+        userAttributes.add(nsAccountLockAttrType);
+    }
 
+    private static void getActiveDirectoryAttributeTypes(List<AttributeType> 
userAttributes) {
         AttributeType sAMAccountNameAttrType = new 
AttributeType("1.2.840.113556.1.4.221");
-        sAMAccountNameAttrType.setNames(new String[]{"sAMAccountName"});
+        sAMAccountNameAttrType.setNames("sAMAccountName");
         sAMAccountNameAttrType.setSchemaName("other");
-        sAMAccountNameAttrType.setSyntax(new 
LdapSyntax("1.3.6.1.4.1.1466.115.121.1.15"));
-        sAMAccountNameAttrType.setDescription("Microsoft sAMAccountName 
attribute for compatibility");
-        sAMAccountNameAttrType.setEquality(new 
MatchingRule("caseignorematch"));
-        sAMAccountNameAttrType.setSubstring(new 
MatchingRule("caseignoresubstringsmatch"));
-        schemaManager.add(sAMAccountNameAttrType);
-
-        ObjectClass personObjectClass = 
schemaManager.lookupObjectClassRegistry("person");
-        personObjectClass.unlock();
-        personObjectClass.addMayAttributeTypes(memberOfAttrType, 
sAMAccountNameAttrType);
-        personObjectClass.lock();
+        sAMAccountNameAttrType.setSyntaxOid("1.3.6.1.4.1.1466.115.121.1.15");
+        sAMAccountNameAttrType.setDescription("Microsoft Active Directory 
sAMAccountName attribute for compatibility");
+        sAMAccountNameAttrType.setEqualityOid("2.5.13.2"); // caseignorematch
+        sAMAccountNameAttrType.setSubstringOid("2.5.13.4"); // 
caseignoresubstringsmatch
+        sAMAccountNameAttrType.setSingleValued(true);
+        userAttributes.add(sAMAccountNameAttrType);
 
-        return schemaManager;
+        AttributeType userAccountControlAttrType = new 
AttributeType("1.2.840.113556.1.4.8");
+        userAccountControlAttrType.setNames("userAccountControl");
+        userAccountControlAttrType.setSchemaName("other");
+        
userAccountControlAttrType.setSyntaxOid("1.3.6.1.4.1.1466.115.121.1.27");
+        userAccountControlAttrType.setDescription("Microsoft Active Directory  
User Account Control attribute for compatibility");
+        userAccountControlAttrType.setEqualityOid("2.5.13.14"); // integermatch
+        userAccountControlAttrType.setOrderingOid("2.5.13.15"); // 
integerorderingmatch
+        userAccountControlAttrType.setSingleValued(true);
+        userAttributes.add(userAccountControlAttrType);
     }
 }
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackend.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackend.java
index 8546a3acd..9568f2471 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackend.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackend.java
@@ -20,6 +20,7 @@ package org.apache.knox.gateway.services.ldap.backend;
 import com.google.gson.Gson;
 import org.apache.directory.api.ldap.model.entry.DefaultEntry;
 import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.message.SearchScope;
 import org.apache.directory.api.ldap.model.name.Dn;
 import org.apache.directory.api.ldap.model.schema.SchemaManager;
 import org.apache.knox.gateway.i18n.messages.MessagesFactory;
@@ -33,7 +34,10 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * File-based backend that reads user/group data from JSON
@@ -41,6 +45,10 @@ import java.util.Map;
 public class FileBackend implements LdapBackend {
     private static final LdapMessages LOG = 
MessagesFactory.get(LdapMessages.class);
 
+    private static final Pattern UID_PATTERN = 
Pattern.compile(".*\\(uid=([^)]+)\\).*");
+    private static final Pattern CN_PATTERN = 
Pattern.compile(".*\\(cn=([^)]+)\\).*");
+    private static final Pattern SAMAACCOUNTNAME_PATTERN = 
Pattern.compile(".*\\(sAMAccountName=([^)]+)\\).*");
+
     static final String TYPE = "file";
 
     private Map<String, UserData> users = new HashMap<>();
@@ -136,7 +144,7 @@ public class FileBackend implements LdapBackend {
     }
 
     @Override
-    public List<String> getUserGroups(String username) throws Exception {
+    public List<String> getUserGroups(String username, SchemaManager 
schemaManager) throws Exception {
         UserData userData = users.get(username);
         return userData != null && userData.groups != null ? userData.groups : 
Collections.emptyList();
     }
@@ -145,9 +153,13 @@ public class FileBackend implements LdapBackend {
     public List<Entry> searchUsers(String filter, SchemaManager schemaManager) 
throws Exception {
         List<Entry> results = new ArrayList<>();
 
+        String userFilter = extractUser(filter).toLowerCase(Locale.ROOT);
+
         // Simple filter matching - just check if username matches
         for (String username : users.keySet()) {
-            if (filter.contains("uid=" + username) || filter.contains("*")) {
+            String usernameLowerCase = username.toLowerCase(Locale.ROOT);
+            if (userFilter.equalsIgnoreCase(usernameLowerCase) ||
+                    (userFilter.contains("*") && 
userFilter.contains(usernameLowerCase))) {
                 Entry entry = getUser(username, schemaManager);
                 if (entry != null) {
                     results.add(entry);
@@ -170,4 +182,28 @@ public class FileBackend implements LdapBackend {
 
         return false;
     }
+
+    @Override
+    public List<Entry> search(String searchBase, SearchScope searchScope, 
String filter, SchemaManager schemaManager) throws Exception {
+        return searchUsers(filter, schemaManager);
+    }
+
+    private String extractUser(String filter) {
+        Matcher uidMatcher = UID_PATTERN.matcher(filter);
+        if (uidMatcher.matches()) {
+            return uidMatcher.group(1);
+        }
+
+        Matcher cnMatcher = CN_PATTERN.matcher(filter);
+        if (cnMatcher.matches()) {
+            return cnMatcher.group(1);
+        }
+
+        Matcher samaaccountnameMatcher = 
SAMAACCOUNTNAME_PATTERN.matcher(filter);
+        if (samaaccountnameMatcher.matches()) {
+            return samaaccountnameMatcher.group(1);
+        }
+
+        return null;
+    }
 }
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FilterMappingVisitor.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FilterMappingVisitor.java
new file mode 100644
index 000000000..69d89c355
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FilterMappingVisitor.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.services.ldap.backend;
+
+import org.apache.directory.api.ldap.model.entry.Value;
+import org.apache.directory.api.ldap.model.filter.BranchNode;
+import org.apache.directory.api.ldap.model.filter.ExprNode;
+import org.apache.directory.api.ldap.model.filter.FilterVisitor;
+import org.apache.directory.api.ldap.model.filter.LeafNode;
+import org.apache.directory.api.ldap.model.filter.SimpleNode;
+import org.apache.directory.api.ldap.model.schema.SchemaManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * FilterVisitor that maps LDAP search filters from the proxy attributes to
+ * remote attributes.
+ */
+public class FilterMappingVisitor implements FilterVisitor {
+
+    private final String userIdentifierAttribute;
+    private final String userObjectClass;
+    private final String groupObjectClass;
+    private final SchemaManager schemaManager;
+
+    public FilterMappingVisitor(String userIdentifierAttribute, String 
userObjectClass, String groupObjectClass, SchemaManager schemaManager) {
+        this.userIdentifierAttribute = userIdentifierAttribute;
+        this.userObjectClass = userObjectClass;
+        this.groupObjectClass = groupObjectClass;
+        this.schemaManager = schemaManager;
+    }
+
+    @Override
+    public Object visit(ExprNode exprNode) {
+        if (exprNode == null) {
+            return null;
+        }
+
+        if (exprNode.isLeaf()) {
+            return handleLeafNode((LeafNode) exprNode);
+        } else {
+            return handleBranchNode((BranchNode) exprNode);
+        }
+    }
+
+    private Object handleLeafNode(LeafNode leafNode) {
+        String currentAttribute = leafNode.getAttribute();
+
+        // Map the uid attribute search to the configured user identifier 
(e.g., sAMAccountName)
+        if ("uid".equalsIgnoreCase(currentAttribute) && 
!"uid".equalsIgnoreCase(userIdentifierAttribute)) {
+            leafNode.setAttribute(userIdentifierAttribute);
+            
leafNode.setAttributeType(schemaManager.getAttributeType(userIdentifierAttribute));
+        }
+
+        // Map group or user object class values to the configured values
+        if ("objectClass".equalsIgnoreCase(currentAttribute)) {
+            if (leafNode instanceof SimpleNode) {
+                SimpleNode valueNode = (SimpleNode) leafNode;
+                Value currentValue = valueNode.getValue();
+                if (currentValue != null) {
+                    if 
("groupofnames".equalsIgnoreCase(currentValue.getString())) {
+                        valueNode.setValue(new Value(groupObjectClass));
+                    }
+                    if 
("inetOrgPerson".equalsIgnoreCase(currentValue.getString())) {
+                        valueNode.setValue(new Value(userObjectClass));
+                    }
+                }
+            }
+        }
+
+        return leafNode;
+    }
+
+    private Object handleBranchNode(BranchNode branchNode) {
+        // recursively call visit on all the children
+        List<ExprNode> newChildren = new ArrayList<>();
+        for (ExprNode child : branchNode.getChildren()) {
+            Object newChild = child.accept(this);
+            if (newChild instanceof ExprNode) {
+                newChildren.add((ExprNode) newChild);
+            }
+        }
+        branchNode.setChildren(newChildren);
+        return branchNode;
+    }
+
+    @Override
+    public boolean canVisit(ExprNode exprNode) {
+        return true;
+    }
+
+    @Override
+    public boolean isPrefix() {
+        return true;
+    }
+
+    @Override
+    public List<ExprNode> getOrder(BranchNode branchNode, List<ExprNode> list) 
{
+        return list;
+    }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackend.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackend.java
index f5cc9c75a..51ef8e4f8 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackend.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackend.java
@@ -18,6 +18,7 @@
 package org.apache.knox.gateway.services.ldap.backend;
 
 import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.message.SearchScope;
 import org.apache.directory.api.ldap.model.name.Dn;
 import org.apache.directory.api.ldap.model.schema.SchemaManager;
 
@@ -59,9 +60,10 @@ public interface LdapBackend {
     /**
      * Get groups for a user
      * @param username The username
-     * @return List of group names
+     * @param schemaManager Schema manager for creating entries
+     * @return List of group names or null if not found
      */
-    List<String> getUserGroups(String username) throws Exception;
+    List<String> getUserGroups(String username, SchemaManager schemaManager) 
throws Exception;
 
     /**
      * Search for users matching a filter
@@ -71,6 +73,16 @@ public interface LdapBackend {
      */
     List<Entry> searchUsers(String filter, SchemaManager schemaManager) throws 
Exception;
 
+    /**
+     * Search for entries matching a filter
+     * @param searchBase The base DN for the search
+     * @param searchScope The scope of the search
+     * @param filter LDAP filter string (simplified)
+     * @param schemaManager Schema manager for creating entries
+     * @return List of matching entries
+     */
+    List<Entry> search(String searchBase, SearchScope searchScope, String 
filter, SchemaManager schemaManager) throws Exception;
+
     /**
      * Authenticate a user with password
      *
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackend.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackend.java
index d92603270..2c8be212e 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackend.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackend.java
@@ -60,27 +60,31 @@ public class LdapProxyBackend implements LdapBackend {
     private String ldapUrl;
     private String bindDn;
     private String bindPassword;
-    private String userSearchBase;
-    private String groupSearchBase;
-    private String proxyBaseDn;  // Base DN for proxy entries (e.g., 
dc=proxy,dc=com)
-    private String remoteBaseDn;  // Base DN for remote server searches (e.g., 
dc=hadoop,dc=apache,dc=org)
     private int port;
     private String host;
 
+    private RemoteSchemaConverter remoteSchemaConverter;
+
+    // Proxy configuration
+    private String proxyBaseDn;  // Base DN for proxy entries (e.g., 
dc=proxy,dc=com)
+    private String proxyUserSearchBase;
+    private String proxyGroupSearchBase;
+
+    // Backend configuration
+    private String remoteBaseDn;  // Base DN for remote server searches (e.g., 
dc=hadoop,dc=apache,dc=org)
+    private String remoteUserSearchBase;
+    private String remoteGroupSearchBase;
+
     // Configurable attributes for AD/LDAP compatibility
-    private String userIdentifierAttribute = "uid"; // uid for LDAP, 
sAMAccountName for AD
+    private String remoteUserIdentifierAttribute = "uid"; // uid for LDAP, 
sAMAccountName for AD
     private String userSearchFilter = "({userIdAttr}={username})"; // Will be 
populated with userIdentifierAttribute
-    private String groupMemberAttribute = "memberUid"; // member for AD, 
memberUid for POSIX
+    private String remoteGroupMemberAttribute = "memberUid"; // member for AD, 
memberUid for POSIX
+    private String remoteUserObjectClass = "inetOrgPerson"; // user for AD, 
inetOrgPerson otherwise
+    private String remoteGroupObjectClass = "groupofnames"; // group for AD, 
groupofnames otherwise
     private boolean useMemberOf; // Use memberOf attribute for group lookup 
(efficient for AD)
     private boolean recursiveGroupResolution;
     private int recursiveGroupResolutionMaxDepth;
 
-    private List<String> proxyEntryAttributeTypes = List.of(
-            // "uid" will always be filled
-            "cn",
-            "dn",
-            "mail",
-            "description");
     private final String proxyEntryGroupMembershipAttributeType = "memberOf";
 
     // Connection pool for efficient connection reuse
@@ -88,17 +92,6 @@ public class LdapProxyBackend implements LdapBackend {
 
     public LdapProxyBackend(String name, Map<String, String> config) {
         this.name = name;
-        // Proxy base DN is for entries created in the proxy LDAP server
-        proxyBaseDn = config.get("baseDn");
-        if (proxyBaseDn == null || proxyBaseDn.isEmpty()) {
-            throw new IllegalArgumentException("baseDn is required for LDAP 
proxy backend");
-        }
-
-        // Remote base DN is for searching the remote LDAP server
-        remoteBaseDn = config.get("remoteBaseDn");
-        if (remoteBaseDn == null || remoteBaseDn.isEmpty()) {
-            throw new IllegalArgumentException("remoteBaseDn is required for 
LDAP proxy backend - this is the base DN of the remote LDAP server");
-        }
 
         // Support both url and host/port configuration
         ldapUrl = config.get("url");
@@ -129,22 +122,49 @@ public class LdapProxyBackend implements LdapBackend {
             bindPassword = config.get("systemPassword");
         }
 
-        // Search bases use the remote server's base DN
-        userSearchBase = config.getOrDefault("userSearchBase", "ou=people," + 
remoteBaseDn);
-        groupSearchBase = config.getOrDefault("groupSearchBase", "ou=groups," 
+ remoteBaseDn);
+        // Proxy base DN is for entries created in the proxy LDAP server
+        proxyBaseDn = config.get("baseDn");
+        if (proxyBaseDn == null || proxyBaseDn.isEmpty()) {
+            throw new IllegalArgumentException("baseDn is required for LDAP 
proxy backend");
+        }
+        // Search bases for the proxy server
+        proxyUserSearchBase = "ou=people," + proxyBaseDn;
+        proxyGroupSearchBase = "ou=groups," + proxyBaseDn;
 
+        // Remote base DN is for searching the remote LDAP server
+        remoteBaseDn = config.get("remoteBaseDn");
+        if (remoteBaseDn == null || remoteBaseDn.isEmpty()) {
+            throw new IllegalArgumentException("remoteBaseDn is required for 
LDAP proxy backend - this is the base DN of the remote LDAP server");
+        }
+        // Search bases for the remote server
+        remoteUserSearchBase = config.getOrDefault("userSearchBase", 
"ou=people," + remoteBaseDn);
+        remoteGroupSearchBase = config.getOrDefault("groupSearchBase", 
"ou=groups," + remoteBaseDn);
         // Configure attribute mappings for AD/LDAP compatibility
-        userIdentifierAttribute = 
config.getOrDefault("userIdentifierAttribute", "uid");
-        groupMemberAttribute = config.getOrDefault("groupMemberAttribute", 
"memberUid");
+        remoteUserIdentifierAttribute = 
config.getOrDefault("userIdentifierAttribute", "uid");
+        remoteGroupMemberAttribute = 
config.getOrDefault("groupMemberAttribute", "memberUid");
+        remoteUserObjectClass = config.getOrDefault("userObjectClass", 
"inetOrgPerson");
+        remoteGroupObjectClass = config.getOrDefault("groupObjectClass", 
"groupOfNames");
+
+        remoteSchemaConverter = new RemoteSchemaConverter(proxyBaseDn,
+                proxyUserSearchBase,
+                proxyGroupSearchBase,
+                remoteBaseDn,
+                remoteUserSearchBase,
+                remoteGroupSearchBase,
+                remoteUserIdentifierAttribute,
+                remoteUserObjectClass,
+                remoteGroupObjectClass);
+
+        // Configure group lookup
         useMemberOf = Boolean.parseBoolean(config.getOrDefault("useMemberOf", 
"false"));
         recursiveGroupResolution = 
Boolean.parseBoolean(config.getOrDefault("recursiveGroupResolution", "false"));
         recursiveGroupResolutionMaxDepth = 
Integer.parseInt(config.getOrDefault("recursiveGroupResolutionMaxDepth", "3"));
 
         // Build search filter template
-        userSearchFilter = "(" + userIdentifierAttribute + "={username})";
+        userSearchFilter = "(" + remoteUserIdentifierAttribute + 
"={username})";
 
         LOG.ldapBackendLoading(getName(), "Proxying " + proxyBaseDn + " to " + 
ldapUrl + " (" + remoteBaseDn + ") with " +
-                              userIdentifierAttribute + " attribute" +
+                remoteUserIdentifierAttribute + " attribute" +
                               (useMemberOf ? " using memberOf lookups" : " 
using group searches") +
                               (recursiveGroupResolution ? " with recursive 
group resolution (max depth: " + recursiveGroupResolutionMaxDepth + ")" : ""));
 
@@ -279,11 +299,15 @@ public class LdapProxyBackend implements LdapBackend {
     @Override
     public boolean authenticate(Dn userDn, String password) {
         final String userDnText = userDn.toString(); //at this point we are 
sure it's not NULL
+
+        // if userDN is using proxy base DN then convert to remote base dn
+        final String remoteUserDnText = 
remoteSchemaConverter.convertProxyDnToRemoteDn(userDnText);
+
         // Create a temporary connection for authentication (bind)
         final LdapConnectionConfig authConfig = new LdapConnectionConfig();
         authConfig.setLdapHost(host);
         authConfig.setLdapPort(port);
-        authConfig.setName(userDnText);
+        authConfig.setName(remoteUserDnText);
         authConfig.setCredentials(password);
         try (LdapConnection connection =  new 
LdapNetworkConnection(authConfig)){
             connection.bind();
@@ -302,10 +326,12 @@ public class LdapProxyBackend implements LdapBackend {
             connection = getConnection();
             // Search for user using configurable attribute
             String filter = userSearchFilter.replace("{username}", username);
-            try (EntryCursor cursor = connection.search(userSearchBase, 
filter, SearchScope.SUBTREE, "*")) {
+            try (EntryCursor cursor = connection.search(remoteUserSearchBase, 
filter, SearchScope.SUBTREE, "*")) {
                 if (cursor.next()) {
                     Entry sourceEntry = cursor.get();
-                    return createProxyEntry(sourceEntry, username, connection, 
schemaManager, createResolvedParentsCache());
+                    addGroupMemberships(sourceEntry, connection, 
createEntryCache(), createResolvedParentsCache());
+                    return 
remoteSchemaConverter.convertRemoteEntryToProxyEntry(sourceEntry, 
schemaManager);
+
                 }
             }
             return null;
@@ -315,72 +341,182 @@ public class LdapProxyBackend implements LdapBackend {
     }
 
     @Override
-    public List<String> getUserGroups(String username) throws Exception {
+    public List<String> getUserGroups(String username, SchemaManager 
schemaManager) throws Exception {
+        Entry user = getUser(username, schemaManager);
+        if (user == null) {
+            return List.of();
+        }
+
+        LdapConnection connection = null;
+        try {
+            connection = getConnection();
+            List<Entry> groups = getUserGroupsEntries(connection, user, 
createEntryCache(), createResolvedParentsCache());
+            List<String> cns = getCnsFromEntries(groups);
+            return cns;
+        } finally {
+            releaseConnection(connection);
+        }
+    }
+
+    @Override
+    public List<Entry> searchUsers(String filter, SchemaManager schemaManager) 
throws Exception {
+        List<Entry> results = new ArrayList<>();
+        LdapConnection connection = null;
+        Map<String, Entry> entryCache = createEntryCache();
+        Map<String, Set<String>> resolvedParentsCache = 
createResolvedParentsCache();
+
+        try {
+            connection = getConnection();
+            String ldapFilter = "(" + remoteUserIdentifierAttribute + "=" + 
filter.trim() + ")";
+            try (EntryCursor cursor = connection.search(remoteUserSearchBase, 
ldapFilter, SearchScope.SUBTREE, "*")) {
+                while (cursor.next()) {
+                    Entry sourceEntry = cursor.get();
+                    addGroupMemberships(sourceEntry, connection, entryCache, 
resolvedParentsCache);
+                    
results.add(remoteSchemaConverter.convertRemoteEntryToProxyEntry(sourceEntry, 
schemaManager));
+                }
+            }
+            return results;
+        } finally {
+            releaseConnection(connection);
+        }
+    }
+
+    @Override
+    public List<Entry> search(String searchBase, SearchScope searchScope, 
String filter, SchemaManager schemaManager) throws Exception {
         LdapConnection connection = null;
+        String remoteSearchBase = 
remoteSchemaConverter.convertProxyDnToRemoteDn(searchBase);
+        String remoteFilter = 
remoteSchemaConverter.convertProxyFilterToRemoteFilter(filter, schemaManager);
+        Map<String, Entry> entryCache = createEntryCache();
+        Map<String, Set<String>> resolvedParentsCache = 
createResolvedParentsCache();
+
         try {
             connection = getConnection();
-            List<Entry> groups = getUserGroupsEntries(connection, username, 
createResolvedParentsCache());
-            return getCnsFromEntries(groups);
+            List<Entry> results = new ArrayList<>();
+            try (EntryCursor cursor = connection.search(remoteSearchBase, 
remoteFilter, searchScope, "*")) {
+                while (cursor.next()) {
+                    Entry entry = cursor.get();
+                    addGroupMemberships(entry, connection, entryCache, 
resolvedParentsCache);
+                    
results.add(remoteSchemaConverter.convertRemoteEntryToProxyEntry(entry, 
schemaManager));
+                }
+            } catch (LdapException e) {
+                LOG.ldapAttributeCopyError(e);
+            }
+            return results;
         } finally {
             releaseConnection(connection);
         }
     }
 
-    private List<Entry> getUserGroupsEntries(LdapConnection connection, String 
username, Map<String, Set<Entry>> resolvedParentsCache) throws Exception {
+    private void addGroupMemberships(Entry entry, LdapConnection connection, 
Map<String, Entry> entryCache, Map<String, Set<String>> resolvedParentsCache) 
throws Exception {
+        // The memberOf attribute is already populated on the entry. Further 
work is only needed
+        // when using recursive group resolution or non using memberOf to find 
groups
+        if (recursiveGroupResolution || !useMemberOf) {
+            List<Entry> groups = getUserGroupsEntries(connection, entry, 
entryCache, resolvedParentsCache);
+            for (Entry groupEntry : groups) {
+                entry.add(proxyEntryGroupMembershipAttributeType, 
groupEntry.getDn().getName());
+            }
+        }
+    }
+
+    private List<Entry> getUserGroupsEntries(LdapConnection connection, Entry 
user, Map<String, Entry> entryCache, Map<String, Set<String>> 
resolvedParentsCache) throws Exception {
         List<Entry> groups = new ArrayList<>();
         if (useMemberOf) {
             // Use memberOf attribute for efficient AD lookups
-            List<String> groupDns = 
getUserGroupsViaMemberOfInternal(connection, username);
+            List<String> groupDns = getGroupsViaMemberOf(connection, user, 
entryCache, resolvedParentsCache);
             for (String groupDn : groupDns) {
-                if (recursiveGroupResolution) {
-                    // We only need the Entry if we are doing recursive group 
resolution
-                    try {
-                        Entry groupEntry = connection.lookup(groupDn, "cn");
-                        if (groupEntry != null) {
-                            groups.add(groupEntry);
-                        } else {
-                            groups.add(createSkeletonGroupEntry(groupDn));
-                        }
-                    } catch (LdapException e) {
-                        groups.add(createSkeletonGroupEntry(groupDn));
-                    }
+                // every dn returned by getUserGroupsViaMemberOf must have 
been seen and saved in the entryCache
+                Entry group = entryCache.get(groupDn);
+                if (group != null) {
+                    groups.add(group);
                 } else {
-                    // Optimized path: just use skeleton entry to extract CN 
from DN
-                    groups.add(createSkeletonGroupEntry(groupDn));
+                    
LOG.ldapRecursiveGroupSearchExpectedGroupNotInCache(groupDn);
                 }
             }
         } else {
             // Use traditional group search approach
-            String filter = userSearchFilter.replace("{username}", username);
-            try (EntryCursor cursor = connection.search(userSearchBase, 
filter, SearchScope.SUBTREE, "dn")) {
-                if (cursor.next()) {
-                    String userDn = cursor.get().getDn().toString();
-                    groups = getUserGroupsInternal(connection, userDn, 
username);
-                }
+            Dn userDn = user.getDn();
+            groups = getUserGroupsInternal(connection, userDn);
+            if (recursiveGroupResolution && !groups.isEmpty()) {
+                groups = resolveGroupsRecursive(connection, groups, 
user.getDn().getName(), entryCache, resolvedParentsCache);
             }
         }
 
-        if (recursiveGroupResolution && !groups.isEmpty()) {
-            groups = resolveGroupsRecursive(connection, groups, username, 
resolvedParentsCache);
-        }
         return groups;
     }
 
-    private List<String> getUserGroupsViaMemberOfInternal(LdapConnection 
connection, String username) throws LdapException, CursorException, IOException 
{
+    private List<String> getGroupsViaMemberOf(LdapConnection connection, Entry 
entry, Map<String, Entry> entryCache, Map<String, Set<String>> 
resolvedParentsCache) throws LdapException, CursorException, IOException {
         final List<String> groupDns = new ArrayList<>();
-        // Search for user and retrieve memberOf attribute
-        final String filter = userSearchFilter.replace("{username}", username);
-        try (EntryCursor cursor = connection.search(userSearchBase, filter, 
SearchScope.SUBTREE, "memberOf")) {
-            if (cursor.next()) {
-                final Attribute memberOfAttr = cursor.get().get("memberOf");
-                if (memberOfAttr != null) {
-                    for (Value value : memberOfAttr) {
-                        groupDns.add(value.getString());
+        // Use the memberOf attribute of the given entry
+        final Attribute memberOfAttr = entry.get("memberOf");
+        if (memberOfAttr != null) {
+            for (Value value : memberOfAttr) {
+                groupDns.add(value.getString());
+            }
+        }
+        if (recursiveGroupResolution) {
+            groupDns.addAll(getGroupsViaMemberOfRecursive(connection, 
entry.getDn().getName(), groupDns, entryCache, resolvedParentsCache));
+        } else {
+            // populate cache with skeletons
+            groupDns.forEach(groupDn -> entryCache.put(groupDn, 
createSkeletonGroupEntry(groupDn)));
+        }
+        return groupDns;
+    }
+
+    private Set<String> getGroupsViaMemberOfRecursive(LdapConnection 
connection, String entryName, List<String> initialGroupDns, Map<String, Entry> 
entryCache, Map<String, Set<String>> resolvedParentsCache) {
+        Set<String> accumulatedGroupDns = new HashSet<>();
+        List<String> currentLevelGroupsDns = initialGroupDns;
+        int depth = 0;
+        while (!currentLevelGroupsDns.isEmpty() && depth < 
recursiveGroupResolutionMaxDepth) {
+            logRecursiveSearchProgressViaMemberOf(entryName, 
currentLevelGroupsDns, depth);
+            List<String> nextLevelGroupDns = new ArrayList<>();
+            for (String groupDn : currentLevelGroupsDns) {
+                Set<String> parents;
+                if (resolvedParentsCache.containsKey(groupDn)) {
+                    parents = resolvedParentsCache.get(groupDn);
+                } else {
+                    parents = populateParentCacheViaMemberOf(connection, 
groupDn, entryCache, resolvedParentsCache);
+                }
+                for (String parentDn : parents) {
+                    if (!accumulatedGroupDns.contains(parentDn)) {
+                        accumulatedGroupDns.add(parentDn);
+                        nextLevelGroupDns.add(parentDn);
+                    } else {
+                        LOG.ldapRecursiveGroupSearchCycleDetected(entryName, 
parentDn);
                     }
                 }
             }
+            currentLevelGroupsDns = nextLevelGroupDns;
+            depth++;
+
+            if (depth == recursiveGroupResolutionMaxDepth && 
!currentLevelGroupsDns.isEmpty()) {
+                LOG.ldapRecursiveGroupSearchMaxDepthReached(entryName, 
recursiveGroupResolutionMaxDepth);
+            }
         }
-        return groupDns;
+        return accumulatedGroupDns;
+    }
+
+    private Set<String> populateParentCacheViaMemberOf(LdapConnection 
connection, String groupDn, Map<String, Entry> entryCache, Map<String, 
Set<String>> resolvedParentsCache) {
+        Set<String> parents = new HashSet<>();
+        Entry groupEntry;
+        if (entryCache.containsKey(groupDn)) {
+            groupEntry = entryCache.get((groupDn));
+        } else {
+            try {
+                groupEntry = connection.lookup(groupDn, "memberOf");
+            } catch (LdapException e) {
+                // assume group doesn't exist and has no parent groups
+                groupEntry = createSkeletonGroupEntry(groupDn);
+            }
+            entryCache.put(groupDn, groupEntry);
+        }
+        Attribute memberOf = groupEntry.get("memberOf");
+        if (memberOf != null) {
+            for (Value value : memberOf) {
+                parents.add(value.getNormalized());
+            }
+        }
+        resolvedParentsCache.put(groupDn, parents);
+        return parents;
     }
 
     /**
@@ -407,8 +543,8 @@ public class LdapProxyBackend implements LdapBackend {
         }
     }
 
-    private List<Entry> resolveGroupsRecursive(LdapConnection connection, 
List<Entry> initialGroups, String username,
-                                               Map<String, Set<Entry>> 
resolvedParentsCache) throws LdapException, CursorException, IOException {
+    private List<Entry> resolveGroupsRecursive(LdapConnection connection, 
List<Entry> initialGroups, String entryName,
+                                               Map<String, Entry> entryCache, 
Map<String, Set<String>> resolvedParentsCache) throws LdapException, 
CursorException, IOException {
         LOG.ldapRecursiveGroupSearchConfig(recursiveGroupResolution, 
recursiveGroupResolutionMaxDepth);
 
         Set<String> allGroupDns = new HashSet<>();
@@ -420,7 +556,7 @@ public class LdapProxyBackend implements LdapBackend {
             allGroups.add(group);
         }
 
-        logRecursiveSearchProgress(username, initialGroups, 0);
+        logRecursiveSearchProgress(entryName, initialGroups, 0);
 
         int depth = 1;
         while (!currentLevelGroups.isEmpty() && depth < 
recursiveGroupResolutionMaxDepth) {
@@ -428,30 +564,29 @@ public class LdapProxyBackend implements LdapBackend {
             List<Entry> groupsToSearch = new ArrayList<>();
 
             // Check cache first
-            populateFromCache(resolvedParentsCache, currentLevelGroups, 
allGroupDns, allGroups, nextLevelGroups, groupsToSearch);
+            populateFromCache(entryCache, resolvedParentsCache, 
currentLevelGroups, allGroupDns, allGroups, nextLevelGroups, groupsToSearch);
 
             if (!groupsToSearch.isEmpty()) {
-                StringBuilder filterBuilder = new StringBuilder("(|");
+                List<Dn> groupDns = new ArrayList<>();
                 for (Entry group : groupsToSearch) {
-                    String dn = group.getDn().getName();
-                    
filterBuilder.append("(member=").append(dn).append(")(uniqueMember=").append(dn).append(")");
+                    groupDns.add(group.getDn());
                 }
-                filterBuilder.append(")");
+                String filter = 
buildMultipleGroupMemberFilter(groupDns.toArray(new Dn[0]));
 
-                try (EntryCursor cursor = connection.search(groupSearchBase, 
filterBuilder.toString(), SearchScope.SUBTREE, "cn", "member", "uniqueMember")) 
{
+                try (EntryCursor cursor = 
connection.search(remoteGroupSearchBase, filter, SearchScope.SUBTREE, "cn", 
"memberUid", "member", "uniqueMember")) {
                     while (cursor.next()) {
                         Entry parentGroup = cursor.get();
                         String parentDn = parentGroup.getDn().getNormName();
 
                         // Update cache for all groups found in this search
-                        updateCache(resolvedParentsCache, groupsToSearch, 
parentGroup);
+                        updateCache(entryCache, resolvedParentsCache, 
groupsToSearch, parentGroup);
 
                         if (!allGroupDns.contains(parentDn)) {
                             allGroupDns.add(parentDn);
                             allGroups.add(parentGroup);
                             nextLevelGroups.add(parentGroup);
                         } else {
-                            
LOG.ldapRecursiveGroupSearchCycleDetected(username, parentDn);
+                            
LOG.ldapRecursiveGroupSearchCycleDetected(entryName, parentDn);
                         }
                     }
                 }
@@ -462,26 +597,26 @@ public class LdapProxyBackend implements LdapBackend {
                 }
             }
 
-            logRecursiveSearchProgress(username, nextLevelGroups, depth);
+            logRecursiveSearchProgress(entryName, nextLevelGroups, depth);
             currentLevelGroups = nextLevelGroups;
             depth++;
 
             if (depth == recursiveGroupResolutionMaxDepth && 
!currentLevelGroups.isEmpty()) {
-                LOG.ldapRecursiveGroupSearchMaxDepthReached(username, 
recursiveGroupResolutionMaxDepth);
+                LOG.ldapRecursiveGroupSearchMaxDepthReached(entryName, 
recursiveGroupResolutionMaxDepth);
             }
         }
 
-        LOG.ldapRecursiveGroupSearchFinished(username, allGroups.size());
+        LOG.ldapRecursiveGroupSearchFinished(entryName, allGroups.size());
         return allGroups;
     }
 
-    private void populateFromCache(Map<String, Set<Entry>> 
resolvedParentsCache, List<Entry> currentLevelGroups, Set<String> allGroupDns, 
List<Entry> allGroups, List<Entry> nextLevelGroups, List<Entry> groupsToSearch) 
{
+    private void populateFromCache(Map<String, Entry> entryCache, Map<String, 
Set<String>> resolvedParentsCache, List<Entry> currentLevelGroups, Set<String> 
allGroupDns, List<Entry> allGroups, List<Entry> nextLevelGroups, List<Entry> 
groupsToSearch) {
         for (Entry group : currentLevelGroups) {
-            Set<Entry> parents = 
resolvedParentsCache.get(group.getDn().getNormName());
+            Set<String> parents = 
resolvedParentsCache.get(group.getDn().getNormName());
             if (parents != null) {
                 LOG.ldapRecursiveGroupSearchCacheHit(group.getDn().getName(), 
parents.size());
-                parents.forEach(parent -> {
-                    String parentDn = parent.getDn().getNormName();
+                parents.forEach(parentDn -> {
+                    Entry parent = entryCache.get(parentDn);
                     if (!allGroupDns.contains(parentDn)) {
                         allGroupDns.add(parentDn);
                         allGroups.add(parent);
@@ -494,11 +629,13 @@ public class LdapProxyBackend implements LdapBackend {
         }
     }
 
-    private void updateCache(Map<String, Set<Entry>> resolvedParentsCache, 
List<Entry> groupsToSearch, Entry parentGroup) {
+    private void updateCache(Map<String, Entry> entryCache, Map<String, 
Set<String>> resolvedParentsCache, List<Entry> groupsToSearch, Entry 
parentGroup) {
+        String parentDn = parentGroup.getDn().getNormName();
+        entryCache.putIfAbsent(parentDn, parentGroup);
         for (Entry child : groupsToSearch) {
             if (isMember(parentGroup, child.getDn())) {
                 LOG.ldapRecursiveGroupSearchCacheAdd(child.getDn().getName(), 
parentGroup.getDn().getName());
-                
resolvedParentsCache.computeIfAbsent(child.getDn().getNormName(), k -> new 
HashSet<>()).add(parentGroup);
+                
resolvedParentsCache.computeIfAbsent(child.getDn().getNormName(), k -> new 
HashSet<>()).add(parentDn);
             }
         }
     }
@@ -523,8 +660,14 @@ public class LdapProxyBackend implements LdapBackend {
         return false;
     }
 
-    private void logRecursiveSearchProgress(String username, List<Entry> 
groups, int depth) {
-        LOG.ldapRecursiveGroupSearchProgress(username, groups.size(),
+    private void logRecursiveSearchProgressViaMemberOf(String entryName, 
List<String> groups, int depth) {
+        LOG.ldapRecursiveGroupSearchProgress(entryName, groups.size(),
+                groups.stream().collect(Collectors.joining(",")),
+                depth);
+    }
+
+    private void logRecursiveSearchProgress(String entryName, List<Entry> 
groups, int depth) {
+        LOG.ldapRecursiveGroupSearchProgress(entryName, groups.size(),
                 groups.stream().map(e -> 
e.getDn().getRdn().getValue()).collect(Collectors.joining(",")),
                 depth);
     }
@@ -540,27 +683,16 @@ public class LdapProxyBackend implements LdapBackend {
         return null;
     }
 
-    private List<Entry> getUserGroupsInternal(LdapConnection connection, 
String userDn, String username) throws LdapException, CursorException, 
IOException {
+    // tODO reuse this method in recursive calls
+    private List<Entry> getUserGroupsInternal(LdapConnection connection, Dn... 
dns) throws LdapException, CursorException, IOException {
         List<Entry> groups = new ArrayList<>();
-
-        // Search for groups where user is a member - build filter based on 
configuration
-        String filter;
-        if ("member".equals(groupMemberAttribute)) {
-            // AD style - uses full DN
-            filter = "(|" +
-                    "(member=" + userDn + ")" +
-                    "(uniqueMember=" + userDn + ")" +
-                    ")";
-        } else {
-            // POSIX style - uses username
-            filter = "(|" +
-                    "(memberUid=" + username + ")" +
-                    "(member=" + userDn + ")" +
-                    "(uniqueMember=" + userDn + ")" +
-                    ")";
+        if (dns.length == 0) {
+            return groups;
         }
 
-        try (EntryCursor cursor = connection.search(groupSearchBase, filter, 
SearchScope.SUBTREE, "cn")) {
+        String filter = buildMultipleGroupMemberFilter(dns);
+
+        try (EntryCursor cursor = connection.search(remoteGroupSearchBase, 
filter, SearchScope.SUBTREE, "cn")) {
             while (cursor.next()) {
                 groups.add(cursor.get());
             }
@@ -569,6 +701,35 @@ public class LdapProxyBackend implements LdapBackend {
         return groups;
     }
 
+    private String buildMultipleGroupMemberFilter(Dn... dns) {
+        StringBuilder filterBuilder = new StringBuilder();
+        filterBuilder.append("(|");
+        for (Dn dn : dns) {
+            String dnString = dn.toString();
+            String rdnString = dn.getRdn().getValue();
+            // Search for groups where user is a member - build filter based 
on configuration
+            if ("member".equals(remoteGroupMemberAttribute)) {
+                // AD style - uses full DN
+                filterBuilder.append("(member=");
+                filterBuilder.append(dnString);
+                filterBuilder.append(")(uniqueMember=");
+                filterBuilder.append(dnString);
+                filterBuilder.append(")");
+            } else {
+                // POSIX style - uses username
+                filterBuilder.append("(memberUid=");
+                filterBuilder.append(rdnString);
+                filterBuilder.append(")(member=");
+                filterBuilder.append(dnString);
+                filterBuilder.append(")(uniqueMember=");
+                filterBuilder.append(dnString);
+                filterBuilder.append(")");
+            }
+        }
+        filterBuilder.append(")");
+        return filterBuilder.toString();
+    }
+
     private List<String> getCnsFromEntries(Collection<Entry> entries) throws 
LdapException {
         List<String> cns = new ArrayList<>();
         for (Entry entry : entries) {
@@ -580,99 +741,15 @@ public class LdapProxyBackend implements LdapBackend {
         return cns;
     }
 
-    @Override
-    public List<Entry> searchUsers(String filter, SchemaManager schemaManager) 
throws Exception {
-        List<Entry> results = new ArrayList<>();
-        LdapConnection connection = null;
-        Map<String, Set<Entry>> resolvedParentsCache = 
createResolvedParentsCache();
-
-        try {
-            connection = getConnection();
-            String ldapFilter = "(" + userIdentifierAttribute + "=" + 
filter.trim() + ")";
-            try (EntryCursor cursor = connection.search(userSearchBase, 
ldapFilter, SearchScope.SUBTREE, "*")) {
-                while (cursor.next()) {
-                    Entry sourceEntry = cursor.get();
-                    Attribute idAttr = 
sourceEntry.get(userIdentifierAttribute);
-                    if (idAttr != null) {
-                        String username = idAttr.getString();
-                        Entry entry = createProxyEntry(sourceEntry, username, 
connection, schemaManager, resolvedParentsCache);
-                        results.add(entry);
-                    }
-                }
-            }
-            return results;
-        } finally {
-            releaseConnection(connection);
-        }
+    protected Map<String, Entry> createEntryCache() {
+        return new HashMap<>();
     }
 
     /**
      * Factory method for the resolved parents cache.
      * Overridden in tests to verify caching behavior.
      */
-    protected Map<String, Set<Entry>> createResolvedParentsCache() {
+    protected Map<String, Set<String>> createResolvedParentsCache() {
         return new HashMap<>();
     }
-
-    /**
-     * Creates a proxy entry from a backend source entry with all required 
attributes.
-     * This method standardizes the conversion of backend LDAP entries to 
proxy entries,
-     * preserving the backend DN and copying all standard user attributes.
-     *
-     * @param sourceEntry The entry from the backend LDAP server
-     * @param username The username for the entry
-     * @param connection The LDAP connection for fetching group information
-     * @param schemaManager The schema manager for creating entries
-     * @param resolvedParentsCache Cache for direct parent groups to avoid 
redundant searches
-     * @return A new Entry with backend DN and all copied attributes
-     * @throws Exception if entry creation or attribute copying fails
-     */
-    private Entry createProxyEntry(Entry sourceEntry, String username, 
LdapConnection connection, SchemaManager schemaManager, Map<String, Set<Entry>> 
resolvedParentsCache) throws Exception {
-        // Standard proxy approach: return entry with backend DN unchanged
-        // This preserves DN integrity for bind operations and DN references
-        Entry entry = new DefaultEntry(schemaManager);
-        entry.setDn(sourceEntry.getDn());
-
-        // Copy all attributes as-is from backend
-        copyAttribute(sourceEntry, entry, "objectClass");
-        copyAttribute(sourceEntry, entry, userIdentifierAttribute);
-
-        // Map identifier attribute to uid for consistency if needed
-        if (!"uid".equals(userIdentifierAttribute)) {
-            Attribute idAttr = sourceEntry.get(userIdentifierAttribute);
-            if (idAttr != null) {
-                entry.add("uid", idAttr.getString());
-            }
-        }
-
-        for (String attributeType : proxyEntryAttributeTypes) {
-            copyAttribute(sourceEntry, entry, attributeType);
-        }
-
-        if (useMemberOf && !recursiveGroupResolution) {
-            copyAttribute(sourceEntry, entry, 
proxyEntryGroupMembershipAttributeType);
-        } else {
-            List<Entry> groups = getUserGroupsEntries(connection, username, 
resolvedParentsCache);
-            for (Entry groupEntry : groups) {
-                entry.add(proxyEntryGroupMembershipAttributeType, 
groupEntry.getDn().getName());
-            }
-        }
-
-        return entry;
-    }
-
-    private void copyAttribute(Entry source, Entry target, String 
attributeName) throws LdapException {
-        final Attribute attribute = source.get(attributeName);
-        if (attribute != null) {
-            // Copy all values of the attribute (important for multi-valued 
attributes like objectClass)
-            for (Value value : attribute) {
-                try {
-                    target.add(attributeName, value.getString());
-                } catch (LdapException e) {
-                    LOG.ldapAttributeCopyError(e);
-                    throw e;
-                }
-            }
-        }
-    }
 }
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/RemoteSchemaConverter.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/RemoteSchemaConverter.java
new file mode 100644
index 000000000..bd55f58ba
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/RemoteSchemaConverter.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.services.ldap.backend;
+
+import org.apache.directory.api.ldap.model.entry.Attribute;
+import org.apache.directory.api.ldap.model.entry.DefaultEntry;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.entry.Value;
+import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.filter.ExprNode;
+import org.apache.directory.api.ldap.model.filter.FilterParser;
+import org.apache.directory.api.ldap.model.schema.SchemaManager;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.ldap.LdapMessages;
+
+public class RemoteSchemaConverter {
+    private static final LdapMessages LOG = 
MessagesFactory.get(LdapMessages.class);
+
+    // Proxy configuration
+    private final String proxyBaseDn;  // Base DN for proxy entries (e.g., 
dc=proxy,dc=com)
+    private final String proxyUserSearchBase;
+    private final String proxyGroupSearchBase;
+
+    // Backend configuration
+    private final String remoteBaseDn;  // Base DN for remote server searches 
(e.g., dc=hadoop,dc=apache,dc=org)
+    private final String remoteUserSearchBase;
+    private final String remoteGroupSearchBase;
+    private final String remoteUserIdentifierAttribute;
+    private final String remoteUserObjectClass;
+    private final String remoteGroupObjectClass;
+
+    public RemoteSchemaConverter(String proxyBaseDn,
+                                 String proxyUserSearchBase,
+                                 String proxyGroupSearchBase,
+                                 String remoteBaseDn,
+                                 String remoteUserSearchBase,
+                                 String remoteGroupSearchBase,
+                                 String remoteUserIdentifierAttribute,
+                                 String remoteUserObjectClass,
+                                 String remoteGroupObjectClass) {
+        this.proxyBaseDn = proxyBaseDn;
+        this.proxyUserSearchBase = proxyUserSearchBase;
+        this.proxyGroupSearchBase = proxyGroupSearchBase;
+        this.remoteBaseDn = remoteBaseDn;
+        this.remoteUserSearchBase = remoteUserSearchBase;
+        this.remoteGroupSearchBase = remoteGroupSearchBase;
+        this.remoteUserIdentifierAttribute = remoteUserIdentifierAttribute;
+        this.remoteUserObjectClass = remoteUserObjectClass;
+        this.remoteGroupObjectClass = remoteGroupObjectClass;
+    }
+
+    /**
+     * Creates a proxy entry from a backend source entry with all required 
attributes.
+     * This method standardizes the conversion of backend LDAP entries to 
proxy entries,
+     * preserving the backend DN and copying all standard user attributes.
+     *
+     * @param sourceEntry The entry from the backend LDAP server
+     * @param schemaManager The schemaManager
+     * @return A new Entry with backend DN and all copied attributes
+     * @throws LdapException if entry creation or attribute copying fails
+     */
+    public Entry convertRemoteEntryToProxyEntry(Entry sourceEntry, 
SchemaManager schemaManager) throws LdapException {
+        // Standard proxy approach: return entry with backend DN unchanged
+        // This preserves DN integrity for bind operations and DN references
+        Entry entry = new DefaultEntry(schemaManager);
+        entry.setDn(sourceEntry.getDn());
+
+        // Copy all known AttributeTypes as-is from backend response
+        for (Attribute attribute : sourceEntry.getAttributes()) {
+            copyAttribute(sourceEntry, entry, attribute.getId());
+        }
+
+        // Map identifier attribute to uid for consistency if needed
+        if (!"uid".equals(remoteUserIdentifierAttribute)) {
+            Attribute idAttr = sourceEntry.get(remoteUserIdentifierAttribute);
+            if (idAttr != null) {
+                entry.add("uid", idAttr.getString());
+            }
+        }
+
+        // replace userObjectClass and groupObjectClass object classes
+        Attribute objectClassAttribute = sourceEntry.get("objectclass");
+        if (objectClassAttribute.contains(remoteGroupObjectClass)) {
+            entry.remove("objectclass", remoteGroupObjectClass);
+            entry.add("objectclass", "groupofnames");
+        }
+        if (objectClassAttribute.contains(remoteUserObjectClass)) {
+            entry.remove("objectclass", remoteUserObjectClass);
+            entry.add("objectclass", "inetOrgPerson");
+        }
+
+        return entry;
+    }
+
+    /**
+     * Converts an LDAP search filter from proxy attributes to remote 
attributes.
+     * @param filter the filter
+     * @param schemaManager the schema manager
+     * @return the converted filter
+     * @throws Exception if the filter cannot be parsed
+     */
+    public String convertProxyFilterToRemoteFilter(String filter, 
SchemaManager schemaManager) throws Exception {
+        FilterMappingVisitor filterMappingVisitor = new 
FilterMappingVisitor(remoteUserIdentifierAttribute, remoteUserObjectClass, 
remoteGroupObjectClass, schemaManager);
+
+        // Filter likely has already been annotated by other interceptors.
+        // Clean the filter by removing any modifications or annotations
+        String rawFilter = filter.replaceAll(":\\[.*?\\]", 
"").replaceAll("=\\s+", "=");
+        ExprNode cleanNode = FilterParser.parse(schemaManager, rawFilter);
+
+        return cleanNode.accept(filterMappingVisitor).toString();
+    }
+
+    /**
+     * Copy attributes from a remote entry to a proxy entry. DN values are 
converted from
+     * the remote base to the proxy base
+     * @param source The remote entry
+     * @param target The proxy entry
+     * @param attributeName The name of the attribute to copy
+     */
+    public void copyAttribute(Entry source, Entry target, String 
attributeName) {
+        final Attribute attribute = source.get(attributeName);
+        if (attribute != null) {
+            // Copy all values of the attribute (important for multi-valued 
attributes like objectClass)
+            for (Value value : attribute) {
+                String valueString = 
convertRemoteDnToProxyDn(value.toString());
+                if (!target.contains(attributeName, valueString)) {
+                    try {
+                        target.add(attributeName, valueString);
+                    } catch (LdapException e) {
+                        LOG.ldapAttributeCopyError(e);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Converts a remote dn string to a proxy dn string. This also works for 
search base strings.
+     * @param string the remote dn or search base
+     * @return the proxy search base
+     */
+    public String convertRemoteDnToProxyDn(String string) {
+        return string == null ?
+                null :
+                string.replaceAll("(?i)" + remoteGroupSearchBase, 
proxyGroupSearchBase)
+                .replaceAll("(?i)" + remoteUserSearchBase, proxyUserSearchBase)
+                .replaceAll("(?i)" + remoteBaseDn, proxyBaseDn);
+    }
+
+    /**
+     * Converts a proxy dn string to a remote dn string. This also works for 
search base strings.
+     * @param string the proxy dn or search base
+     * @return the remote search base
+     */
+    public String convertProxyDnToRemoteDn(String string) {
+        return string == null ?
+                null :
+                string.replaceAll("(?i)" + proxyGroupSearchBase, 
remoteGroupSearchBase)
+                .replaceAll("(?i)" + proxyUserSearchBase, remoteUserSearchBase)
+                .replaceAll("(?i)" + proxyBaseDn, remoteBaseDn);
+    }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptor.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DisabledUserInterceptor.java
similarity index 52%
copy from 
gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptor.java
copy to 
gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DisabledUserInterceptor.java
index 346fe21f9..2f05ad4df 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptor.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DisabledUserInterceptor.java
@@ -22,7 +22,6 @@ import 
org.apache.directory.api.ldap.model.cursor.CursorException;
 import org.apache.directory.api.ldap.model.cursor.ListCursor;
 import org.apache.directory.api.ldap.model.entry.Attribute;
 import org.apache.directory.api.ldap.model.entry.Entry;
-import org.apache.directory.api.ldap.model.entry.Value;
 import org.apache.directory.api.ldap.model.exception.LdapException;
 import org.apache.directory.server.core.api.filtering.EntryFilteringCursor;
 import org.apache.directory.server.core.api.filtering.EntryFilteringCursorImpl;
@@ -31,14 +30,27 @@ import 
org.apache.directory.server.core.api.interceptor.context.SearchOperationC
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
+import java.util.Map;
 
-public class DuplicateUserFilteringInterceptor extends BaseInterceptor {
+public class DisabledUserInterceptor extends BaseInterceptor {
 
-    public DuplicateUserFilteringInterceptor(String name) {
+    private static final String UAC_ATTRIBUTENAME = "useraccountcontrol";
+    // Active Directory UserAccountControl Bitmasks
+    private static final int UAC_ACCOUNTDISABLE = 0x0002;
+
+    private static final String NSACCOUNTLOCK_ATTRIBUTE = "nsaccountlock";
+    private static final String NSACCOUNTLOCK_DISABLED_VALUE = "true";
+
+    private boolean removeDisabledUsers;
+
+    public DisabledUserInterceptor(String name, Map<String, String> config) {
+        this(name, 
Boolean.parseBoolean(config.getOrDefault("removeDisabledUsers", "false")));
+    }
+
+    protected DisabledUserInterceptor(String name, boolean 
removeDisabledUsers) {
         super(name);
+        this.removeDisabledUsers = removeDisabledUsers;
     }
 
     @Override
@@ -55,32 +67,58 @@ public class DuplicateUserFilteringInterceptor extends 
BaseInterceptor {
                 // rethrow exception on incomplete iteration
                 throw new LdapException(e);
             }
-            filteredEntries = filterDuplicateUsers(originalEntries);
+            filteredEntries = filterDisabledUsers(originalEntries);
         } catch (IOException e) {
             // IOException would only occur after finishing iterating over 
results
             // we can ignore this exception and return the filtered entries
+        } catch (Exception e) {
+            throw e;
         }
         return new EntryFilteringCursorImpl(new ListCursor<>(filteredEntries), 
ctx, schemaManager);
     }
 
     @VisibleForTesting
-    List<Entry> filterDuplicateUsers(List<Entry> originalEntries) {
-        Set<Value> seenUids = new HashSet<>();
+    List<Entry> filterDisabledUsers(List<Entry> originalEntries) throws 
LdapException {
         List<Entry> filteredEntries = new ArrayList<>();
-
         for (Entry entry : originalEntries) {
-            Attribute uid = entry.get("uid");
-            if (uid == null) {
-                // keep entry because it's not a user
+            if (!isAccountDisabled(entry)) {
                 filteredEntries.add(entry);
-            } else {
-                Value uidValue =  uid.get();
-                if (!seenUids.contains(uidValue)) {
-                    filteredEntries.add(entry);
-                    seenUids.add(uidValue);
+            } else if (!removeDisabledUsers) {
+                // translate disabled to use nsaccountlock flag if it isn't 
already set
+                if (!entry.contains(NSACCOUNTLOCK_ATTRIBUTE, 
NSACCOUNTLOCK_DISABLED_VALUE)) {
+                    if (entry.containsAttribute(NSACCOUNTLOCK_ATTRIBUTE)) {
+                        entry.removeAttributes(NSACCOUNTLOCK_ATTRIBUTE);
+                    }
+                    entry.add(NSACCOUNTLOCK_ATTRIBUTE, 
NSACCOUNTLOCK_DISABLED_VALUE);
                 }
-            }
+                filteredEntries.add(entry);
+            } // don't add entry to filteredEntries if removeDisabledUsers 
flag is set
         }
         return filteredEntries;
     }
+
+    private boolean isAccountDisabled(Entry entry) {
+        return isNsAccountLockDisabled(entry) || isUacAccountDisabled(entry);
+    }
+
+    private boolean isNsAccountLockDisabled(Entry entry) {
+        return entry.contains(NSACCOUNTLOCK_ATTRIBUTE, 
NSACCOUNTLOCK_DISABLED_VALUE);
+    }
+
+    private boolean isUacAccountDisabled(Entry entry) {
+        // Check userAccountControl attribute for Active Directory
+        Attribute uacAttribute = entry.get(UAC_ATTRIBUTENAME);
+        if (uacAttribute != null) {
+            String uacString = uacAttribute.get().getString();
+            try {
+                int uacMask = Integer.parseInt(uacString);
+                return (uacMask & UAC_ACCOUNTDISABLE) == UAC_ACCOUNTDISABLE;
+            } catch (NumberFormatException e) {
+                // Assume account is active if value is unparseable
+                return false;
+            }
+        }
+        // account is not disabled using the UAC attribute
+        return false;
+    }
 }
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DisabledUserInterceptorFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DisabledUserInterceptorFactory.java
new file mode 100644
index 000000000..f26fbc20b
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DisabledUserInterceptorFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.services.ldap.interceptor;
+
+import org.apache.directory.server.core.api.interceptor.Interceptor;
+import org.apache.knox.gateway.config.GatewayConfig;
+
+import java.util.Map;
+
+public class DisabledUserInterceptorFactory implements 
KnoxLdapInterceptorFactory {
+    public static final String TYPE = "disableduserfilter";
+
+    @Override
+    public Interceptor create(GatewayConfig gatewayConfig, String name, 
Map<String, String> config) {
+        return new DisabledUserInterceptor(name, config);
+    }
+
+    @Override
+    public String getType() {
+        return TYPE;
+    }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptor.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptor.java
index 346fe21f9..f434652b5 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptor.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptor.java
@@ -36,6 +36,7 @@ import java.util.List;
 import java.util.Set;
 
 public class DuplicateUserFilteringInterceptor extends BaseInterceptor {
+    private static final String UID_ATTRIBUTE = "uid";
 
     public DuplicateUserFilteringInterceptor(String name) {
         super(name);
@@ -69,7 +70,7 @@ public class DuplicateUserFilteringInterceptor extends 
BaseInterceptor {
         List<Entry> filteredEntries = new ArrayList<>();
 
         for (Entry entry : originalEntries) {
-            Attribute uid = entry.get("uid");
+            Attribute uid = entry.get(UID_ATTRIBUTE);
             if (uid == null) {
                 // keep entry because it's not a user
                 filteredEntries.add(entry);
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java
index 03743d959..014cd2bb8 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java
@@ -38,8 +38,6 @@ import 
org.apache.knox.gateway.services.ldap.backend.LdapBackend;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 /**
  * Interceptor for LDAP operations to proxy user searches to backends when not 
found locally
@@ -47,15 +45,16 @@ import java.util.regex.Pattern;
 public class UserSearchInterceptor extends BaseInterceptor {
 
     private static final LdapMessages LOG = 
MessagesFactory.get(LdapMessages.class);
-    private static final Pattern UID_PATTERN = 
Pattern.compile(".*\\(uid=([^)]+)\\).*");
-    private static final Pattern CN_PATTERN = 
Pattern.compile(".*\\(cn=([^)]+)\\).*");
-    private static final Pattern SAMAACCOUNTNAME_PATTERN = 
Pattern.compile(".*\\(sAMAccountName=([^)]+)\\).*");
 
     private final LdapBackend backend;
 
     public UserSearchInterceptor(String name, Map<String, String> config) 
throws Exception {
+        this(name, BackendFactory.createBackend(name, config));
+    }
+
+    protected UserSearchInterceptor(String name, LdapBackend backend) {
         super(name);
-        backend = BackendFactory.createBackend(name, config);
+        this.backend = backend;
     }
 
     public LdapBackend getBackend() {
@@ -69,9 +68,9 @@ public class UserSearchInterceptor extends BaseInterceptor {
             String username = LdapUtils.extractUsernameFromDn(ctx.getDn());
             if (username != null) {
                 try {
-                    entry = backend.getUser(username, 
directoryService.getSchemaManager());
+                    entry = backend.getUser(username, schemaManager);
                 } catch (Exception e) {
-                    LOG.ldapServiceStopFailed(e);
+                    LOG.ldapLookupFailed(ctx.getDn().toString(),e);
                 }
             }
         }
@@ -80,64 +79,30 @@ public class UserSearchInterceptor extends BaseInterceptor {
 
     @Override
     public EntryFilteringCursor search(SearchOperationContext ctx) throws 
LdapException {
+
         String filter = ctx.getFilter() != null ? ctx.getFilter().toString() : 
"";
         String baseDn = ctx.getDn() != null ? ctx.getDn().toString() : "";
 
         LOG.ldapSearch(baseDn, filter);
 
-        // First execute the next interceptor in the chain
-        EntryFilteringCursor originalResults;
-        originalResults = next(ctx);
-
-        // Check if this is a user search and call the backends
-        if (isUserSearch(filter)) {
-            String username = extractUser(filter);
-
-            // Check if we have any results from local search
-            List<Entry> entries = new ArrayList<>();
-            try {
-                while (originalResults.next()) {
-                    entries.add(originalResults.get());
-                }
-                originalResults.close();
-            } catch (Exception e) {
-                // If we get an error or no results, try the backends
-            }
+        List<Entry> entries = new ArrayList<>();
 
-            if (username != null) {
-                try {
-                    if (username.contains("*")) {
-                        // Wildcard search - use searchUsers
-                        LOG.ldapSearch(baseDn, "wildcard user search: " + 
username);
-                        // Return backend results directly without caching to 
avoid deadlock
-                        // (caching during an active search can cause ApacheDS 
locking issues)
-                        entries.addAll(backend.searchUsers(username, 
schemaManager));
-                    } else {
-                        // if no results, perform single-user search
-                        if (entries.isEmpty()) {
-                            // Specific user lookup
-                            Entry backendEntry = backend.getUser(username, 
schemaManager);
-                            LOG.ldapUserLoaded(username);
-
-                            if (backendEntry != null) {
-                                // Return backend result directly without 
caching
-                                entries.add(backendEntry);
-                                
LOG.ldapUserEntry(backendEntry.getDn().toString());
-                            } else {
-                                LOG.ldapUserNull(username);
-                            }
-                        }
-                    }
-                } catch (Exception e) {
-                    LOG.ldapSearchFailed(baseDn, filter, e);
-                }
+        // First execute the next interceptor in the chain
+        try (EntryFilteringCursor originalResults = next(ctx)){
+            while (originalResults.next()) {
+                entries.add(originalResults.get());
             }
-
-            // Return cursor with our results - use a simple approach
-            return new EntryFilteringCursorImpl(new ListCursor<>(entries), 
ctx, schemaManager);
+        } catch (Exception e) {
+            // If we get an error or no results, try the backends
+        }
+        try {
+            entries.addAll(backend.search(baseDn, ctx.getScope(), filter, 
schemaManager));
+        } catch (Exception e) {
+            LOG.ldapSearchFailed(baseDn, filter, e);
         }
 
-        return originalResults;
+        // Return cursor with our results - use a simple approach
+        return new EntryFilteringCursorImpl(new ListCursor<>(entries), ctx, 
schemaManager);
     }
 
     @Override
@@ -166,30 +131,5 @@ public class UserSearchInterceptor extends BaseInterceptor 
{
             throw new RuntimeException(e);
         }
     }
-
-    private boolean isUserSearch(String filter) {
-        return UID_PATTERN.matcher(filter).matches()
-                || CN_PATTERN.matcher(filter).matches()
-                || SAMAACCOUNTNAME_PATTERN.matcher(filter).matches();
-    }
-
-    private String extractUser(String filter) {
-        Matcher uidMatcher = UID_PATTERN.matcher(filter);
-        if (uidMatcher.matches()) {
-            return uidMatcher.group(1);
-        }
-
-        Matcher cnMatcher = CN_PATTERN.matcher(filter);
-        if (cnMatcher.matches()) {
-            return cnMatcher.group(1);
-        }
-
-        Matcher samaaccountnameMatcher = 
SAMAACCOUNTNAME_PATTERN.matcher(filter);
-        if (samaaccountnameMatcher.matches()) {
-            return samaaccountnameMatcher.group(1);
-        }
-
-        return null;
-    }
 }
 
diff --git 
a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.interceptor.KnoxLdapInterceptorFactory
 
b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.interceptor.KnoxLdapInterceptorFactory
index 22629d400..717536277 100644
--- 
a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.interceptor.KnoxLdapInterceptorFactory
+++ 
b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.interceptor.KnoxLdapInterceptorFactory
@@ -19,4 +19,5 @@
 # Built-in LDAP interceptor factory implementations
 
org.apache.knox.gateway.services.ldap.interceptor.LDAPRolesLookupInterceptorFactory
 org.apache.knox.gateway.services.ldap.interceptor.UserSearchInterceptorFactory
-org.apache.knox.gateway.services.ldap.interceptor.DuplicateUserFilteringInterceptorFactory
\ No newline at end of file
+org.apache.knox.gateway.services.ldap.interceptor.DuplicateUserFilteringInterceptorFactory
+org.apache.knox.gateway.services.ldap.interceptor.DisabledUserInterceptorFactory
\ No newline at end of file
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendTest.java
index 85152cea0..acf8a786d 100644
--- 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendTest.java
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendTest.java
@@ -18,11 +18,15 @@
 package org.apache.knox.gateway.services.ldap.backend;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import org.apache.directory.api.ldap.model.entry.Entry;
 import org.apache.directory.api.ldap.model.entry.Value;
+import org.apache.directory.api.ldap.model.message.SearchScope;
+import org.apache.directory.api.ldap.model.name.Dn;
 import org.apache.directory.api.ldap.model.schema.SchemaManager;
 import org.apache.directory.server.core.api.CoreSession;
 import org.apache.directory.server.core.api.DirectoryService;
@@ -49,6 +53,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.Callable;
 import java.util.concurrent.atomic.AtomicInteger;
 
 public class LdapProxyBackendTest {
@@ -181,25 +186,44 @@ public class LdapProxyBackendTest {
         ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
         Entry entry = ldapProxyBackend.getUser("TestCn1", schemaManager);
-        validateUserEntry(entry, "TestCn1", "TestCn1", 
"[email protected]", "Test user ldaptest1");
+        validateUserEntry(entry, "ldaptest1", "TestCn1", 
"[email protected]", "Test user ldaptest1");
         validateMemberOf(entry, Set.of(
                 "cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org",
                 "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org"));
     }
 
+    @Test
+    public void testGetUserByCNFillsInUIDIfNotPresent() throws Exception {
+        Map<String, String> config = createConfigWithUserAttr("cn");
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+
+        Entry entry = ldapProxyBackend.getUser("TestCn3", schemaManager);
+        validateUserEntry(entry, "TestCn3", "TestCn3", 
"[email protected]", "Test user ldaptest3");
+    }
+
     @Test
     public void testGetUserBySAMAccountName() throws Exception {
         Map<String, String> config = 
createConfigWithUserAttr("sAMAccountName");
         ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
         Entry entry = ldapProxyBackend.getUser("TestSam1", schemaManager);
-        validateUserEntry(entry, "TestSam1", "TestCn1", 
"[email protected]", "Test user ldaptest1");
+        validateUserEntry(entry, "ldaptest1", "TestCn1", 
"[email protected]", "Test user ldaptest1");
         assertEquals("TestSam1", entry.get("sAMAccountName").getString());
         validateMemberOf(entry, Set.of(
                 "cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org",
                 "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org"));
     }
 
+    @Test
+    public void testGetUserBySAMAccountNameFillsInUIDIfNotPresent() throws 
Exception {
+        Map<String, String> config = 
createConfigWithUserAttr("sAMAccountName");
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+
+        Entry entry = ldapProxyBackend.getUser("TestSam3", schemaManager);
+        validateUserEntry(entry, "TestSam3", "TestCn3", 
"[email protected]", "Test user ldaptest3");
+        assertEquals("TestSam3", entry.get("sAMAccountName").getString());
+    }
+
     @Test
     public void testGetUserUseMemberOf() throws Exception {
         Map<String, String> config = new HashMap<>(ldapBackendConfig);
@@ -217,7 +241,7 @@ public class LdapProxyBackendTest {
     public void testGetUserGroups() throws Exception {
         ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
 
-        List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest1");
+        List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest1", 
schemaManager);
         assertTrue(userGroups.contains("group1"));
         assertTrue(userGroups.contains("group2"));
     }
@@ -226,7 +250,7 @@ public class LdapProxyBackendTest {
     public void testGetUserGroupsNoGroups() throws Exception {
         ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
 
-        List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest2");
+        List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest2", 
schemaManager);
         assertTrue(userGroups.isEmpty());
     }
 
@@ -234,7 +258,7 @@ public class LdapProxyBackendTest {
     public void testGetUserGroupsNoUser() throws Exception {
         ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
 
-        List<String> userGroups = ldapProxyBackend.getUserGroups("nobody");
+        List<String> userGroups = ldapProxyBackend.getUserGroups("nobody", 
schemaManager);
         assertTrue(userGroups.isEmpty());
     }
 
@@ -244,7 +268,7 @@ public class LdapProxyBackendTest {
         config.put("useMemberOf", "true");
         ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
-        List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest2");
+        List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest2", 
schemaManager);
         assertTrue(userGroups.contains("groupMemberOf1"));
         assertTrue(userGroups.contains("groupMemberOf2"));
     }
@@ -255,7 +279,7 @@ public class LdapProxyBackendTest {
         config.put("useMemberOf", "true");
         ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
-        List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest1");
+        List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest1", 
schemaManager);
         assertTrue(userGroups.isEmpty());
     }
 
@@ -265,7 +289,7 @@ public class LdapProxyBackendTest {
         config.put("useMemberOf", "true");
         ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
-        List<String> userGroups = ldapProxyBackend.getUserGroups("nobody");
+        List<String> userGroups = ldapProxyBackend.getUserGroups("nobody", 
schemaManager);
         assertTrue(userGroups.isEmpty());
     }
 
@@ -292,14 +316,14 @@ public class LdapProxyBackendTest {
     public void testSearchUsersByCn() throws Exception {
         Map<String, String> config = createConfigWithUserAttr("cn");
         ldapProxyBackend = new LdapProxyBackend("testbackend", config);
-        validateUserSearch("*", 3, Set.of("TestCn1", "TestCn2", "Guest"));
+        validateUserSearch("*", 4, Set.of("ldaptest1", "ldaptest2", "Guest", 
"TestCn3"));
     }
 
     @Test
     public void testSearchUsersPartialByCn() throws Exception {
         Map<String, String> config = createConfigWithUserAttr("cn");
         ldapProxyBackend = new LdapProxyBackend("testbackend", config);
-        validateUserSearch("TestCn*", 2, Set.of("TestCn1", "TestCn2"));
+        validateUserSearch("TestCn*", 3, Set.of("ldaptest1", "ldaptest2", 
"TestCn3"));
     }
 
     @Test
@@ -314,14 +338,14 @@ public class LdapProxyBackendTest {
     public void testSearchUsersBySAMAccountName() throws Exception {
         Map<String, String> config = 
createConfigWithUserAttr("sAMAccountName");
         ldapProxyBackend = new LdapProxyBackend("testbackend", config);
-        validateUserSearch("*", 2, Set.of("TestSam1", "TestSam2"));
+        validateUserSearch("*", 3, Set.of("ldaptest1", "ldaptest2", 
"TestSam3"));
     }
 
     @Test
     public void testSearchUsersPartialBySAMAccountName() throws Exception {
         Map<String, String> config = 
createConfigWithUserAttr("sAMAccountName");
         ldapProxyBackend = new LdapProxyBackend("testbackend", config);
-        validateUserSearch("TestSam*", 2, Set.of("TestSam1", "TestSam2"));
+        validateUserSearch("TestSam*", 3, Set.of("ldaptest1", "ldaptest2", 
"TestSam3"));
     }
 
     @Test
@@ -337,7 +361,7 @@ public class LdapProxyBackendTest {
         Map<String, String> config = createRecursiveConfig(2);
         ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
-        List<String> userGroups = 
ldapProxyBackend.getUserGroups("recursiveUser");
+        List<String> userGroups = 
ldapProxyBackend.getUserGroups("recursiveUser", schemaManager);
         assertEquals(4, userGroups.size());
         assertTrue(userGroups.contains("level1Group"));
         assertTrue(userGroups.contains("level2Group"));
@@ -350,7 +374,7 @@ public class LdapProxyBackendTest {
         Map<String, String> config = createRecursiveConfig(4);
         ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
-        List<String> userGroups = 
ldapProxyBackend.getUserGroups("recursiveUser");
+        List<String> userGroups = 
ldapProxyBackend.getUserGroups("recursiveUser", schemaManager);
         assertEquals(6, userGroups.size());
         assertTrue(userGroups.contains("level1Group"));
         assertTrue(userGroups.contains("level2Group"));
@@ -365,7 +389,7 @@ public class LdapProxyBackendTest {
         Map<String, String> config = createRecursiveConfig(10);
         ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
-        List<String> userGroups = 
ldapProxyBackend.getUserGroups("recursiveUser");
+        List<String> userGroups = 
ldapProxyBackend.getUserGroups("recursiveUser", schemaManager);
         assertTrue(userGroups.contains("cycleGroupA"));
         assertTrue(userGroups.contains("cycleGroupB"));
     }
@@ -377,54 +401,238 @@ public class LdapProxyBackendTest {
 
         Entry entry = ldapProxyBackend.getUser("recursiveUser", schemaManager);
         validateMemberOf(entry, Set.of(
-                "cn=level1Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
-                "cn=level2Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
-                "cn=level3Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
-                "cn=level4Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
-                "cn=cycleGroupA,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
-                
"cn=cycleGroupB,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org"));
+                "cn=level1Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=level2Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=level3Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=level4Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=cycleGroupA,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=cycleGroupB,ou=groups,dc=hadoop,dc=apache,dc=org"));
     }
 
     @Test
     public void testSearchUsersRecursiveWithSharedGroups() throws Exception {
-        Map<String, String> config = createRecursiveConfig(5);
+        testSearchUsersRecursiveWithSharedGroups(
+                () -> ldapProxyBackend.searchUsers("recursiveUser*", 
schemaManager));
+    }
 
-        final AtomicInteger cacheHits = new AtomicInteger(0);
-        ldapProxyBackend = new LdapProxyBackend("testbackend", config) {
-            @Override
-            protected Map<String, Set<Entry>> createResolvedParentsCache() {
-                return new HashMap<>() {
-                    @Override
-                    public Set< 
org.apache.directory.api.ldap.model.entry.Entry> get(Object key) {
-                        if (super.get(key) != null) {
-                            cacheHits.incrementAndGet();
-                        }
-                        return super.get(key);
-                    }
-                };
-            }
-        };
+    @Test
+    public void testSearchRecursiveWithSharedGroups() throws Exception {
+        testSearchUsersRecursiveWithSharedGroups(
+                () -> 
ldapProxyBackend.search("ou=people,dc=hadoop,dc=apache,dc=org", 
SearchScope.SUBTREE, "(uid=recursiveUser*)", schemaManager));
+    }
 
-        // Search for all recursive users (recursiveUser and recursiveUser2)
-        // They share level1Group, cycleGroupA, and all their ancestors.
-        List<Entry> entries = ldapProxyBackend.searchUsers("recursiveUser*", 
schemaManager);
-        assertEquals(2, entries.size());
+    @Test
+    public void testSearchObjectClassInetOrgPerson() throws Exception {
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
+        validateSearch("ou=people,dc=hadoop,dc=apache,dc=org", 
"(objectClass=inetOrgPerson)", 4, Set.of("ldaptest1", "ldaptest2", "guest", 
"TestCn3"));
+    }
 
-        Set<String> expectedGroups = Set.of(
-                "cn=level1Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
-                "cn=level2Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
-                "cn=level3Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
-                "cn=level4Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
-                "cn=cycleGroupA,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
-                
"cn=cycleGroupB,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org");
+    @Test
+    public void testSearchByUid() throws Exception {
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
+        validateSearch("ou=people,dc=hadoop,dc=apache,dc=org", "(uid=guest)", 
1, Set.of("guest"));
+    }
 
-        for (Entry entry : entries) {
-            validateMemberOf(entry, expectedGroups);
-        }
+    @Test
+    public void testSearchByUidWildcard() throws Exception {
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
+        validateSearch("ou=people,dc=hadoop,dc=apache,dc=org", "(uid=*)", 3, 
Set.of("ldaptest1", "ldaptest2", "guest"));
+    }
 
-        // Verify that caching actually happened.
-        // For the second user, many groups should have been found in the 
cache.
-        assertEquals("Expected 6 cache hits for shared groups, but got " + 
cacheHits.get(), 6, cacheHits.get());
+    @Test
+    public void testSearchByUidSubstringWildcard() throws Exception {
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
+        validateSearch("ou=people,dc=hadoop,dc=apache,dc=org", "(uid=ldap*)", 
2, Set.of("ldaptest1", "ldaptest2"));
+    }
+
+    @Test
+    public void testSearchObjectClassGroupOfNames() throws Exception {
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
+        validateSearch("ou=groups,dc=hadoop,dc=apache,dc=org", 
"(objectClass=groupOfNames)", 3, Set.of("group1", "group2", 
"nameddifferently"));
+    }
+
+    @Test
+    public void testSearchByCn() throws Exception {
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
+        validateSearch("ou=groups,dc=hadoop,dc=apache,dc=org", "(cn=group1)", 
1, Set.of("group1"));
+    }
+
+    @Test
+    public void testSearchByCnWildcard() throws Exception {
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
+        validateSearch("ou=groups,dc=hadoop,dc=apache,dc=org", "(cn=*)", 3, 
Set.of("group1", "group2", "nameddifferently"));
+    }
+
+    @Test
+    public void testSearchByCnWSubstringildcard() throws Exception {
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
+        validateSearch("ou=groups,dc=hadoop,dc=apache,dc=org", "(cn=group*)", 
2, Set.of("group1", "group2"));
+    }
+
+    @Test
+    public void testSearchByUidOrCnWildcard() throws Exception {
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
+        validateSearch("dc=hadoop,dc=apache,dc=org", 
"(|(uid=ldap*)(cn=group*))", 4, Set.of("ldaptest1", "ldaptest2", "group1", 
"group2"));
+    }
+
+    @Test
+    public void testSearchRecursiveUserGroupsDepth2() throws Exception {
+        Map<String, String> config = createRecursiveConfig(2);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+
+        List<Entry> entries = 
ldapProxyBackend.search("ou=people,dc=hadoop,dc=apache,dc=org", 
SearchScope.SUBTREE, "(uid=recursiveUser)", schemaManager);
+        assertEquals(1, entries.size());
+        validateMemberOf(entries.get(0), Set.of(
+                "cn=level1Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=level2Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=cycleGroupA,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=cycleGroupB,ou=groups,dc=hadoop,dc=apache,dc=org"));
+    }
+
+    @Test
+    public void testSearchRecursiveGroupsDepth4() throws Exception {
+        Map<String, String> config = createRecursiveConfig(4);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+
+        List<Entry> entries = 
ldapProxyBackend.search("ou=people,dc=hadoop,dc=apache,dc=org", 
SearchScope.SUBTREE, "(uid=recursiveUser)", schemaManager);
+        assertEquals(1, entries.size());
+        validateMemberOf(entries.get(0), Set.of(
+                "cn=level1Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=level2Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=level3Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=level4Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=cycleGroupA,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=cycleGroupB,ou=groups,dc=hadoop,dc=apache,dc=org"));
+    }
+
+    @Test
+    public void testSearchRecursiveGroups() throws Exception {
+        Map<String, String> config = createRecursiveConfig(10);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+
+        List<Entry> entries = 
ldapProxyBackend.search("ou=people,dc=hadoop,dc=apache,dc=org", 
SearchScope.SUBTREE, "(uid=recursiveUser)", schemaManager);
+        assertEquals(1, entries.size());
+        validateMemberOf(entries.get(0), Set.of(
+                "cn=level1Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=level2Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=level3Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=level4Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=cycleGroupA,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=cycleGroupB,ou=groups,dc=hadoop,dc=apache,dc=org"));
+    }
+
+    @Test
+    public void testSearchRecursiveUserGroupsViaMemberOfDepth2() throws 
Exception {
+        Map<String, String> config = createRecursiveConfigForMemberOf(2);
+        config.put("useMemberOf", "true");
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+
+        List<Entry> entries = 
ldapProxyBackend.search("ou=people,dc=hadoop,dc=apache,dc=org", 
SearchScope.SUBTREE, "(uid=memberOfUser)", schemaManager);
+        assertEquals(1, entries.size());
+        validateMemberOf(entries.get(0), Set.of(
+                "cn=memberOflevel1,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOflevel2,ou=groups,dc=hadoop,dc=apache,dc=org"));
+    }
+
+    @Test
+    public void testSearchRecursiveGroupsViaMemberOfDepth3() throws Exception {
+        Map<String, String> config = createRecursiveConfigForMemberOf(3);
+        config.put("useMemberOf", "true");
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+
+        List<Entry> entries = 
ldapProxyBackend.search("ou=people,dc=hadoop,dc=apache,dc=org", 
SearchScope.SUBTREE, "(uid=memberOfUser)", schemaManager);
+        assertEquals(1, entries.size());
+        validateMemberOf(entries.get(0), Set.of(
+                "cn=memberOflevel1,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOflevel2,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOflevel3,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOfCycleA,ou=groups,dc=hadoop,dc=apache,dc=org"));
+    }
+
+    @Test
+    public void testSearchRecursiveGroupsViaMemberOfDepth4() throws Exception {
+        Map<String, String> config = createRecursiveConfigForMemberOf(4);
+        config.put("useMemberOf", "true");
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+
+        List<Entry> entries = 
ldapProxyBackend.search("ou=people,dc=hadoop,dc=apache,dc=org", 
SearchScope.SUBTREE, "(uid=memberOfUser)", schemaManager);
+        assertEquals(1, entries.size());
+        validateMemberOf(entries.get(0), Set.of(
+                "cn=memberOflevel1,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOflevel2,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOflevel3,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOflevel4,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOfCycleA,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOfCycleB,ou=groups,dc=hadoop,dc=apache,dc=org"));
+    }
+
+    @Test
+    public void testSearchRecursiveGroupsViaMemberOf() throws Exception {
+        Map<String, String> config = createRecursiveConfigForMemberOf(10);
+        config.put("useMemberOf", "true");
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+
+        List<Entry> entries = 
ldapProxyBackend.search("ou=people,dc=hadoop,dc=apache,dc=org", 
SearchScope.SUBTREE, "(uid=memberOfUser)", schemaManager);
+        assertEquals(1, entries.size());
+        validateMemberOf(entries.get(0), Set.of(
+                "cn=memberOflevel1,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOflevel2,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOflevel3,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOflevel4,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOfCycleA,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOfCycleB,ou=groups,dc=hadoop,dc=apache,dc=org"));
+    }
+
+    @Test
+    public void testSearchUsersRecursiveWithSharedGroupsViaMemberOf() throws 
Exception {
+        testSearchUsersRecursiveSharedGroupsViaMemberOf(
+                () -> ldapProxyBackend.searchUsers("memberOfUser*", 
schemaManager));
+    }
+
+    @Test
+    public void testSearchRecursiveWithSharedGroupsViaMemberOf() throws 
Exception {
+        testSearchUsersRecursiveSharedGroupsViaMemberOf(
+                () -> 
ldapProxyBackend.search("ou=people,dc=hadoop,dc=apache,dc=org", 
SearchScope.SUBTREE, "(uid=memberOfUser*)", schemaManager));
+    }
+
+    @Test
+    public void testAuthenticate() throws Exception {
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
+        Dn dn = new Dn("uid=guest,ou=people,dc=hadoop,dc=apache,dc=org");
+        assertTrue(ldapProxyBackend.authenticate(dn, "guest-password"));
+    }
+
+    @Test
+    public void testAuthenticateBadPassword() throws Exception {
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
+        Dn dn = new Dn("uid=guest,ou=people,dc=hadoop,dc=apache,dc=org");
+        assertFalse(ldapProxyBackend.authenticate(dn, "bad-password"));
+    }
+
+    @Test
+    public void testAuthenticateNoUser() throws Exception {
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
+        Dn dn = new Dn("uid=nobody,ou=people,dc=hadoop,dc=apache,dc=org");
+        assertFalse(ldapProxyBackend.authenticate(dn, "guest-password"));
+    }
+
+    @Test
+    public void testAuthenticateConvertsBaseDn() throws Exception {
+        Map<String, String> config = new HashMap<>(ldapBackendConfig);
+        config.put("baseDn", "dc=proxy,dc=org");
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+        Dn dn = new Dn("uid=guest,ou=people,dc=proxy,dc=org");
+        assertTrue(ldapProxyBackend.authenticate(dn, "guest-password"));
+    }
+
+    @Test
+    public void testAuthenticateConvertsUserSearchBase() throws Exception {
+        Map<String, String> config = new HashMap<>(ldapBackendConfig);
+        config.put("baseDn", "dc=proxy,dc=org");
+        config.put("userSearchBase", 
"ou=recursiveMemberOfPeople,dc=hadoop,dc=apache,dc=org");
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+        Dn dn = new Dn("uid=memberOfUser2,ou=people,dc=proxy,dc=org");
+        assertTrue(ldapProxyBackend.authenticate(dn, 
"memberOfUser2-password"));
     }
 
     // Helper methods for refactoring
@@ -444,6 +652,16 @@ public class LdapProxyBackendTest {
         return config;
     }
 
+    private Map<String, String> createRecursiveConfigForMemberOf(int depth) {
+        Map<String, String> config = new HashMap<>(ldapBackendConfig);
+        config.put("recursiveGroupResolution", "true");
+        config.put("recursiveGroupResolutionMaxDepth", String.valueOf(depth));
+        config.put("userSearchBase", 
"ou=recursiveMemberOfPeople,dc=hadoop,dc=apache,dc=org");
+        config.put("groupSearchBase", 
"ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org");
+        config.put("useMemberOf", "true");
+        return config;
+    }
+
     private void validateUserEntry(Entry entry, String expectedUid, String 
expectedCn, String expectedMail, String expectedDesc) throws Exception {
         assertEquals(expectedUid, entry.get("uid").getString());
         assertEquals(expectedCn, entry.get("cn").getString());
@@ -452,6 +670,7 @@ public class LdapProxyBackendTest {
     }
 
     private void validateMemberOf(Entry entry, Set<String> expectedGroups) 
throws Exception {
+        assertNotNull(entry.get("memberOf"));
         assertEquals(expectedGroups.size(), entry.get("memberOf").size());
         Set<String> foundGroups = new HashSet<>();
         for (Value value : entry.get("memberOf")) {
@@ -471,4 +690,74 @@ public class LdapProxyBackendTest {
             assertTrue("Expected UID " + uid + " not found", 
foundUids.contains(uid));
         }
     }
+
+    private void validateSearch(String searchBase, String filter, int 
expectedSize, Set<String> expectedRdns) throws Exception {
+        List<Entry> entries = ldapProxyBackend.search(searchBase, 
SearchScope.SUBTREE, filter, schemaManager);
+        Set<String> foundRdns = new HashSet<>();
+        for (Entry entry : entries) {
+            Dn dn = entry.getDn();
+            foundRdns.add(dn.getRdn().getValue());
+        }
+        assertEquals(expectedSize, entries.size());
+        for (String rdn : expectedRdns) {
+            assertTrue("Expected RDN " + rdn + " not found", 
foundRdns.contains(rdn));
+        }
+    }
+
+    private void 
testSearchUsersRecursiveWithSharedGroups(Callable<List<Entry>> ldapSearch) 
throws Exception {
+        Map<String, String> config = createRecursiveConfig(5);
+        Set<String> expectedGroups = Set.of(
+                "cn=level1Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=level2Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=level3Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=level4Group,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=cycleGroupA,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=cycleGroupB,ou=groups,dc=hadoop,dc=apache,dc=org");
+
+        testSearchUsersRecursiveSharedGroups(config, ldapSearch, 2, 
expectedGroups, 6);
+    }
+
+    private void 
testSearchUsersRecursiveSharedGroupsViaMemberOf(Callable<List<Entry>> 
ldapSearch) throws Exception {
+        Map<String, String> config = createRecursiveConfigForMemberOf(5);
+        Set<String> expectedGroups = Set.of(
+                "cn=memberOflevel1,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOflevel2,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOflevel3,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOflevel4,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOfCycleA,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=memberOfCycleB,ou=groups,dc=hadoop,dc=apache,dc=org");
+
+        testSearchUsersRecursiveSharedGroups(config, ldapSearch, 2, 
expectedGroups, 6);
+    }
+
+    private void testSearchUsersRecursiveSharedGroups(Map<String,String> 
config, Callable<List<Entry>> ldapSearch, int expectedEntries, Set<String> 
expectedGroups, int expectedCacheHits) throws Exception {
+        final AtomicInteger cacheHits = new AtomicInteger(0);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config) {
+            @Override
+            protected Map<String, Set<String>> createResolvedParentsCache() {
+                return new HashMap<>() {
+                    @Override
+                    public Set<String> get(Object key) {
+                        if (super.containsKey(key)) {
+                            cacheHits.incrementAndGet();
+                        }
+                        return super.get(key);
+                    }
+                };
+            }
+        };
+
+        // Search for all recursive users (recursiveUser and recursiveUser2)
+        // They share level1Group, cycleGroupA, and all their ancestors.
+        List<Entry> entries = ldapSearch.call();
+        assertEquals(expectedEntries, entries.size());
+
+        for (Entry entry : entries) {
+            validateMemberOf(entry, expectedGroups);
+        }
+
+        // Verify that caching actually happened.
+        // For the second user, many groups should have been found in the 
cache.
+        assertEquals("Expected " + expectedCacheHits + " cache hits for shared 
groups, but got " + cacheHits.get(), expectedCacheHits, cacheHits.get());
+    }
 }
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DisabledUserInterceptorTest.java
similarity index 59%
copy from 
gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorTest.java
copy to 
gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DisabledUserInterceptorTest.java
index d4088a15b..beeee1b5b 100644
--- 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorTest.java
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DisabledUserInterceptorTest.java
@@ -17,16 +17,13 @@
  */
 package org.apache.knox.gateway.services.ldap.interceptor;
 
-import static org.junit.Assert.assertEquals;
+import static 
org.apache.knox.gateway.services.ldap.interceptor.InterceptorTestUtils.assertNextEntryUid;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import org.apache.directory.api.ldap.model.entry.Attribute;
 import org.apache.directory.api.ldap.model.entry.DefaultEntry;
 import org.apache.directory.api.ldap.model.entry.Entry;
-import org.apache.directory.api.ldap.model.entry.Value;
 import org.apache.directory.api.ldap.model.schema.SchemaManager;
-import org.apache.directory.server.core.api.CoreSession;
 import org.apache.directory.server.core.api.DirectoryService;
 import org.apache.directory.server.core.api.filtering.EntryFilteringCursor;
 import 
org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
@@ -38,21 +35,16 @@ import org.junit.Test;
 
 import java.util.List;
 
-/**
- * Unit tests for DuplicateUserFilteringInterceptor.
- */
-public class DuplicateUserFilteringInterceptorTest {
-
+public class DisabledUserInterceptorTest {
     private static final String TEST_INTERCEPTOR = "TEST";
     private static final String NEXT_INTERCEPTOR = "NEXT";
 
-    private DuplicateUserFilteringInterceptor interceptor;
+    private DisabledUserInterceptor interceptor;
 
     private DirectoryService directoryService;
     private SchemaManager schemaManager;
     private ConfigurableEntriesTestInterceptor nextInterceptor;
     private SearchOperationContext ctx;
-    private CoreSession session;
 
     @Before
     public void setUp() throws Exception {
@@ -60,8 +52,15 @@ public class DuplicateUserFilteringInterceptorTest {
         directoryService.setShutdownHookEnabled(false);
         schemaManager = SchemaManagerFactory.createSchemaManager();
         directoryService.setSchemaManager(schemaManager);
+    }
 
-        interceptor = new DuplicateUserFilteringInterceptor(TEST_INTERCEPTOR);
+    @After
+    public void tearDown() throws Exception {
+        directoryService.shutdown();
+    }
+
+    private void setupInterceptor(boolean removeDisabledUSers) throws 
Exception {
+        interceptor = new DisabledUserInterceptor(TEST_INTERCEPTOR, 
removeDisabledUSers);
         interceptor.init(directoryService);
         directoryService.addLast(interceptor);
 
@@ -69,20 +68,14 @@ public class DuplicateUserFilteringInterceptorTest {
         nextInterceptor.init(directoryService);
         directoryService.addLast(nextInterceptor);
 
-        session = directoryService.getSession();
-
-        ctx = new SearchOperationContext(session);
+        ctx = new SearchOperationContext(directoryService.getSession());
         ctx.setInterceptors(List.of(TEST_INTERCEPTOR, NEXT_INTERCEPTOR));
-
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        directoryService.shutdown();
     }
 
     @Test
     public void testEmptyCursor() throws Exception {
+        setupInterceptor(true);
+
         nextInterceptor.setEntries(List.of());
 
         try (EntryFilteringCursor results = interceptor.search(ctx)) {
@@ -94,16 +87,23 @@ public class DuplicateUserFilteringInterceptorTest {
     }
 
     @Test
-    public void testNoDuplicateEntries() throws Exception {
-        Entry entry1 = new DefaultEntry(schemaManager);
-        entry1.add("uid", "user1");
-        Entry entry2 = new DefaultEntry(schemaManager);
-        entry2.add("uid", "user2");
-        nextInterceptor.setEntries(List.of(entry1, entry2));
+    public void testRemoveDisabledUsers() throws Exception {
+        setupInterceptor(true);
+
+        Entry disabledEntryUac = new DefaultEntry(schemaManager);
+        disabledEntryUac.add("uid", "disabledEntryUac");
+        disabledEntryUac.add("useraccountcontrol", "2");
+        Entry disabledEntryNsaccountlock = new DefaultEntry(schemaManager);
+        disabledEntryNsaccountlock.add("uid", "disabledEntryNsaccountlock");
+        disabledEntryNsaccountlock.add("nsaccountlock", "true");
+        Entry enabledEntry = new DefaultEntry(schemaManager);
+        enabledEntry.add("uid", "enabledEntry");
+        enabledEntry.add("useraccountcontrol", "512");
+        nextInterceptor.setEntries(List.of(disabledEntryUac, 
disabledEntryNsaccountlock, enabledEntry));
 
         try (EntryFilteringCursor results = interceptor.search(ctx)) {
-            assertNextEntryUid(results, "user1");
-            assertNextEntryUid(results, "user2");
+            Entry entry = assertNextEntryUid(results, "enabledEntry");
+            assertFalse(entry.contains("nsaccountlock", "true"));
             assertFalse("No more entries expected", results.next());
         }
 
@@ -112,28 +112,31 @@ public class DuplicateUserFilteringInterceptorTest {
     }
 
     @Test
-    public void testDuplicateEntries() throws Exception {
-        Entry entry1 = new DefaultEntry(schemaManager);
-        entry1.add("uid", "user1");
-        Entry entry2 = new DefaultEntry(schemaManager);
-        entry2.add("uid", "user1");
-        nextInterceptor.setEntries(List.of(entry1, entry2));
+    public void testDontRemoveDisabledUsers() throws Exception {
+        setupInterceptor(false);
+
+        Entry disabledEntryUac = new DefaultEntry(schemaManager);
+        disabledEntryUac.add("uid", "disabledEntryUac");
+        disabledEntryUac.add("useraccountcontrol", "2");
+        Entry disabledEntryNsaccountlock = new DefaultEntry(schemaManager);
+        disabledEntryNsaccountlock.add("uid", "disabledEntryNsaccountlock");
+        disabledEntryNsaccountlock.add("nsaccountlock", "true");
+        Entry enabledEntry = new DefaultEntry(schemaManager);
+        enabledEntry.add("uid", "enabledEntry");
+        enabledEntry.add("useraccountcontrol", "512");
+        nextInterceptor.setEntries(List.of(disabledEntryUac, 
disabledEntryNsaccountlock, enabledEntry));
 
         try (EntryFilteringCursor results = interceptor.search(ctx)) {
-            assertNextEntryUid(results, "user1");
+            Entry entry1 = assertNextEntryUid(results, "disabledEntryUac");
+            assertTrue(entry1.contains("nsaccountlock", "true"));
+            Entry entry2 = assertNextEntryUid(results, 
"disabledEntryNsaccountlock");
+            assertTrue(entry2.contains("nsaccountlock", "true"));
+            Entry entry3 = assertNextEntryUid(results, "enabledEntry");
+            assertFalse(entry3.contains("nsaccountlock", "true"));
             assertFalse("No more entries expected", results.next());
         }
 
         EntryFilteringCursor nextInterceptorCursor = 
nextInterceptor.getCursor();
         assertTrue("Cursor must be closed", nextInterceptorCursor.isClosed());
     }
-
-    private void assertNextEntryUid(EntryFilteringCursor cursor, String uid) 
throws Exception {
-        assertTrue("Cursor should have another entry", cursor.next());
-        Entry entry = cursor.get();
-        Attribute uidAttr = entry.get("uid");
-        assertEquals("Attribute should have only one value", 1, 
uidAttr.size());
-        Value value = uidAttr.get();
-        assertEquals("Uid should match " + uid, uid, value.getString());
-    }
 }
\ No newline at end of file
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorTest.java
index d4088a15b..cb9052215 100644
--- 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorTest.java
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorTest.java
@@ -17,16 +17,13 @@
  */
 package org.apache.knox.gateway.services.ldap.interceptor;
 
-import static org.junit.Assert.assertEquals;
+import static 
org.apache.knox.gateway.services.ldap.interceptor.InterceptorTestUtils.assertNextEntryUid;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import org.apache.directory.api.ldap.model.entry.Attribute;
 import org.apache.directory.api.ldap.model.entry.DefaultEntry;
 import org.apache.directory.api.ldap.model.entry.Entry;
-import org.apache.directory.api.ldap.model.entry.Value;
 import org.apache.directory.api.ldap.model.schema.SchemaManager;
-import org.apache.directory.server.core.api.CoreSession;
 import org.apache.directory.server.core.api.DirectoryService;
 import org.apache.directory.server.core.api.filtering.EntryFilteringCursor;
 import 
org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
@@ -52,7 +49,6 @@ public class DuplicateUserFilteringInterceptorTest {
     private SchemaManager schemaManager;
     private ConfigurableEntriesTestInterceptor nextInterceptor;
     private SearchOperationContext ctx;
-    private CoreSession session;
 
     @Before
     public void setUp() throws Exception {
@@ -69,9 +65,7 @@ public class DuplicateUserFilteringInterceptorTest {
         nextInterceptor.init(directoryService);
         directoryService.addLast(nextInterceptor);
 
-        session = directoryService.getSession();
-
-        ctx = new SearchOperationContext(session);
+        ctx = new SearchOperationContext(directoryService.getSession());
         ctx.setInterceptors(List.of(TEST_INTERCEPTOR, NEXT_INTERCEPTOR));
 
     }
@@ -127,13 +121,4 @@ public class DuplicateUserFilteringInterceptorTest {
         EntryFilteringCursor nextInterceptorCursor = 
nextInterceptor.getCursor();
         assertTrue("Cursor must be closed", nextInterceptorCursor.isClosed());
     }
-
-    private void assertNextEntryUid(EntryFilteringCursor cursor, String uid) 
throws Exception {
-        assertTrue("Cursor should have another entry", cursor.next());
-        Entry entry = cursor.get();
-        Attribute uidAttr = entry.get("uid");
-        assertEquals("Attribute should have only one value", 1, 
uidAttr.size());
-        Value value = uidAttr.get();
-        assertEquals("Uid should match " + uid, uid, value.getString());
-    }
 }
\ No newline at end of file
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/InterceptorTestUtils.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/InterceptorTestUtils.java
new file mode 100644
index 000000000..9b27631a7
--- /dev/null
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/InterceptorTestUtils.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.services.ldap.interceptor;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.directory.api.ldap.model.entry.Attribute;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.entry.Value;
+import org.apache.directory.server.core.api.filtering.EntryFilteringCursor;
+
+public class InterceptorTestUtils {
+
+    public static Entry assertNextEntryUid(EntryFilteringCursor cursor, String 
uid) throws Exception {
+        assertTrue("Cursor should have another entry", cursor.next());
+        Entry entry = cursor.get();
+        Attribute uidAttr = entry.get("uid");
+        assertEquals("Attribute should have only one value", 1, 
uidAttr.size());
+        Value value = uidAttr.get();
+        assertEquals("Uid should match " + uid, uid, value.getString());
+        return entry;
+    }
+}
diff --git a/gateway-server/src/test/resources/ldap-proxy-backend-test.ldif 
b/gateway-server/src/test/resources/ldap-proxy-backend-test.ldif
index 186342780..91fb4136c 100644
--- a/gateway-server/src/test/resources/ldap-proxy-backend-test.ldif
+++ b/gateway-server/src/test/resources/ldap-proxy-backend-test.ldif
@@ -58,6 +58,12 @@ objectclass:groupOfNames
 cn: group2
 member: uid=ldaptest1,ou=people,dc=hadoop,dc=apache,dc=org
 
+dn: cn=nameddifferently,ou=groups,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass:groupOfNames
+cn: nameddifferently
+member: uid=guest,ou=people,dc=hadoop,dc=apache,dc=org
+
 dn: uid=ldaptest1,ou=people,dc=hadoop,dc=apache,dc=org
 objectclass:top
 objectclass:person
@@ -85,3 +91,16 @@ mail: [email protected]
 description: Test user ldaptest2
 memberOf: cn=groupMemberOf1,ou=groups,dc=hadoop,dc=apache,dc=org
 memberOf: cn=groupMemberOf2,ou=groups,dc=hadoop,dc=apache,dc=org
+
+dn: cn=TestCn3,ou=people,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass:person
+objectclass:organizationalPerson
+objectclass:inetOrgPerson
+cn: TestCn3
+sn: Ldap
+sAMAccountName: TestSam3
+userPassword: 12345
+mail: [email protected]
+description: Test user ldaptest3
+
diff --git a/gateway-server/src/test/resources/ldap-recursive-test.ldif 
b/gateway-server/src/test/resources/ldap-recursive-test.ldif
index e6182e0dd..da8732759 100644
--- a/gateway-server/src/test/resources/ldap-recursive-test.ldif
+++ b/gateway-server/src/test/resources/ldap-recursive-test.ldif
@@ -28,6 +28,18 @@ objectClass: top
 objectClass: organizationalUnit
 ou: recursiveGroups
 
+# entry for recursive people container for memberOf tests
+dn: ou=recursiveMemberOfPeople,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass:organizationalUnit
+ou: recursiveMemberOfPeople
+
+# entry for recursive group container for memberOf tests
+dn: ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+objectClass: top
+objectClass: organizationalUnit
+ou: recursiveMemberOfGroups
+
 # entry for the end user
 dn: uid=recursiveUser,ou=recursivePeople,dc=hadoop,dc=apache,dc=org
 objectclass:top
@@ -93,3 +105,77 @@ objectclass:top
 objectclass:groupOfNames
 cn: cycleGroupB
 member: cn=cycleGroupA,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org
+
+# entry for sample user memberOfUser using memberOf
+dn: uid=memberOfUser,ou=recursiveMemberOfPeople,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass:person
+objectclass:organizationalPerson
+objectclass:inetOrgPerson
+cn: MemberOf
+sn: User
+uid: memberOfUser
+userPassword:memberOfUser-password
+memberOf: 
cn=memberOflevel1,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+
+# entry for sample user memberOfUser using memberOf
+dn: uid=memberOfUser2,ou=recursiveMemberOfPeople,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass:person
+objectclass:organizationalPerson
+objectclass:inetOrgPerson
+cn: MemberOf2
+sn: User
+uid: memberOfUser2
+userPassword:memberOfUser2-password
+memberOf: 
cn=memberOflevel1,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+
+# memberOf Level 1 Group
+dn: cn=memberOflevel1,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass: groupofnames
+cn: memberOflevel1
+memberOf: 
cn=memberOflevel2,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+member: uid=memberOfUser,ou=recursiveMemberOfPeople,dc=hadoop,dc=apache,dc=org
+member: uid=memberOfUser2,ou=recursiveMemberOfPeople,dc=hadoop,dc=apache,dc=org
+
+# memberOf Level 2 Group (Member is Level 1)
+dn: cn=memberOflevel2,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass: groupofnames
+cn: memberOflevel2level2
+memberOf: 
cn=memberOflevel3,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+memberOf: 
cn=memberOfCycleA,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+member: cn=memberOflevel1,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org
+
+# memberOf Level 3 Group (Member is Level 2)
+dn: cn=memberOflevel3,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass: groupofnames
+cn: memberOflevel3
+memberOf: 
cn=memberOflevel4,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+member: cn=memberOflevel2,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+
+# memberOf Level 4 Group (Member is Level 3)
+dn: cn=memberOflevel4,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass: groupofnames
+cn: memberOflevel4
+member: cn=memberOflevel3,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+
+# memberOf Cycle Group A (level 3)
+dn: cn=memberOfCycleA,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass: groupofnames
+cn: memberOfCycleA
+memberOf: 
cn=memberOfCycleB,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+member: cn=memberOfCycleB,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+member: cn=memberOflevel2,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+
+# memberOf Cycle Group B (level 4)
+dn: cn=memberOfCycleB,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass: groupofnames
+cn: memberOfCycleB
+memberOf: 
cn=memberOfCycleA,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
+member: cn=memberOfCycleA,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
diff --git a/knox-site/docs/service_ldap_server.md 
b/knox-site/docs/service_ldap_server.md
index 0d4a482c4..4dedf9468 100644
--- a/knox-site/docs/service_ldap_server.md
+++ b/knox-site/docs/service_ldap_server.md
@@ -10,7 +10,7 @@ Key features include:
 - **Pluggable LDAP Interceptors**: Support for customization of LDAP search 
behavior including combining results from multiple LDAP backends.
 - **Pluggable Backends**: Support for multiple, different data sources (JSON 
files, remote LDAP/AD).
 - **Embedded Server**: No need for an external LDAP server for simple use 
cases or testing.
-- **Active Directory Integration**: Optimized for proxying to AD with support 
for `sAMAccountName`.
+- **Active Directory Integration**: Optimized for proxying to AD with support 
for `sAMAccountName` and `userAccountControl`.
 
 ## Architecture
 
@@ -110,13 +110,21 @@ remains available with its default credentials.
 | :--- | :--- | :--- |
 | `gateway.ldap.interceptor.<name>.interceptorType` | N/A | The type of 
interceptor to use (`backend` or `duplicateuserfilter`). |
 
-#### Duplicate User Filter Interceptor (`backend`)
+#### User Search Interceptor (`backend`)
 
-The duplicate user filter interceptor ensures that each `Entry` has a unique 
`uid`. This interceptor removes entries that have a duplicate `uid`. 
+The user search interceptor is created if the `interceptorType` configuration 
is set to `backend`. This interceptor forwards search queries to its configured 
backend.
 
-#### User Search Interceptor (`duplicateuserfilter`)
+#### Duplicate User Filter Interceptor (`duplicateuserfilter`)
 
-The user search interceptor is created if the `interceptorType` configuration 
is set to `backend`. This interceptor forwards search queries to its configured 
backend.
+The duplicate user filter interceptor ensures that each `Entry` in the results 
has a unique `uid`. This interceptor removes entries that have a duplicate 
`uid`. 
+
+#### Disabled User Filter Interceptor (`disableduserfilter`)
+
+The disabled user filter interceptor will recognize and filter disabled users. 
If the `removeDisabledUsers` property is set to `true`, users will be removed 
from the results. Users will have their `nsAccountLock` attribute set if 
`removeDisabledUsers` is set to `false`.
+
+| Property | Default Value | Description |
+| :--- | :--- | :--- |
+| `gateway.ldap.interceptor.<name>.removeDisabledUsers` | `false` | Whether to 
remove disabled users from the results. |
 
 #### Roles Lookup Interceptor (`rolesLookup`)
 
@@ -178,15 +186,19 @@ The proxy backend delegates lookups to a remote LDAP or 
Active Directory server.
 | `gateway.ldap.interceptor.<name>.url` | N/A | Remote LDAP URL (e.g., 
`ldap://remote-host:389`). |
 | `gateway.ldap.interceptor.<name>.host` | N/A | Host of remote LDAP (used if 
`url` is not provided). |
 | `gateway.ldap.interceptor.<name>.port` | N/A | Port of remote LDAP (used if 
`url` is not provided). |
+| `gateway.ldap.interceptor.<name>.baseDn` | N/A | **Required**. The base DN 
of the LDAP proxy server. |
 | `gateway.ldap.interceptor.<name>.remoteBaseDn` | N/A | **Required**. The 
base DN of the remote LDAP server. |
 | `gateway.ldap.interceptor.<name>.systemUsername` | N/A | Bind DN for the 
remote server (alias: `bindDn`). |
 | `gateway.ldap.interceptor.<name>.systemPassword` | N/A | Password for the 
bind DN (alias: `bindPassword`). |
 | `gateway.ldap.interceptor.<name>.userSearchBase` | 
`ou=people,{remoteBaseDn}` | Base DN for user searches on the remote server. |
 | `gateway.ldap.interceptor.<name>.groupSearchBase` | 
`ou=groups,{remoteBaseDn}` | Base DN for group searches on the remote server. |
 | `gateway.ldap.interceptor.<name>.userIdentifierAttribute` | `uid` | 
Attribute used for user lookup (e.g., `sAMAccountName` for AD). |
+| `gateway.ldap.interceptor.<name>.userObjectClass` | `inetOrgPerson` | 
Objectclass for identifying users in remote server (e.g., `person` for AD). |
+| `gateway.ldap.interceptor.<name>.groupObjectClass` | `groupOfNames` | 
Objectclass for identifying groups in remote server (e.g., `group` for AD). |
 | `gateway.ldap.interceptor.<name>.groupMemberAttribute` | `memberUid` | 
Attribute used for group membership (e.g., `member` for AD). |
 | `gateway.ldap.interceptor.<name>.useMemberOf` | `false` | If `true`, use the 
`memberOf` attribute for efficient group lookups. |
-| `gateway.interceptor.<name>.proxy.poolMaxActive` | `8` | Maximum number of 
active connections in the pool. |
+| `gateway.ldap.interceptor.<name>.proxy.poolMaxActive` | `8` | Maximum number 
of active connections in the pool. |
+| `gateway.ldap.interceptor.<name>.proxy.poolMaxActive` | `8` | Maximum number 
of active connections in the pool. |
 
 ## Active Directory (AD) Integration
 
@@ -194,7 +206,7 @@ The Knox LDAP Service includes several optimizations for 
working with Active Dir
 
 - **sAMAccountName Support**: The service recognizes `sAMAccountName` in 
search filters, allowing seamless integration with Windows environments.
 - **Efficient Group Lookups**: By setting 
`gateway.ldap.backend.proxy.useMemberOf` to `true`, Knox can retrieve all of a 
user's groups in a single query by reading the `memberOf` attribute, rather 
than searching all group objects.
-- **Schema Extensions**: AD-specific attributes (`memberOf`, `sAMAccountName`) 
are programmatically added to the embedded ApacheDS schema to prevent 
"attribute not found" errors during proxying.
+- **Schema Extensions**: Attributes (e.g., `memberOf`, `nsAccountLock`, and 
AD-specific attributes `sAMAccountName` and `userAccountControl`) are 
programmatically added to the embedded ApacheDS schema to prevent "attribute 
not found" errors during proxying.
 - **Case Sensitivity**: Search filters are handled to accommodate AD's 
case-insensitive nature for user identifiers.
 
 ## Usage Example
@@ -218,7 +230,7 @@ To configure Knox to act as an LDAP proxy for a local file 
and an Active Directo
 
 <property>
     <name>gateway.ldap.interceptor.names</name>
-    <value>localfile,adexample,extrenalldap,duplicatefilter,rolesLookup</value>
+    
<value>localfile,adexample,extrenalldap,duplicatefilter,disableduserfilter,rolesLookup</value>
 </property>
 
 <property>
@@ -282,6 +294,14 @@ To configure Knox to act as an LDAP proxy for a local file 
and an Active Directo
     <name>gateway.ldap.interceptor.adexample.useMemberOf</name>
     <value>true</value>
 </property>
+<property>
+    <name>gateway.ldap.interceptor.adexample.userObjectClass</name>
+    <value>person</value>
+</property>
+<property>
+    <name>gateway.ldap.interceptor.adexample.groupObjectClass</name>
+    <value>group</value>
+</property>
 
 <!--
 Example: Using external LDAP with authentication (supports both naming 
conventions)
@@ -341,6 +361,16 @@ Alternative: Use host and port instead of URL
     <value>duplicateuserfilter</value>
 </property>
 
+<!-- Disabled User Filter Interceptor -->
+<property>
+    <name>gateway.ldap.interceptor.disableduserfilter.interceptorType</name>
+    <value>disableduserfilter</value>
+</property>
+<property>
+    
<name>gateway.ldap.interceptor.disableduserfilter.removeDisabledUsers</name>
+    <value>false</value>
+</property>
+
 <!-- Roles Lookup Interceptor -->
 <property>
     <name>gateway.ldap.interceptor.rolesLookup.interceptorType</name>


Reply via email to