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 27c279a360d180d98a9ee2e7fb8e5e35b7ddb00b
Author: David Han <[email protected]>
AuthorDate: Mon Jun 8 05:30:24 2026 -0500

    KNOX-3330: Refactor LDAP Proxy configuration to support multiple backends 
(#1240)
---
 .github/workflows/build/gateway-site.xml           |  30 ++--
 .../gateway/config/impl/GatewayConfigImpl.java     |   8 +-
 .../services/ldap/KnoxLDAPServerManager.java       | 185 +++++++++++++------
 .../knox/gateway/services/ldap/LdapMessages.java   |  26 ++-
 .../services/ldap/backend/BackendFactory.java      |  23 ++-
 .../gateway/services/ldap/backend/FileBackend.java |  27 ++-
 .../services/ldap/backend/FileBackendFactory.java  |  32 ++++
 .../gateway/services/ldap/backend/LdapBackend.java |  14 +-
 .../services/ldap/backend/LdapBackendFactory.java  |  25 +++
 .../services/ldap/backend/LdapProxyBackend.java    |  32 ++--
 .../ldap/backend/LdapProxyBackendFactory.java      |  32 ++++
 .../DuplicateUserFilteringInterceptor.java         |  87 +++++++++
 .../DuplicateUserFilteringInterceptorFactory.java  |  36 ++++
 .../ldap/interceptor/InterceptorFactory.java       |  59 ++++++
 .../interceptor/KnoxLdapInterceptorFactory.java    |  41 +++++
 .../UserSearchInterceptor.java}                    |  79 ++++----
 .../interceptor/UserSearchInterceptorFactory.java  |  36 ++++
 .../java/org/apache/knox/gateway/util/KnoxCLI.java |   1 +
 ...teway.services.ldap.backend.LdapBackendFactory} |   6 +-
 ...es.ldap.interceptor.KnoxLdapInterceptorFactory} |   6 +-
 .../src/main/resources/conf/gateway-site.xml       | 113 ++++++++----
 .../services/ldap/KnoxLDAPServerManagerTest.java   | 198 ++++++++++++++++++---
 .../gateway/services/ldap/KnoxLDAPServiceTest.java |   7 +-
 .../services/ldap/backend/BackendFactoryTest.java  |  47 +++--
 .../ldap/backend/LdapProxyBackendTest.java         |  63 +++----
 .../DuplicateUserFilteringInterceptorTest.java     | 166 +++++++++++++++++
 .../org/apache/knox/gateway/GatewayTestConfig.java |   6 +-
 .../apache/knox/gateway/config/GatewayConfig.java  |  18 +-
 knox-site/docs/service_ldap_server.md              | 125 +++++++++----
 29 files changed, 1223 insertions(+), 305 deletions(-)

diff --git a/.github/workflows/build/gateway-site.xml 
b/.github/workflows/build/gateway-site.xml
index d8c1bbbd1..8f35a939f 100644
--- a/.github/workflows/build/gateway-site.xml
+++ b/.github/workflows/build/gateway-site.xml
@@ -145,42 +145,50 @@ limitations under the License.
         <name>gateway.ldap.base.dn</name>
         <value>dc=hadoop,dc=apache,dc=org</value>
     </property>
-    <property>
-        <name>gateway.ldap.backend.type</name>
-        <value>ldap</value>
-    </property>
     <property>
         <name>gateway.ldap.recursive.group.resolution</name>
         <value>true</value>
     </property>
+    <property>
+        <name>gateway.ldap.interceptor.names</name>
+        <value>demoldap</value>
+    </property>
 
     <!-- LDAP Backend specific configuration (proxying to demo ldap) -->
     <property>
-        <name>gateway.ldap.backend.ldap.url</name>
+        <name>gateway.ldap.interceptor.demoldap.interceptorType</name>
+        <value>backend</value>
+    </property>
+    <property>
+        <name>gateway.ldap.interceptor.demoldap.backendType</name>
+        <value>ldap</value>
+    </property>
+    <property>
+        <name>gateway.ldap.interceptor.demoldap.url</name>
         <value>ldap://ldap:33389</value>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.remoteBaseDn</name>
+        <name>gateway.ldap.interceptor.demoldap.remoteBaseDn</name>
         <value>dc=hadoop,dc=apache,dc=org</value>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.systemUsername</name>
+        <name>gateway.ldap.interceptor.demoldap.systemUsername</name>
         <value>uid=guest,ou=people,dc=hadoop,dc=apache,dc=org</value>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.systemPassword</name>
+        <name>gateway.ldap.interceptor.demoldap.systemPassword</name>
         <value>guest-password</value>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.userSearchBase</name>
+        <name>gateway.ldap.interceptor.demoldap.userSearchBase</name>
         <value>ou=people,dc=hadoop,dc=apache,dc=org</value>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.groupSearchBase</name>
+        <name>gateway.ldap.interceptor.demoldap.groupSearchBase</name>
         <value>ou=groups,dc=hadoop,dc=apache,dc=org</value>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.groupMemberAttribute</name>
+        <name>gateway.ldap.interceptor.demoldap.groupMemberAttribute</name>
         <value>member</value>
     </property>
 
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
index eb3f9f227..995fa81be 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
@@ -1754,8 +1754,8 @@ public class GatewayConfigImpl extends Configuration 
implements GatewayConfig {
   }
 
   @Override
-  public String getLDAPBackendType() {
-    return get(LDAP_BACKEND_TYPE, "file");
+  public List<String> getLDAPInterceptorNames() {
+    return splitConfigValueToList(LDAP_INTERCEPTOR_NAMES);
   }
 
   @Override
@@ -1782,9 +1782,9 @@ public class GatewayConfigImpl extends Configuration 
implements GatewayConfig {
   }
 
   @Override
-  public Map<String, String> getLDAPBackendConfig(String backendType) {
+  public Map<String, String> getLDAPInterceptorConfig(String interceptorName) {
     Map<String, String> config = new HashMap<>();
-    String prefix = "gateway.ldap.backend." + backendType + ".";
+    String prefix = "gateway.ldap.interceptor." + interceptorName + ".";
 
     for (String key : getPropertyNames()) {
       if (key != null && key.startsWith(prefix)) {
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java
index eebf9251e..ff4edb20e 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java
@@ -17,12 +17,22 @@
  */
 package org.apache.knox.gateway.services.ldap;
 
+import com.google.common.annotations.VisibleForTesting;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.directory.api.ldap.model.cursor.Cursor;
+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.message.SearchRequest;
+import org.apache.directory.api.ldap.model.message.SearchRequestImpl;
+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.DefaultDirectoryService;
 import org.apache.directory.server.core.api.DirectoryService;
+import org.apache.directory.server.core.api.DnFactory;
 import org.apache.directory.server.core.api.InstanceLayout;
 import org.apache.directory.server.core.api.interceptor.Interceptor;
 import org.apache.directory.server.core.api.schema.SchemaPartition;
@@ -32,13 +42,16 @@ import org.apache.directory.server.ldap.LdapServer;
 import org.apache.directory.server.protocol.shared.transport.TcpTransport;
 import org.apache.knox.gateway.config.GatewayConfig;
 import org.apache.knox.gateway.i18n.messages.MessagesFactory;
-import org.apache.knox.gateway.services.ldap.backend.BackendFactory;
-import org.apache.knox.gateway.services.ldap.backend.LdapBackend;
+import org.apache.knox.gateway.services.ldap.interceptor.InterceptorFactory;
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Manages the ApacheDS LDAP server instance with pluggable backends
@@ -46,13 +59,16 @@ import java.util.Map;
 public class KnoxLDAPServerManager {
     private static final LdapMessages LOG = 
MessagesFactory.get(LdapMessages.class);
 
-    private DirectoryService directoryService;
+    @VisibleForTesting
+    DirectoryService directoryService;
     private LdapServer ldapServer;
-    private LdapBackend backend;
+    private GatewayConfig gatewayConfig;
+    private List<Interceptor> interceptors;
     private File workDir;
     private int port;
     private String baseDn;
-    private String remoteBaseDn;
+    // Collection of DNs for the proxied backend LDAP servers
+    private Set<String> baseDns;
 
     /**
      * Initialize the LDAP server with the given configuration
@@ -60,6 +76,8 @@ public class KnoxLDAPServerManager {
      * @param config Gateway configuration
      */
     public void initialize(GatewayConfig config) throws Exception {
+        this.gatewayConfig = config;
+
         // Prepare work directory for LDAP data
         File gatewayDataDir = new File(config.getGatewayDataDir());
         this.workDir = new File(gatewayDataDir, "ldap-server");
@@ -67,27 +85,8 @@ public class KnoxLDAPServerManager {
         // Get configuration
         this.port = config.getLDAPPort();
         this.baseDn = config.getLDAPBaseDN();
-        String backendType = config.getLDAPBackendType();
-
-        // Get backend-specific configuration using prefixed properties
-        Map<String, String> backendConfig = 
config.getLDAPBackendConfig(backendType);
-
-        // Add common configuration
-        backendConfig.put("baseDn", baseDn);
-
-        // Add legacy dataFile property for backwards compatibility with file 
backend
-        if ("file".equalsIgnoreCase(backendType) && 
!backendConfig.containsKey("dataFile")) {
-            backendConfig.put("dataFile", config.getLDAPBackendDataFile());
-        }
-
-        backendConfig.put("recursiveGroupResolution", 
String.valueOf(config.isLDAPRecursiveGroupResolutionEnabled()));
-        backendConfig.put("recursiveGroupResolutionMaxDepth", 
String.valueOf(config.getLDAPRecursiveGroupResolutionMaxDepth()));
 
-        // For proxy backends, extract remoteBaseDn if present
-        this.remoteBaseDn = backendConfig.get("remoteBaseDn");
-
-        // Initialize backend
-        backend = BackendFactory.createBackend(backendType, backendConfig);
+        this.interceptors = createInterceptors(config);
 
         // Clean up previous run if it didn't shut down cleanly
         File lockFile = new File(workDir, "run/instance.lock");
@@ -99,6 +98,34 @@ public class KnoxLDAPServerManager {
         workDir.mkdirs();
     }
 
+    private List<Interceptor> createInterceptors(GatewayConfig config) throws 
Exception {
+        List<String> interceptorNames = config.getLDAPInterceptorNames();
+        List<Interceptor> interceptors = new 
ArrayList<>(interceptorNames.size());
+        for (String interceptorName : interceptorNames) {
+            // Get backend-specific configuration using prefixed properties
+            Map<String, String> interceptorConfig = 
config.getLDAPInterceptorConfig(interceptorName);
+
+            // Add common configuration
+            interceptorConfig.put("baseDn", baseDn);
+
+            // Add common LDAP Proxy configurations to backends
+            String interceptorType = interceptorConfig.get("interceptorType");
+            String backendType = interceptorConfig.get("backendType");
+            if ("backend".equalsIgnoreCase(interceptorType)) {
+                interceptorConfig.put("recursiveGroupResolution", 
String.valueOf(config.isLDAPRecursiveGroupResolutionEnabled()));
+                interceptorConfig.put("recursiveGroupResolutionMaxDepth", 
String.valueOf(config.getLDAPRecursiveGroupResolutionMaxDepth()));
+                if ("file".equalsIgnoreCase(backendType) &&
+                        !interceptorConfig.containsKey("dataFile")) {
+                    // Add legacy dataFile property for backwards 
compatibility with file backend
+                    interceptorConfig.put("dataFile", 
config.getLDAPBackendDataFile());
+                }
+            }
+
+            
interceptors.add(InterceptorFactory.createInterceptor(interceptorName, 
interceptorConfig));
+        }
+        return interceptors;
+    }
+
     /**
      * Start the LDAP server
      */
@@ -134,17 +161,11 @@ public class KnoxLDAPServerManager {
         partition.setPartitionPath(new File(workDir, "proxy").toURI());
         directoryService.addPartition(partition);
 
-        // Create partition for remote base DN if different from proxy base DN
-        // This allows backend entries with remote DNs to be returned in 
search results
-        if (remoteBaseDn != null && !remoteBaseDn.equals(baseDn)) {
-            JdbmPartition remotePartition = new JdbmPartition(schemaManager, 
directoryService.getDnFactory());
-            remotePartition.setId("remote");
-            remotePartition.setSuffixDn(new Dn(schemaManager, remoteBaseDn));
-            remotePartition.setPartitionPath(new File(workDir, 
"remote").toURI());
-            directoryService.addPartition(remotePartition);
-        }
+        baseDns = new HashSet<>();
+        baseDns.add(baseDn);
+        addRemotePartitions();
 
-        addGroupLookupInterceptor();
+        addInterceptors();
 
         // Allow anonymous access
         directoryService.setAllowAnonymousAccess(true);
@@ -152,8 +173,8 @@ public class KnoxLDAPServerManager {
         // Start the service
         directoryService.startup();
 
-        // Add base entries to the partition
-        createBaseEntries(schemaManager);
+        // Add base entries to the partitions
+        createBaseEntries(baseDns, schemaManager);
 
         // Create LDAP server on configured port
         ldapServer = new LdapServer();
@@ -165,25 +186,51 @@ public class KnoxLDAPServerManager {
         LOG.ldapServiceStarted(port);
     }
 
-    private void addGroupLookupInterceptor() {
-        // Add our interceptor for group lookups and bind proxying
-        // We need to insert it before AuthenticationInterceptor to intercept 
bind requests
-        final List<Interceptor> interceptors = new 
ArrayList<>(directoryService.getInterceptors());
+    private void addRemotePartitions() throws LdapException {
+        SchemaManager schemaManager = directoryService.getSchemaManager();
+        DnFactory dnFactory = directoryService.getDnFactory();
+        List<String> interceptorNames = 
gatewayConfig.getLDAPInterceptorNames();
+        for (String interceptorName : interceptorNames) {
+            // Get backend-specific configuration using prefixed properties
+            Map<String, String> interceptorConfig = 
gatewayConfig.getLDAPInterceptorConfig(interceptorName);
+
+            String remoteBaseDn = interceptorConfig.get("remoteBaseDn");
+            if (StringUtils.isNotBlank(remoteBaseDn)) {
+                if (!baseDns.contains(remoteBaseDn)) {
+                    //create partition
+                    String id = interceptorName.replaceAll("\\s+", "");
+                    JdbmPartition remotePartition = new 
JdbmPartition(schemaManager, dnFactory);
+                    remotePartition.setId(id);
+                    remotePartition.setSuffixDn(new Dn(schemaManager, 
remoteBaseDn));
+                    remotePartition.setPartitionPath(new File(workDir, 
id).toURI());
+                    directoryService.addPartition(remotePartition);
+                    baseDns.add(remoteBaseDn);
+                }
+            }
+        }
+    }
+
+    private void addInterceptors() throws LdapException {
+        // Find location of AuthenticationInterceptor.
+        // We need to insert interceptors before AuthenticationInterceptor to 
intercept bind requests
+        final List<Interceptor> dsInterceptors = new 
ArrayList<>(directoryService.getInterceptors());
         int authIdx = -1;
-        for (int i = 0; i < interceptors.size(); i++) {
-            if 
(interceptors.get(i).getName().equalsIgnoreCase("authenticationInterceptor")) {
+        for (int i = 0; i < dsInterceptors.size(); i++) {
+            if 
(dsInterceptors.get(i).getName().equalsIgnoreCase("authenticationInterceptor")) 
{
                 authIdx = i;
                 break;
             }
         }
 
-        final GroupLookupInterceptor interceptor = new 
GroupLookupInterceptor(directoryService, backend);
-        if (authIdx != -1) {
-            interceptors.add(authIdx, interceptor);
-        } else {
-            interceptors.add(interceptor);
+        // Add our configured interceptors for group lookups and bind proxying
+        for (Interceptor interceptor : interceptors) {
+            if (authIdx != -1) {
+                dsInterceptors.add(authIdx, interceptor);
+            } else {
+                dsInterceptors.add(interceptor);
+            }
         }
-        directoryService.setInterceptors(interceptors);
+        directoryService.setInterceptors(dsInterceptors);
     }
 
     /**
@@ -195,6 +242,7 @@ public class KnoxLDAPServerManager {
         if (ldapServer != null) {
             try {
                 ldapServer.stop();
+                ldapServer = null;
             } catch (Exception e) {
                 LOG.ldapServiceStopFailed(e);
             }
@@ -203,6 +251,7 @@ public class KnoxLDAPServerManager {
         if (directoryService != null) {
             try {
                 directoryService.shutdown();
+                directoryService = null;
             } catch (Exception e) {
                 LOG.ldapServiceStopFailed(e);
             }
@@ -211,13 +260,10 @@ public class KnoxLDAPServerManager {
         LOG.ldapServiceStopped();
     }
 
-    private void createBaseEntries(SchemaManager schemaManager) throws 
Exception {
-        // Create base entries for proxy base DN
-        createBaseEntriesForDn(schemaManager, baseDn);
-
-        // Create base entries for remote base DN if different
-        if (remoteBaseDn != null && !remoteBaseDn.equals(baseDn)) {
-            createBaseEntriesForDn(schemaManager, remoteBaseDn);
+    private void createBaseEntries(Collection<String> baseDns, SchemaManager 
schemaManager) throws Exception {
+        // Create base entries for proxy base DN and remote base DNs
+        for (String baseDn : baseDns) {
+            createBaseEntriesForDn(schemaManager, baseDn);
         }
     }
 
@@ -266,7 +312,32 @@ public class KnoxLDAPServerManager {
      * @return List of group names
      */
     public List<String> getUserGroups(String username) throws Exception {
-        return backend.getUserGroups(username);
+        SearchRequest searchRequest = new SearchRequestImpl();
+        searchRequest.setBase(new Dn(directoryService.getSchemaManager(), 
baseDn));
+        searchRequest.setScope(SearchScope.SUBTREE);
+        searchRequest.setFilter("(uid=" + username + ")");
+        searchRequest.addAttributes("*");
+
+        List<String> groups = new ArrayList<>();
+        try (Cursor<Entry> cursor = 
directoryService.getAdminSession().search(searchRequest)) {
+            if (cursor.next()) {
+                Entry entry = cursor.get();
+                Attribute memberOf = entry.get("memberOf");
+                if (memberOf != null) {
+                    for (Value value : memberOf) {
+                        String groupDn = value.getString();
+                        if 
(groupDn.toLowerCase(Locale.ROOT).startsWith("cn=")) {
+                            int commaIdx = groupDn.indexOf(',');
+                            if (commaIdx > 0) {
+                                groups.add(groupDn.substring(3, commaIdx));
+                            }
+                        }
+
+                    }
+                }
+            }
+        }
+        return groups;
     }
 
     /**
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 221e36e4d..947581247 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
@@ -49,13 +49,29 @@ public interface LdapMessages {
             text = "Failed to stop LDAP service: {0}")
     void ldapServiceStopFailed(@StackTrace(level = MessageLevel.DEBUG) 
Exception e);
 
+    @Message(level = MessageLevel.ERROR,
+            text = "Interceptor type ''{0}'' for interceptor ''{1}'' not 
found")
+    void ldapInterceptorNotFound(String interceptorType, String 
interceptorName);
+
+    @Message(level = MessageLevel.ERROR,
+            text = "Interceptor type not found for interceptor ''{0}''")
+    void ldapInterceptorTypeNotFound(String interceptorName);
+
+    @Message(level = MessageLevel.INFO,
+            text = "Creating interceptor: {0} (via {1})")
+    void ldapInterceptorCreating(String interceptorName, String source);
+
     @Message(level = MessageLevel.INFO,
             text = "Loading backend: {0} (via {1})")
     void ldapBackendLoading(String backendName, String source);
 
-    @Message(level = MessageLevel.WARN,
-            text = "Backend ''{0}'' not found, using FileBackend")
-    void ldapBackendNotFound(String backendName);
+    @Message(level = MessageLevel.ERROR,
+            text = "Backend type ''{0}'' for backend ''{1}'' not found")
+    void ldapBackendNotFound(String backendType, String backendName);
+
+    @Message(level = MessageLevel.ERROR,
+            text = "Backend type not found for backend ''{0}''")
+    void ldapBackendTypeNotFound(String backendName);
 
     @Message(level = MessageLevel.WARN,
             text = "Data file not found: {0}, creating sample data")
@@ -73,6 +89,10 @@ public interface LdapMessages {
             text = "LDAP Search: {0} | {1}")
     void ldapSearch(String baseDn, String filter);
 
+    @Message(level = MessageLevel.ERROR,
+            text = "LDAP Search failed: {0} | {1}, {2}")
+    void ldapSearchFailed(String baseDn, String filter, @StackTrace(level = 
MessageLevel.DEBUG) Exception e);
+
     @Message(level = MessageLevel.DEBUG,
             text = "LDAP Bind: {0}")
     void ldapBind(String dn);
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/BackendFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/BackendFactory.java
index f432a6dc5..a204ac460 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/BackendFactory.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/BackendFactory.java
@@ -32,18 +32,25 @@ public class BackendFactory {
     private static final LdapMessages LOG = 
MessagesFactory.get(LdapMessages.class);
 
     public static LdapBackend createBackend(String backendName, Map<String, 
String> config) throws Exception {
-        // Use ServiceLoader to discover all available backends (built-in and 
external plugins)
-        ServiceLoader<LdapBackend> loader = 
ServiceLoader.load(LdapBackend.class);
-        for (LdapBackend backend : loader) {
-            if (backend.getName().equalsIgnoreCase(backendName)) {
-                LOG.ldapBackendLoading(backend.getName(), "ServiceLoader");
-                backend.initialize(config);
+        String backendType = config.get("backendType");
+        if (backendType == null) {
+            // No backend type configured found
+            LOG.ldapBackendTypeNotFound(backendName);
+            throw new IllegalArgumentException("No LDAP backend type 
configured for : " + backendName);
+        }
+
+        // Use ServiceLoader to discover all available backend factories 
(built-in and external plugins)
+        ServiceLoader<LdapBackendFactory> loader = 
ServiceLoader.load(LdapBackendFactory.class);
+        for (LdapBackendFactory backendFactory : loader) {
+            if (backendFactory.getType().equalsIgnoreCase(backendType)) {
+                LOG.ldapBackendLoading(backendType, "ServiceLoader");
+                LdapBackend backend = backendFactory.create(backendName, 
config);
                 return backend;
             }
         }
 
         // No matching backend found
-        LOG.ldapBackendNotFound(backendName);
-        throw new IllegalArgumentException("No LDAP backend found for type: " 
+ backendName);
+        LOG.ldapBackendNotFound(backendType, backendName);
+        throw new IllegalArgumentException("No LDAP backend factory of type " 
+ backendType + " found for : " + backendName);
     }
 }
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 9c3d96aed..8546a3acd 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
@@ -41,9 +41,12 @@ import java.util.Map;
 public class FileBackend implements LdapBackend {
     private static final LdapMessages LOG = 
MessagesFactory.get(LdapMessages.class);
 
+    static final String TYPE = "file";
+
     private Map<String, UserData> users = new HashMap<>();
-    private String dataFile;
-    private String baseDn;
+    private final String dataFile;
+    private final String baseDn;
+    private final String name;
 
     static class UserData {
         String username;
@@ -58,16 +61,26 @@ public class FileBackend implements LdapBackend {
         List<UserData> users;
     }
 
+    public FileBackend(String name, Map<String, String> config) throws 
Exception {
+        this.name = name;
+        dataFile = config.getOrDefault("dataFile", "ldap-users.json");
+        baseDn = config.getOrDefault("baseDn", "dc=proxy,dc=com");
+        loadData();
+    }
+
     @Override
     public String getName() {
-        return "file";
+        return name;
     }
 
     @Override
-    public void initialize(Map<String, String> config) throws Exception {
-        dataFile = config.getOrDefault("dataFile", "ldap-users.json");
-        baseDn = config.getOrDefault("baseDn", "dc=proxy,dc=com");
-        loadData();
+    public String getType() {
+        return TYPE;
+    }
+
+    @Override
+    public String getBaseDn() {
+        return baseDn;
     }
 
     private void loadData() throws Exception {
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackendFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackendFactory.java
new file mode 100644
index 000000000..847753940
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackendFactory.java
@@ -0,0 +1,32 @@
+/*
+ * 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 java.util.Map;
+
+public class FileBackendFactory implements LdapBackendFactory {
+    @Override
+    public LdapBackend create(String name, Map<String, String> config) throws 
Exception {
+        return new FileBackend(name, config);
+    }
+
+    @Override
+    public String getType() {
+        return FileBackend.TYPE;
+    }
+}
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 4c459ff26..f5cc9c75a 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
@@ -22,7 +22,6 @@ import org.apache.directory.api.ldap.model.name.Dn;
 import org.apache.directory.api.ldap.model.schema.SchemaManager;
 
 import java.util.List;
-import java.util.Map;
 
 /**
  * Interface for pluggable LDAP backends.
@@ -33,16 +32,21 @@ import java.util.Map;
  * - REST APIs (Knox, Ranger, etc.)
  */
 public interface LdapBackend {
+
     /**
-     * Get the name of this backend implementation
+     * Get the name of this backend
      */
     String getName();
 
     /**
-     * Initialize the backend with configuration
-     * @param config Configuration properties
+     * Get the type of this backend implementation
+     */
+    String getType();
+
+    /**
+     * Get the base dn of this backend
      */
-    void initialize(Map<String, String> config) throws Exception;
+    String getBaseDn();
 
     /**
      * Get a user entry by username
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackendFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackendFactory.java
new file mode 100644
index 000000000..2ec00eb37
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackendFactory.java
@@ -0,0 +1,25 @@
+/*
+ * 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 java.util.Map;
+
+public interface LdapBackendFactory {
+    LdapBackend create(String name, Map<String, String> config) throws 
Exception;
+    String getType();
+}
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 59355e4ad..07337b049 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
@@ -54,6 +54,9 @@ import java.util.stream.Collectors;
 public class LdapProxyBackend implements LdapBackend {
     private static final LdapMessages LOG = 
MessagesFactory.get(LdapMessages.class);
 
+    static final String TYPE = "ldap";
+
+    private String name;
     private String ldapUrl;
     private String bindDn;
     private String bindPassword;
@@ -83,13 +86,8 @@ public class LdapProxyBackend implements LdapBackend {
     // Connection pool for efficient connection reuse
     private LdapConnectionPool connectionPool;
 
-    @Override
-    public String getName() {
-        return "ldap";
-    }
-
-    @Override
-    public void initialize(Map<String, String> config) throws Exception {
+    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()) {
@@ -154,14 +152,28 @@ public class LdapProxyBackend implements LdapBackend {
         initializeConnectionPool(config);
     }
 
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public String getType() {
+        return TYPE;
+    }
+
+    @Override
+    public String getBaseDn() {
+        return remoteBaseDn;
+    }
+
     /**
      * Initializes the LDAP connection pool with configurable parameters.
      * Uses a validating pool to ensure connections remain healthy.
      *
      * @param config Configuration map that may contain pool settings
-     * @throws Exception if connection pool initialization fails
      */
-    private void initializeConnectionPool(Map<String, String> config) throws 
Exception {
+    private void initializeConnectionPool(Map<String, String> config) {
         // Configure connection settings
         LdapConnectionConfig connectionConfig = new LdapConnectionConfig();
         connectionConfig.setLdapHost(host);
@@ -186,7 +198,7 @@ public class LdapProxyBackend implements LdapBackend {
         connectionPool.setMaxTotal(maxActive);
         connectionPool.setTestOnBorrow(true);
 
-        LOG.ldapBackendLoading(getName(), "Initialized connection pool with 
maxActive=" + maxActive);
+        LOG.ldapBackendLoading(getType(), "Initialized connection pool with 
maxActive=" + maxActive);
     }
 
     private void parseLdapUrl(String url) {
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendFactory.java
new file mode 100644
index 000000000..f138a827d
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendFactory.java
@@ -0,0 +1,32 @@
+/*
+ * 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 java.util.Map;
+
+public class LdapProxyBackendFactory implements LdapBackendFactory {
+    @Override
+    public LdapBackend create(String name, Map<String, String> config) throws 
Exception {
+        return new LdapProxyBackend(name, config);
+    }
+
+    @Override
+    public String getType() {
+        return LdapProxyBackend.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
new file mode 100644
index 000000000..fa7287df1
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptor.java
@@ -0,0 +1,87 @@
+/*
+ * 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 com.google.common.annotations.VisibleForTesting;
+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;
+import org.apache.directory.server.core.api.interceptor.BaseInterceptor;
+import 
org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class DuplicateUserFilteringInterceptor extends BaseInterceptor {
+
+    public DuplicateUserFilteringInterceptor(String name) {
+        super(name);
+    }
+
+    @Override
+    public EntryFilteringCursor search(SearchOperationContext ctx) throws 
LdapException {
+        // First execute the interceptor chain to get the results
+        List<Entry> filteredEntries = List.of();
+        try (EntryFilteringCursor originalResults = next(ctx)) {
+            List<Entry> originalEntries = new ArrayList<>();
+            try {
+                while (originalResults.next()) {
+                    originalEntries.add(originalResults.get());
+                }
+                originalResults.close();
+            } catch (CursorException e) {
+                // rethrow exception on incomplete iteration
+                throw new LdapException(e);
+            }
+            filteredEntries = filterDuplicateUsers(originalEntries);
+        } catch (IOException e) {
+            // IOException would only occur after finishing iterating over 
results
+            // we can ignore this exception and return the filtered entries
+        }
+        return new EntryFilteringCursorImpl(new ListCursor<>(filteredEntries), 
ctx, schemaManager);
+    }
+
+    @VisibleForTesting
+    List<Entry> filterDuplicateUsers(List<Entry> originalEntries) {
+        Set<Value> seenUids = new HashSet<>();
+        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
+                filteredEntries.add(entry);
+            } else {
+                Value uidValue =  uid.get();
+                if (!seenUids.contains(uidValue)) {
+                    filteredEntries.add(entry);
+                    seenUids.add(uidValue);
+                }
+            }
+        }
+        return filteredEntries;
+    }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorFactory.java
new file mode 100644
index 000000000..b443c2def
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorFactory.java
@@ -0,0 +1,36 @@
+/*
+ * 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 java.util.Map;
+
+public class DuplicateUserFilteringInterceptorFactory implements 
KnoxLdapInterceptorFactory {
+    public static final String TYPE = "duplicateuserfilter";
+
+    @Override
+    public Interceptor create(String name, Map<String, String> config) {
+        return new DuplicateUserFilteringInterceptor(name);
+    }
+
+    @Override
+    public String getType() {
+        return TYPE;
+    }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/InterceptorFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/InterceptorFactory.java
new file mode 100644
index 000000000..6b26cc116
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/InterceptorFactory.java
@@ -0,0 +1,59 @@
+/*
+ * 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.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.ldap.LdapMessages;
+
+import java.util.Map;
+import java.util.ServiceLoader;
+
+/**
+ * Factory for creating LDAP Interceptor implementations using ServiceLoader 
for full extensibility.
+ * Backends are discovered via 
META-INF/services/org.apache.knox.gateway.services.ldap.interceptor.KnoxLdapInterceptorFactory
+ * Built-in interceptors are registered via ServiceLoader along with any 
external plugins.
+ */
+public class InterceptorFactory {
+    private static final LdapMessages LOG = 
MessagesFactory.get(LdapMessages.class);
+
+    public static Interceptor createInterceptor(String interceptorName, 
Map<String, String> config) throws Exception {
+        String interceptorType = config.get("interceptorType");
+        if (interceptorType == null) {
+            // No backend type configured found
+            LOG.ldapInterceptorTypeNotFound(interceptorName);
+            throw new IllegalArgumentException("No LDAP interceptor type 
configured for : " + interceptorName);
+        }
+
+        // Use ServiceLoader to discover all available interceptors (built-in 
and external plugins)
+        // Indirect instantiation through a factory is used to allow 
configuration of multiple instances
+        // of the same class of interceptor. e.g., if multiple backends are 
configured
+        ServiceLoader<KnoxLdapInterceptorFactory> loader = 
ServiceLoader.load(KnoxLdapInterceptorFactory.class);
+        for (KnoxLdapInterceptorFactory interceptorFactory : loader) {
+            if 
(interceptorFactory.getType().equalsIgnoreCase(interceptorType)) {
+                LOG.ldapInterceptorCreating(interceptorType, "ServiceLoader");
+                Interceptor interceptor = 
interceptorFactory.create(interceptorName, config);
+                return interceptor;
+            }
+        }
+
+        // No matching interceptor found
+        LOG.ldapInterceptorNotFound(interceptorType, interceptorName);
+        throw new IllegalArgumentException("No LDAP interceptor of type " + 
interceptorType + " found for : " + interceptorName);
+    }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/KnoxLdapInterceptorFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/KnoxLdapInterceptorFactory.java
new file mode 100644
index 000000000..2eb4fb5f9
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/KnoxLdapInterceptorFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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 java.util.Map;
+
+/**
+ * Factory interface for creating Interceptor instances.
+ */
+public interface KnoxLdapInterceptorFactory {
+    /**
+     * Instantiate and interceptor
+     * @param name the name of the interceptor
+     * @param config the configuration for the interceptor
+     * @return the interceptor
+     */
+    Interceptor create(String name, Map<String, String> config) throws 
Exception;
+
+    /**
+     * Get the type of interceptor this factory creates
+     * @return the type of interceptor ths factory creates
+     */
+    String getType();
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/GroupLookupInterceptor.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java
similarity index 77%
rename from 
gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/GroupLookupInterceptor.java
rename to 
gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java
index 1218c8c78..1ac2ef5a2 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/GroupLookupInterceptor.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java
@@ -15,13 +15,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.knox.gateway.services.ldap;
+package org.apache.knox.gateway.services.ldap.interceptor;
 
 import org.apache.directory.api.ldap.model.constants.AuthenticationLevel;
 import org.apache.directory.api.ldap.model.cursor.ListCursor;
 import org.apache.directory.api.ldap.model.entry.Entry;
 import org.apache.directory.api.ldap.model.exception.LdapException;
-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.LdapPrincipal;
@@ -32,27 +31,41 @@ import 
org.apache.directory.server.core.api.interceptor.context.BindOperationCon
 import 
org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
 import 
org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
 import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.ldap.LdapMessages;
+import org.apache.knox.gateway.services.ldap.LdapUtils;
+import org.apache.knox.gateway.services.ldap.backend.BackendFactory;
 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 backend when not 
found locally
+ * Interceptor for LDAP operations to proxy user searches to backends when not 
found locally
  */
-public class GroupLookupInterceptor extends BaseInterceptor {
+public class UserSearchInterceptor extends BaseInterceptor {
+
     private static final LdapMessages LOG = 
MessagesFactory.get(LdapMessages.class);
-    private DirectoryService directoryService;
-    private LdapBackend backend;
     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=([^)]+)\\).*");
 
-    public GroupLookupInterceptor(DirectoryService directoryService, 
LdapBackend backend) {
-        this.directoryService = directoryService;
-        this.backend = backend;
+    private final LdapBackend backend;
+
+    public UserSearchInterceptor(String name, Map<String, String> config) 
throws Exception {
+        super(name);
+        backend = BackendFactory.createBackend(name, config);
+    }
+
+    public LdapBackend getBackend() {
+        return backend;
+    }
+
+    @Override
+    public void init(DirectoryService directoryService) throws LdapException {
+        super.init(directoryService);
     }
 
     @Override
@@ -78,15 +91,11 @@ public class GroupLookupInterceptor extends BaseInterceptor 
{
 
         LOG.ldapSearch(baseDn, filter);
 
-        // First try the normal search
+        // First execute the next interceptor in the chain
         EntryFilteringCursor originalResults;
-        try {
-            originalResults = next(ctx);
-        } catch (Exception e) {
-            throw new LdapException(e);
-        }
+        originalResults = next(ctx);
 
-        // Check if this is a user search and if we got no results, try the 
backend
+        // Check if this is a user search and call the backends
         if (isUserSearch(filter)) {
             String username = extractUser(filter);
 
@@ -98,42 +107,40 @@ public class GroupLookupInterceptor extends 
BaseInterceptor {
                 }
                 originalResults.close();
             } catch (Exception e) {
-                // If we get an error or no results, try the backend
+                // If we get an error or no results, try the backends
             }
 
-            // If no local results, try backend
-            if (entries.isEmpty() && username != null) {
+            if (username != null) {
                 try {
-                    SchemaManager schemaManager = 
directoryService.getSchemaManager();
-
                     if (username.contains("*")) {
                         // Wildcard search - use searchUsers
                         LOG.ldapSearch(baseDn, "wildcard user search: " + 
username);
-                        List<Entry> backendEntries = 
backend.searchUsers(username, schemaManager);
-
                         // Return backend results directly without caching to 
avoid deadlock
                         // (caching during an active search can cause ApacheDS 
locking issues)
-                        entries.addAll(backendEntries);
+                        entries.addAll(backend.searchUsers(username, 
schemaManager));
                     } else {
-                        // Specific user lookup
-                        LOG.ldapUserLoaded(username);
-                        Entry backendEntry = backend.getUser(username, 
schemaManager);
-
-                        if (backendEntry != null) {
-                            // Return backend result directly without caching
-                            entries.add(backendEntry);
-                            LOG.ldapUserEntry(backendEntry.toString());
-                        } else {
-                            LOG.ldapUserNull(username);
+                        // 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.toString());
+                            } else {
+                                LOG.ldapUserNull(username);
+                            }
                         }
                     }
                 } catch (Exception e) {
-                    LOG.ldapServiceStopFailed(e);
+                    LOG.ldapSearchFailed(baseDn, filter, e);
                 }
             }
 
             // Return cursor with our results - use a simple approach
-            return new EntryFilteringCursorImpl(new ListCursor<>(entries), 
ctx, directoryService.getSchemaManager());
+            return new EntryFilteringCursorImpl(new ListCursor<>(entries), 
ctx, schemaManager);
         }
 
         return originalResults;
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptorFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptorFactory.java
new file mode 100644
index 000000000..862d3a964
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptorFactory.java
@@ -0,0 +1,36 @@
+/*
+ * 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 java.util.Map;
+
+public class UserSearchInterceptorFactory implements 
KnoxLdapInterceptorFactory {
+    public static final String TYPE = "backend";
+
+    @Override
+    public Interceptor create(String name, Map<String, String> config) throws 
Exception {
+        return new UserSearchInterceptor(name, config);
+    }
+
+    @Override
+    public String getType() {
+        return TYPE;
+    }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/util/KnoxCLI.java 
b/gateway-server/src/main/java/org/apache/knox/gateway/util/KnoxCLI.java
index b1465a7af..784c416c1 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/util/KnoxCLI.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/util/KnoxCLI.java
@@ -1911,6 +1911,7 @@ public class KnoxCLI extends Configured implements Tool {
         return;
       }
 
+      services.start();
       try {
         final KnoxLDAPService ldapService = 
services.getService(ServiceType.LDAP_SERVICE);
         out.println("Querying KnoxLDAPService for groups of user: " + 
username);
diff --git 
a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackend
 
b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackendFactory
similarity index 83%
copy from 
gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackend
copy to 
gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackendFactory
index c8bd82de6..424d400b8 100644
--- 
a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackend
+++ 
b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackendFactory
@@ -16,6 +16,6 @@
 # limitations under the License.
 ##########################################################################
 
-# Built-in LDAP backend implementations
-org.apache.knox.gateway.services.ldap.backend.FileBackend
-org.apache.knox.gateway.services.ldap.backend.LdapProxyBackend
\ No newline at end of file
+# Built-in LDAP backend factory implementations
+org.apache.knox.gateway.services.ldap.backend.FileBackendFactory
+org.apache.knox.gateway.services.ldap.backend.LdapProxyBackendFactory
\ No newline at end of file
diff --git 
a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackend
 
b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.interceptor.KnoxLdapInterceptorFactory
similarity index 80%
rename from 
gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackend
rename to 
gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.interceptor.KnoxLdapInterceptorFactory
index c8bd82de6..3275b3329 100644
--- 
a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackend
+++ 
b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.interceptor.KnoxLdapInterceptorFactory
@@ -16,6 +16,6 @@
 # limitations under the License.
 ##########################################################################
 
-# Built-in LDAP backend implementations
-org.apache.knox.gateway.services.ldap.backend.FileBackend
-org.apache.knox.gateway.services.ldap.backend.LdapProxyBackend
\ No newline at end of file
+# Built-in LDAP interceptor factory implementations
+org.apache.knox.gateway.services.ldap.interceptor.UserSearchInterceptorFactory
+org.apache.knox.gateway.services.ldap.interceptor.DuplicateUserFilteringInterceptorFactory
\ No newline at end of file
diff --git a/gateway-server/src/main/resources/conf/gateway-site.xml 
b/gateway-server/src/main/resources/conf/gateway-site.xml
index ce7525e2d..70ab3b188 100644
--- a/gateway-server/src/main/resources/conf/gateway-site.xml
+++ b/gateway-server/src/main/resources/conf/gateway-site.xml
@@ -63,9 +63,9 @@ limitations under the License.
     </property>
 
     <property>
-        <name>gateway.ldap.backend.type</name>
-        <value>ldap</value>
-        <description>Backend type for LDAP service. Currently supported: file, 
ldap. Future: jdbc, knox.</description>
+        <name>gateway.ldap.interceptor.names</name>
+        <value>ldapproxy,duplicatefilter</value>
+        <description>Interceptor names for LDAP service in priority 
order.</description>
     </property>
 
     <property>
@@ -74,61 +74,86 @@ limitations under the License.
         <description>Path to JSON data file for file-based backend. Supports 
${GATEWAY_DATA_HOME} variable.</description>
     </property>
 
-    <!-- LDAP backend proxy configuration (active when 
gateway.ldap.backend.type=ldap) -->
+    <!-- LDAP backend proxy configuration (active when 
gateway.ldap.interceptor.names includes ldapproxy) -->
+    <property>
+        <name>gateway.ldap.interceptor.ldapproxy.interceptorType</name>
+        <value>backend</value>
+        <description>Interceptor type.</description>
+    </property>
+    <property>
+        <name>gateway.ldap.interceptor.ldapproxy.backendType</name>
+        <value>ldap</value>
+        <description>Backend type for LDAP service. Currently supported: file, 
ldap. Future: jdbc, knox.</description>
+    </property>
     <property>
-        <name>gateway.ldap.backend.ldap.url</name>
+        <name>gateway.ldap.interceptor.ldapproxy.url</name>
         <value>ldap://localhost:33389</value>
         <description>LDAP server URL for proxy backend</description>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.remoteBaseDn</name>
+        <name>gateway.ldap.interceptor.ldapproxy.remoteBaseDn</name>
         <value>dc=hadoop,dc=apache,dc=org</value>
         <description>Base DN of the remote LDAP server</description>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.systemUsername</name>
+        <name>gateway.ldap.interceptor.ldapproxy.systemUsername</name>
         <value>uid=guest,ou=people,dc=hadoop,dc=apache,dc=org</value>
         <description>LDAP bind DN for proxy backend 
authentication</description>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.systemPassword</name>
+        <name>gateway.ldap.interceptor.ldapproxy.systemPassword</name>
         <value>guest-password</value>
         <description>LDAP bind password for proxy backend 
authentication</description>
     </property>
 
     <!-- Backend-specific configuration using prefixed properties -->
-    <!-- Uncomment and configure based on backend type specified in 
gateway.ldap.backend.type -->
+    <!-- Uncomment and configure based on interceptor names specified in 
gateway.ldap.interceptor.names -->
 
-    <!-- File backend configuration (gateway.ldap.backend.type=file) -->
+    <!-- File backend configuration 
(gateway.ldap.interceptor.<interceptorName>.backendType=file) -->
     <!--
     <property>
-        <name>gateway.ldap.backend.file.dataFile</name>
+        <name>gateway.ldap.interceptor.ldapfile.interceptorType</name>
+        <value>backend</value>
+        <description>Interceptor type.</description>
+    </property>
+    <property>
+        <name>gateway.ldap.interceptor.ldapfile.backendType</name>
+        <value>file</value>
+        <description>Backend type for LDAP service. Currently supported: file, 
ldap. Future: jdbc, knox.</description>
+    </property>
+    <property>
+        <name>gateway.ldap.interceptor.ldapfile.dataFile</name>
         <value>${GATEWAY_DATA_HOME}/ldap-users.json</value>
         <description>Path to JSON file containing user and group 
data</description>
     </property>
     -->
 
-    <!-- LDAP proxy backend configuration (gateway.ldap.backend.type=ldap) -->
+    <!-- LDAP proxy backend configuration 
(gateway.ldap.interceptor.<interceptorName>.backendType=ldap) -->
     <!-- This backend proxies to an external LDAP server (e.g., demo LDAP) -->
     <!--
     Example 1: Using Knox demo LDAP server (default port 33389)
     <property>
-        <name>gateway.ldap.backend.ldap.url</name>
+        <name>gateway.ldap.interceptor.demoldap.backendType</name>
+        <value>ldap</value>
+        <description>Backend type for LDAP service. Currently supported: file, 
ldap. Future: jdbc, knox.</description>
+    </property>
+    <property>
+        <name>gateway.ldap.interceptor.demoldap.url</name>
         <value>ldap://localhost:33389</value>
         <description>LDAP server URL</description>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.remoteBaseDn</name>
+        <name>gateway.ldap.interceptor.demoldap.remoteBaseDn</name>
         <value>dc=hadoop,dc=apache,dc=org</value>
         <description>Base DN of the remote LDAP server</description>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.systemUsername</name>
+        <name>gateway.ldap.interceptor.demoldap.systemUsername</name>
         <value>uid=guest,ou=people,dc=hadoop,dc=apache,dc=org</value>
         <description>LDAP bind DN for authentication</description>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.systemPassword</name>
+        <name>gateway.ldap.interceptor.demoldap.systemPassword</name>
         <value>guest-password</value>
         <description>LDAP bind password</description>
     </property>
@@ -137,32 +162,37 @@ limitations under the License.
     <!--
     Example 2: Using external LDAP with authentication (supports both naming 
conventions)
     <property>
-        <name>gateway.ldap.backend.ldap.url</name>
+        <name>gateway.ldap.interceptor.externalldap.backendType</name>
+        <value>ldap</value>
+        <description>Backend type for LDAP service. Currently supported: file, 
ldap. Future: jdbc, knox.</description>
+    </property>
+    <property>
+        <name>gateway.ldap.interceptor.externalldap.url</name>
         <value>ldap://ldap.example.com:389</value>
         <description>LDAP server URL</description>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.remoteBaseDn</name>
+        <name>gateway.ldap.interceptor.externalldap.remoteBaseDn</name>
         <value>dc=example,dc=com</value>
         <description>Base DN of the remote LDAP server</description>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.systemUsername</name>
+        <name>gateway.ldap.interceptor.externalldap.systemUsername</name>
         <value>cn=admin,dc=example,dc=com</value>
         <description>LDAP bind DN for authentication (or use 
bindDn)</description>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.systemPassword</name>
+        <name>gateway.ldap.interceptor.externalldap.systemPassword</name>
         <value>secret</value>
         <description>LDAP bind password (or use bindPassword)</description>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.userSearchBase</name>
+        <name>gateway.ldap.interceptor.externalldap.userSearchBase</name>
         <value>ou=people,dc=example,dc=com</value>
         <description>Base DN for user searches on remote server (defaults to 
ou=people,{remoteBaseDn})</description>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.groupSearchBase</name>
+        <name>gateway.ldap.interceptor.externalldap.groupSearchBase</name>
         <value>ou=groups,dc=example,dc=com</value>
         <description>Base DN for group searches on remote server (defaults to 
ou=groups,{remoteBaseDn})</description>
     </property>
@@ -170,58 +200,75 @@ limitations under the License.
     <!--
     Alternative: Use host and port instead of URL
     <property>
-        <name>gateway.ldap.backend.ldap.host</name>
+        <name>gateway.ldap.interceptor.externalldap.host</name>
         <value>localhost</value>
         <description>LDAP server hostname</description>
     </property>
     <property>
-        <name>gateway.ldap.backend.ldap.port</name>
+        <name>gateway.ldap.interceptor.externalldap.port</name>
         <value>33389</value>
         <description>LDAP server port</description>
     </property>
     -->
 
-    <!-- Database backend configuration (gateway.ldap.backend.type=database) 
-->
+    <!-- Database backend configuration (gateway.ldap.backend.type=jdbc) -->
     <!--
     <property>
-        <name>gateway.ldap.backend.database.jdbc-url</name>
+        <name>gateway.ldap.interceptor.jdbc.backendType</name>
+        <value>jdbc</value>
+        <description>Backend type for LDAP service. Currently supported: file, 
ldap. Future: jdbc, knox.</description>
+    </property>
+    <property>
+        <name>gateway.ldap.interceptor.jdbc.url</name>
         <value>jdbc:mysql://localhost:3306/knox</value>
         <description>JDBC connection URL</description>
     </property>
     <property>
-        <name>gateway.ldap.backend.database.driver</name>
+        <name>gateway.ldap.interceptor.jdbc.driver</name>
         <value>com.mysql.cj.jdbc.Driver</value>
         <description>JDBC driver class name</description>
     </property>
     <property>
-        <name>gateway.ldap.backend.database.username</name>
+        <name>gateway.ldap.interceptor.jdbc.username</name>
         <value>knox_user</value>
         <description>Database username</description>
     </property>
     <property>
-        <name>gateway.ldap.backend.database.password</name>
+        <name>gateway.ldap.interceptor.jdbc.password</name>
         <value>secret</value>
         <description>Database password</description>
     </property>
     -->
 
-    <!-- Knox Auth backend configuration (gateway.ldap.backend.type=knox-auth) 
-->
+    <!-- Knox Auth backend configuration (gateway.ldap.backend.type=knox) -->
     <!--
     <property>
-        <name>gateway.ldap.backend.knox-auth.url</name>
+        <name>gateway.ldap.interceptor.knox.backendType</name>
+        <value>knox</value>
+        <description>Backend type for LDAP service. Currently supported: file, 
ldap. Future: jdbc, knox.</description>
+    </property>
+    <property>
+        <name>gateway.ldap.interceptor.knox.url</name>
         <value>https://knox-server/gateway/sandbox/knoxauth/api</value>
         <description>Knox authentication service URL</description>
     </property>
     <property>
-        <name>gateway.ldap.backend.knox-auth.username</name>
+        <name>gateway.ldap.interceptor.knox.username</name>
         <value>admin</value>
         <description>Knox admin username</description>
     </property>
     <property>
-        <name>gateway.ldap.backend.knox-auth.password</name>
+        <name>gateway.ldap.interceptor.knox.password</name>
         <value>admin-password</value>
         <description>Knox admin password</description>
     </property>
     -->
 
+    <!-- Duplicate Filter Interceptor -->
+    <property>
+        <name>gateway.ldap.interceptor.duplicatefilter.interceptorType</name>
+        <value>duplicateuserfilter</value>
+        <description>Interceptor type.</description>
+    </property>
+
 </configuration>
\ No newline at end of file
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java
index 4528c8b4a..c41afb0c0 100644
--- 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java
@@ -19,17 +19,22 @@ package org.apache.knox.gateway.services.ldap;
 
 import org.apache.knox.gateway.config.GatewayConfig;
 import org.easymock.EasyMock;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.knox.gateway.services.ldap.interceptor.UserSearchInterceptor;
 import org.junit.Test;
 import org.junit.Before;
 import org.junit.After;
 
 import java.io.File;
+import java.net.ServerSocket;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.replay;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.assertFalse;
 
@@ -41,6 +46,7 @@ public class KnoxLDAPServerManagerTest {
     private KnoxLDAPServerManager serverManager;
     private File tempWorkDir;
     private File tempLdapFile;
+    private int port;
 
     @Before
     public void setUp() throws Exception {
@@ -59,6 +65,11 @@ public class KnoxLDAPServerManagerTest {
         try (java.io.BufferedWriter writer = 
java.nio.file.Files.newBufferedWriter(tempLdapFile.toPath(), 
java.nio.charset.StandardCharsets.UTF_8)) {
             
writer.write("{\"users\":[{\"dn\":\"uid=admin,ou=people,dc=test,dc=com\",\"uid\":\"admin\",\"cn\":\"Administrator\",\"userPassword\":\"admin-password\"}],\"groups\":[]}");
         }
+
+        // pick an unused port
+        try (ServerSocket socket = new ServerSocket(0)) {
+            port = socket.getLocalPort();
+        }
     }
 
     @After
@@ -77,16 +88,17 @@ public class KnoxLDAPServerManagerTest {
     public void testInitializeWithFileBackend() throws Exception {
         GatewayConfig mockConfig = 
EasyMock.createNiceMock(GatewayConfig.class);
         
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
-        expect(mockConfig.getLDAPPort()).andReturn(3890).anyTimes();
+        expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
         
expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
-        expect(mockConfig.getLDAPBackendType()).andReturn("file").anyTimes();
+        
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes();
         
expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
-        expect(mockConfig.getLDAPBackendConfig("file")).andReturn(new 
HashMap<>()).anyTimes();
+        Map<String, String> backendConfig = 
createFileBackendInterceptorConfig();
+        
expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(backendConfig).anyTimes();
         replay(mockConfig);
 
         serverManager.initialize(mockConfig);
 
-        assertEquals("Port should be set correctly", 3890, 
serverManager.getPort());
+        assertEquals("Port should be set correctly", port, 
serverManager.getPort());
         assertEquals("Base DN should be set correctly", "dc=test,dc=com", 
serverManager.getBaseDn());
         assertFalse("Should not be running after initialize", 
serverManager.isRunning());
     }
@@ -95,44 +107,62 @@ public class KnoxLDAPServerManagerTest {
     public void testInitializeWithLdapBackend() throws Exception {
         GatewayConfig mockConfig = 
EasyMock.createNiceMock(GatewayConfig.class);
         
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
-        expect(mockConfig.getLDAPPort()).andReturn(3891).anyTimes();
-        
expect(mockConfig.getLDAPBaseDN()).andReturn("dc=proxy,dc=com").anyTimes();
-        expect(mockConfig.getLDAPBackendType()).andReturn("ldap").anyTimes();
-
-        Map<String, String> backendConfig = new HashMap<>();
-        backendConfig.put("url", "ldap://localhost:33389";);
-        backendConfig.put("remoteBaseDn", "dc=hadoop,dc=apache,dc=org");
-        backendConfig.put("systemUsername", 
"cn=admin,dc=hadoop,dc=apache,dc=org");
-        backendConfig.put("systemPassword", "admin-password");
-
-        
expect(mockConfig.getLDAPBackendConfig("ldap")).andReturn(backendConfig).anyTimes();
+        expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
+        
expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+        
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("ldapbackend")).anyTimes();
+        Map<String, String> backendConfig = 
createLdapBackendInterceptorConfig();
+        
expect(mockConfig.getLDAPInterceptorConfig("ldapbackend")).andReturn(backendConfig).anyTimes();
         replay(mockConfig);
 
         serverManager.initialize(mockConfig);
 
-        assertEquals("Port should be set correctly", 3891, 
serverManager.getPort());
-        assertEquals("Base DN should be set correctly", "dc=proxy,dc=com", 
serverManager.getBaseDn());
+        assertEquals("Port should be set correctly", port, 
serverManager.getPort());
+        assertEquals("Base DN should be set correctly", "dc=test,dc=com", 
serverManager.getBaseDn());
         assertFalse("Should not be running after initialize", 
serverManager.isRunning());
     }
 
     @Test(expected = Exception.class)
-    public void testInitializeWithInvalidBackendType() throws Exception {
+    public void testInitializeWithInvalidInterceptorType() throws Exception {
         GatewayConfig mockConfig = 
EasyMock.createNiceMock(GatewayConfig.class);
         
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
-        
expect(mockConfig.getLDAPBackendType()).andReturn("invalid").anyTimes();
-        expect(mockConfig.getLDAPBackendConfig("invalid")).andReturn(new 
HashMap<>()).anyTimes();
+        
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("invalid")).anyTimes();
+        Map<String, String> backendConfig = new HashMap<>();
+        backendConfig.put("interceptorType", "badinterceptor");
+        expect(mockConfig.getLDAPInterceptorConfig("invalid")).andReturn(new 
HashMap<>()).anyTimes();
         replay(mockConfig);
 
         serverManager.initialize(mockConfig);
     }
 
+    @Test
+    public void testInitializeWithMultipleBackends() throws Exception {
+        GatewayConfig mockConfig = 
EasyMock.createNiceMock(GatewayConfig.class);
+        
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
+        expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
+        
expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+        
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend", 
"ldapbackend")).anyTimes();
+        
expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
+        Map<String, String> fileBackendConfig = 
createFileBackendInterceptorConfig();
+        
expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(fileBackendConfig).anyTimes();
+        Map<String, String> ldapBackendConfig = 
createLdapBackendInterceptorConfig();
+        
expect(mockConfig.getLDAPInterceptorConfig("ldapbackend")).andReturn(ldapBackendConfig).anyTimes();
+        replay(mockConfig);
+
+        serverManager.initialize(mockConfig);
+
+        assertEquals("Port should be set correctly", port, 
serverManager.getPort());
+        assertEquals("Base DN should be set correctly", "dc=test,dc=com", 
serverManager.getBaseDn());
+        assertFalse("Should not be running after initialize", 
serverManager.isRunning());
+    }
+
     @Test
     public void testLockFileCleanup() throws Exception {
         GatewayConfig mockConfig = 
EasyMock.createNiceMock(GatewayConfig.class);
         
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
-        expect(mockConfig.getLDAPBackendType()).andReturn("file").anyTimes();
+        
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes();
         
expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
-        expect(mockConfig.getLDAPBackendConfig("file")).andReturn(new 
HashMap<>()).anyTimes();
+        Map<String, String> backendConfig = 
createFileBackendInterceptorConfig();
+        
expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(backendConfig).anyTimes();
         replay(mockConfig);
 
         // The manager will use tempWorkDir.getParent()/ldap-server as workDir
@@ -162,9 +192,11 @@ public class KnoxLDAPServerManagerTest {
     public void testStopBeforeStart() throws Exception {
         GatewayConfig mockConfig = 
EasyMock.createNiceMock(GatewayConfig.class);
         
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
-        expect(mockConfig.getLDAPBackendType()).andReturn("file").anyTimes();
+        
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes();
         
expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
-        expect(mockConfig.getLDAPBackendConfig("file")).andReturn(new 
HashMap<>()).anyTimes();
+        Map<String, String> backendConfig = 
createFileBackendInterceptorConfig();
+        
expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(backendConfig).anyTimes();
+
         replay(mockConfig);
 
         serverManager.initialize(mockConfig);
@@ -177,9 +209,10 @@ public class KnoxLDAPServerManagerTest {
     public void testMultipleStopCalls() throws Exception {
         GatewayConfig mockConfig = 
EasyMock.createNiceMock(GatewayConfig.class);
         
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
-        expect(mockConfig.getLDAPBackendType()).andReturn("file").anyTimes();
+        
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes();
         
expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
-        expect(mockConfig.getLDAPBackendConfig("file")).andReturn(new 
HashMap<>()).anyTimes();
+        Map<String, String> backendConfig = 
createFileBackendInterceptorConfig();
+        
expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(backendConfig).anyTimes();
         replay(mockConfig);
 
         serverManager.initialize(mockConfig);
@@ -196,6 +229,119 @@ public class KnoxLDAPServerManagerTest {
         serverManager.start();
     }
 
+    @Test
+    public void testStartWithFileBackend() throws Exception {
+        GatewayConfig mockConfig = 
EasyMock.createNiceMock(GatewayConfig.class);
+        
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
+        expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
+        
expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+        
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes();
+        
expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
+        Map<String, String> backendConfig = 
createFileBackendInterceptorConfig();
+        
expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(backendConfig).anyTimes();
+        replay(mockConfig);
+
+        serverManager.initialize(mockConfig);
+
+        serverManager.start();
+
+        UserSearchInterceptor interceptor = (UserSearchInterceptor) 
serverManager.directoryService.getInterceptor("filebackend");
+        assertNotNull("Interceptor should not be null", interceptor);
+        assertEquals("File backend dn should match configuration",
+                backendConfig.get("baseDn"), 
interceptor.getBackend().getBaseDn());
+        // LdapNoSuchObjectException will be thrown if expected partition does 
not exist
+        serverManager.directoryService.getPartitionNexus().getPartition(
+                new Dn(serverManager.directoryService.getSchemaManager(), 
backendConfig.get("baseDn")));
+    }
+
+    @Test
+    public void testStartWithLdapProxyBackend() throws Exception {
+        GatewayConfig mockConfig = 
EasyMock.createNiceMock(GatewayConfig.class);
+        
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
+        expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
+        
expect(mockConfig.getLDAPBaseDN()).andReturn("dc=proxy,dc=com").anyTimes();
+        
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("ldapbackend")).anyTimes();
+        Map<String, String> backendConfig = 
createLdapBackendInterceptorConfig();
+        
expect(mockConfig.getLDAPInterceptorConfig("ldapbackend")).andReturn(backendConfig).anyTimes();
+        replay(mockConfig);
+
+        serverManager.initialize(mockConfig);
+
+        serverManager.start();
+
+        // Ensure that the partitions are created and backends registered with 
the UserSearchInterceptor
+        UserSearchInterceptor interceptor = (UserSearchInterceptor) 
serverManager.directoryService.getInterceptor("ldapbackend");
+        assertNotNull("Interceptor should not be null", interceptor);
+        assertEquals("LDAP backend dn should match configuration",
+                backendConfig.get("remoteBaseDn"), 
interceptor.getBackend().getBaseDn());
+        // LdapNoSuchObjectException will be thrown if expected partition does 
not exist
+        serverManager.directoryService.getPartitionNexus().getPartition(
+                new Dn(serverManager.directoryService.getSchemaManager(), 
backendConfig.get("baseDn")));
+    }
+
+    @Test
+    public void testStartWithMultipleBackends() throws Exception {
+        GatewayConfig mockConfig = 
EasyMock.createNiceMock(GatewayConfig.class);
+        
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
+        expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
+        
expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+        
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend", 
"ldapbackend")).anyTimes();
+        
expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
+        Map<String, String> fileBackendConfig = 
createFileBackendInterceptorConfig();
+        
expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(fileBackendConfig).anyTimes();
+        Map<String, String> ldapBackendConfig = 
createLdapBackendInterceptorConfig();
+        
expect(mockConfig.getLDAPInterceptorConfig("ldapbackend")).andReturn(ldapBackendConfig).anyTimes();
+        replay(mockConfig);
+
+        serverManager.initialize(mockConfig);
+
+        serverManager.start();
+
+        // Ensure that the partitions are created and backends registered with 
the file backend interceptor
+        UserSearchInterceptor fileInterceptor = (UserSearchInterceptor) 
serverManager.directoryService.getInterceptor("filebackend");
+        assertNotNull("Interceptor should not be null", fileInterceptor);
+        assertEquals("File backend dn should match configuration",
+                fileBackendConfig.get("baseDn"), 
fileInterceptor.getBackend().getBaseDn());
+        // LdapNoSuchObjectException will be thrown if expected partition does 
not exist
+        serverManager.directoryService.getPartitionNexus().getPartition(
+                new Dn(serverManager.directoryService.getSchemaManager(), 
fileBackendConfig.get("baseDn")));
+
+        // Ensure that the partitions are created and backends registered with 
the ldap backend interceptor
+        UserSearchInterceptor ldapInterceptor = (UserSearchInterceptor) 
serverManager.directoryService.getInterceptor("ldapbackend");
+        assertNotNull("Interceptor should not be null", ldapInterceptor);
+        assertEquals("LDAP backend dn should match configuration",
+                ldapBackendConfig.get("remoteBaseDn"), 
ldapInterceptor.getBackend().getBaseDn());
+        // LdapNoSuchObjectException will be thrown if expected partition does 
not exist
+        serverManager.directoryService.getPartitionNexus().getPartition(
+                new Dn(serverManager.directoryService.getSchemaManager(), 
ldapBackendConfig.get("baseDn")));
+    }
+
+    @Test
+    public void testGetUserGroups() {
+
+    }
+
+    private Map<String, String> createFileBackendInterceptorConfig() {
+        Map<String, String> config = new HashMap<>();
+        config.put("interceptorType", "backend");
+        config.put("backendType", "file");
+        config.put("baseDn", "dc=file,dc=com");
+        config.put("dataFile", tempLdapFile.getAbsolutePath());
+        return config;
+    }
+
+    private Map<String, String> createLdapBackendInterceptorConfig() {
+        Map<String, String> config = new HashMap<>();
+        config.put("interceptorType", "backend");
+        config.put("backendType", "ldap");
+        config.put("baseDn", "dc=proxy,dc=com");
+        config.put("url", "ldap://localhost:33389";);
+        config.put("remoteBaseDn", "dc=hadoop,dc=apache,dc=org");
+        config.put("systemUsername", "cn=admin,dc=hadoop,dc=apache,dc=org");
+        config.put("systemPassword", "admin-password");
+        return config;
+    }
+
     private void cleanupTempFiles() {
         if (tempLdapFile != null && tempLdapFile.exists()) {
             tempLdapFile.delete();
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java
index 40777b253..399e05485 100644
--- 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java
@@ -25,6 +25,7 @@ import org.junit.After;
 
 import java.io.File;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import static org.junit.Assert.assertEquals;
@@ -169,13 +170,15 @@ public class KnoxLDAPServiceTest {
         
expect(mockConfig.getGatewayDataDir()).andReturn(tempDataDir.getAbsolutePath()).atLeastOnce();
         
expect(mockConfig.getLDAPPort()).andReturn(3890).times(1).andReturn(3891).anyTimes();
         
expect(mockConfig.getLDAPBaseDN()).andReturn("file".equals(backendType) ? 
"dc=test,dc=com" : "dc=proxy,dc=com").atLeastOnce();
-        
expect(mockConfig.getLDAPBackendType()).andReturn(backendType).atLeastOnce();
-        
expect(mockConfig.getLDAPBackendConfig(backendType)).andReturn(buildBackendConfig(backendType)).atLeastOnce();
+        
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("testbackend")).atLeastOnce();
+        
expect(mockConfig.getLDAPInterceptorConfig("testbackend")).andReturn(buildBackendConfig(backendType)).atLeastOnce();
         replay(mockConfig);
     }
 
     private Map<String, String> buildBackendConfig(String backendType) {
         final Map<String, String> backendConfig = new HashMap<>();
+        backendConfig.put("interceptorType", "backend");
+        backendConfig.put("backendType", backendType);
         if ("ldap".equals(backendType)) {
             backendConfig.put("url", "ldap://localhost:33389";);
             backendConfig.put("remoteBaseDn", "dc=hadoop,dc=apache,dc=org");
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/BackendFactoryTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/BackendFactoryTest.java
index fde29a087..cfb8fa2d5 100644
--- 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/BackendFactoryTest.java
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/BackendFactoryTest.java
@@ -54,20 +54,20 @@ public class BackendFactoryTest {
 
     @Test
     public void testServiceLoaderDiscovery() {
-        ServiceLoader<LdapBackend> loader = 
ServiceLoader.load(LdapBackend.class);
+        ServiceLoader<LdapBackendFactory> loader = 
ServiceLoader.load(LdapBackendFactory.class);
 
         // Should discover at least the built-in backends
         boolean foundFileBackend = false;
         boolean foundLdapBackend = false;
 
-        for (LdapBackend backend : loader) {
-            String backendName = backend.getName();
-            if ("file".equals(backendName)) {
+        for (LdapBackendFactory factory : loader) {
+            String backendType = factory.getType();
+            if ("file".equals(backendType)) {
                 foundFileBackend = true;
-                assertTrue("File backend should be FileBackend instance", 
backend instanceof FileBackend);
-            } else if ("ldap".equals(backendName)) {
+                assertTrue("File backend should be FileBackend instance", 
factory instanceof FileBackendFactory);
+            } else if ("ldap".equals(backendType)) {
                 foundLdapBackend = true;
-                assertTrue("LDAP backend should be LdapProxyBackend instance", 
backend instanceof LdapProxyBackend);
+                assertTrue("LDAP backend should be LdapProxyBackend instance", 
factory instanceof LdapProxyBackendFactory);
             }
         }
 
@@ -77,48 +77,57 @@ public class BackendFactoryTest {
 
     @Test
     public void testCreateFileBackend() throws Exception {
-        LdapBackend fileBackend = BackendFactory.createBackend("file", config);
+        config.put("backendType", "file");
+        LdapBackend fileBackend = BackendFactory.createBackend("testbackend", 
config);
 
         assertNotNull("File backend should be created", fileBackend);
         assertTrue("Should create FileBackend instance", fileBackend 
instanceof FileBackend);
-        assertEquals("Backend name should be 'file'", "file", 
fileBackend.getName());
+        assertEquals("Backend type should be 'file'", "file", 
fileBackend.getType());
+        assertEquals("Backend name should be 'testbackend'", "testbackend", 
fileBackend.getName());
     }
 
     @Test
     public void testCreateLdapBackend() throws Exception {
+        config.put("backendType", "ldap");
         config.put("url", "ldap://localhost:389";);
         config.put("remoteBaseDn", "dc=hadoop,dc=apache,dc=org");
 
-        LdapBackend ldapBackend = BackendFactory.createBackend("ldap", config);
+        LdapBackend ldapBackend = BackendFactory.createBackend("testbackend", 
config);
 
         assertNotNull("LDAP backend should be created", ldapBackend);
         assertTrue("Should create LdapProxyBackend instance", ldapBackend 
instanceof LdapProxyBackend);
-        assertEquals("Backend name should be 'ldap'", "ldap", 
ldapBackend.getName());
+        assertEquals("Backend type should be 'ldap'", "ldap", 
ldapBackend.getType());
+        assertEquals("Backend name should be 'testbackend'", "testbackend", 
ldapBackend.getName());
     }
 
     @Test
-    public void testCaseInsensitiveBackendNames() throws Exception {
+    public void testCaseInsensitiveFileBackend() throws Exception {
         // Test uppercase
-        LdapBackend upperCaseBackend = BackendFactory.createBackend("FILE", 
config);
-        assertTrue("Should create FileBackend with uppercase name", 
upperCaseBackend instanceof FileBackend);
+        config.put("backendType", "FILE");
+        LdapBackend upperCaseBackend = BackendFactory.createBackend("UPPER", 
config);
+        assertTrue("Should create FileBackend with uppercase type", 
upperCaseBackend instanceof FileBackend);
 
         // Test mixed case
-        LdapBackend mixedCaseBackend = BackendFactory.createBackend("File", 
config);
-        assertTrue("Should create FileBackend with mixed case name", 
mixedCaseBackend instanceof FileBackend);
+        config.put("backendType", "File");
+        LdapBackend mixedCaseBackend = BackendFactory.createBackend("Mixed", 
config);
+        assertTrue("Should create FileBackend with mixed case type", 
mixedCaseBackend instanceof FileBackend);
     }
 
     @Test(expected = IllegalArgumentException.class)
     public void testUnknownBackendThrowsException() throws Exception {
-        BackendFactory.createBackend("unknown", config);
+        config.put("backendType", "unknown");
+        BackendFactory.createBackend("testbackend", config);
     }
 
     @Test(expected = IllegalArgumentException.class)
     public void testNullBackendNameThrowsException() throws Exception {
-        BackendFactory.createBackend(null, config);
+        config.put("backendType", null);
+        BackendFactory.createBackend("testbackend", config);
     }
 
     @Test(expected = IllegalArgumentException.class)
     public void testEmptyBackendNameThrowsException() throws Exception {
-        BackendFactory.createBackend("", config);
+        config.put("backendType", "");
+        BackendFactory.createBackend("testbackend", config);
     }
 }
\ 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 0aba49487..85152cea0 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
@@ -39,7 +39,6 @@ import 
org.apache.knox.gateway.security.ldap.SimpleDirectoryService;
 import org.apache.knox.gateway.services.ldap.SchemaManagerFactory;
 import org.junit.After;
 import org.junit.AfterClass;
-import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
@@ -137,20 +136,17 @@ public class LdapProxyBackendTest {
         }
     }
 
-    @Before
-    public void setUp() throws Exception {
-        ldapProxyBackend = new LdapProxyBackend();
-    }
-
     @After
     public void tearDown() throws Exception {
-        ldapProxyBackend.close();
+        if (ldapProxyBackend != null) {
+            ldapProxyBackend.close();
+        }
     }
 
     @Test
     public void testGetUserByDefaultUserSearchFilter() throws Exception {
         // default searches by uid and uses group search for membership
-        ldapProxyBackend.initialize(ldapBackendConfig);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
 
         Entry entry = ldapProxyBackend.getUser("ldaptest1", schemaManager);
         validateUserEntry(entry, "ldaptest1", "TestCn1", 
"[email protected]", "Test user ldaptest1");
@@ -161,7 +157,7 @@ public class LdapProxyBackendTest {
 
     @Test
     public void testGetUserNotFound() throws Exception {
-        ldapProxyBackend.initialize(ldapBackendConfig);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
 
         Entry entry = ldapProxyBackend.getUser("nouser", schemaManager);
         assertNull(entry);
@@ -170,7 +166,7 @@ public class LdapProxyBackendTest {
     @Test
     public void testGetUserByUID() throws Exception {
         Map<String, String> config = createConfigWithUserAttr("uid");
-        ldapProxyBackend.initialize(config);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
         Entry entry = ldapProxyBackend.getUser("ldaptest1", schemaManager);
         validateUserEntry(entry, "ldaptest1", "TestCn1", 
"[email protected]", "Test user ldaptest1");
@@ -182,7 +178,7 @@ public class LdapProxyBackendTest {
     @Test
     public void testGetUserByCN() throws Exception {
         Map<String, String> config = createConfigWithUserAttr("cn");
-        ldapProxyBackend.initialize(config);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
         Entry entry = ldapProxyBackend.getUser("TestCn1", schemaManager);
         validateUserEntry(entry, "TestCn1", "TestCn1", 
"[email protected]", "Test user ldaptest1");
@@ -194,7 +190,7 @@ public class LdapProxyBackendTest {
     @Test
     public void testGetUserBySAMAccountName() throws Exception {
         Map<String, String> config = 
createConfigWithUserAttr("sAMAccountName");
-        ldapProxyBackend.initialize(config);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
         Entry entry = ldapProxyBackend.getUser("TestSam1", schemaManager);
         validateUserEntry(entry, "TestSam1", "TestCn1", 
"[email protected]", "Test user ldaptest1");
@@ -208,7 +204,7 @@ public class LdapProxyBackendTest {
     public void testGetUserUseMemberOf() throws Exception {
         Map<String, String> config = new HashMap<>(ldapBackendConfig);
         config.put("useMemberOf", "true");
-        ldapProxyBackend.initialize(config);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
         Entry entry = ldapProxyBackend.getUser("ldaptest2", schemaManager);
         validateUserEntry(entry, "ldaptest2", "TestCn2", 
"[email protected]", "Test user ldaptest2");
@@ -219,7 +215,7 @@ public class LdapProxyBackendTest {
 
     @Test
     public void testGetUserGroups() throws Exception {
-        ldapProxyBackend.initialize(ldapBackendConfig);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
 
         List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest1");
         assertTrue(userGroups.contains("group1"));
@@ -228,7 +224,7 @@ public class LdapProxyBackendTest {
 
     @Test
     public void testGetUserGroupsNoGroups() throws Exception {
-        ldapProxyBackend.initialize(ldapBackendConfig);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
 
         List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest2");
         assertTrue(userGroups.isEmpty());
@@ -236,7 +232,7 @@ public class LdapProxyBackendTest {
 
     @Test
     public void testGetUserGroupsNoUser() throws Exception {
-        ldapProxyBackend.initialize(ldapBackendConfig);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
 
         List<String> userGroups = ldapProxyBackend.getUserGroups("nobody");
         assertTrue(userGroups.isEmpty());
@@ -246,7 +242,7 @@ public class LdapProxyBackendTest {
     public void testGetUserGroupsUseMemberOf() throws Exception {
         Map<String, String> config = new HashMap<>(ldapBackendConfig);
         config.put("useMemberOf", "true");
-        ldapProxyBackend.initialize(config);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
         List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest2");
         assertTrue(userGroups.contains("groupMemberOf1"));
@@ -257,7 +253,7 @@ public class LdapProxyBackendTest {
     public void testGetUserGroupsUseMemberOfNoGroups() throws Exception {
         Map<String, String> config = new HashMap<>(ldapBackendConfig);
         config.put("useMemberOf", "true");
-        ldapProxyBackend.initialize(config);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
         List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest1");
         assertTrue(userGroups.isEmpty());
@@ -267,7 +263,7 @@ public class LdapProxyBackendTest {
     public void testGetUserGroupsUseMemberOfNoUser() throws Exception {
         Map<String, String> config = new HashMap<>(ldapBackendConfig);
         config.put("useMemberOf", "true");
-        ldapProxyBackend.initialize(config);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
         List<String> userGroups = ldapProxyBackend.getUserGroups("nobody");
         assertTrue(userGroups.isEmpty());
@@ -275,19 +271,19 @@ public class LdapProxyBackendTest {
 
     @Test
     public void testSearchUsers() throws Exception {
-        ldapProxyBackend.initialize(ldapBackendConfig);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
         validateUserSearch("*", 3, Set.of("ldaptest1", "ldaptest2", "guest"));
     }
 
     @Test
     public void testSearchUsersPartial() throws Exception {
-        ldapProxyBackend.initialize(ldapBackendConfig);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
         validateUserSearch("ldap*", 2, Set.of("ldaptest1", "ldaptest2"));
     }
 
     @Test
     public void testSearchUsersNoneFound() throws Exception {
-        ldapProxyBackend.initialize(ldapBackendConfig);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
         List<Entry> entries = ldapProxyBackend.searchUsers("nobody*", 
schemaManager);
         assertTrue(entries.isEmpty());
     }
@@ -295,21 +291,21 @@ public class LdapProxyBackendTest {
     @Test
     public void testSearchUsersByCn() throws Exception {
         Map<String, String> config = createConfigWithUserAttr("cn");
-        ldapProxyBackend.initialize(config);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
         validateUserSearch("*", 3, Set.of("TestCn1", "TestCn2", "Guest"));
     }
 
     @Test
     public void testSearchUsersPartialByCn() throws Exception {
         Map<String, String> config = createConfigWithUserAttr("cn");
-        ldapProxyBackend.initialize(config);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
         validateUserSearch("TestCn*", 2, Set.of("TestCn1", "TestCn2"));
     }
 
     @Test
     public void testSearchUsersNoneFoundByCn() throws Exception {
         Map<String, String> config = createConfigWithUserAttr("cn");
-        ldapProxyBackend.initialize(config);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
         List<Entry> entries = ldapProxyBackend.searchUsers("nobody*", 
schemaManager);
         assertTrue(entries.isEmpty());
     }
@@ -317,21 +313,21 @@ public class LdapProxyBackendTest {
     @Test
     public void testSearchUsersBySAMAccountName() throws Exception {
         Map<String, String> config = 
createConfigWithUserAttr("sAMAccountName");
-        ldapProxyBackend.initialize(config);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
         validateUserSearch("*", 2, Set.of("TestSam1", "TestSam2"));
     }
 
     @Test
     public void testSearchUsersPartialBySAMAccountName() throws Exception {
         Map<String, String> config = 
createConfigWithUserAttr("sAMAccountName");
-        ldapProxyBackend.initialize(config);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
         validateUserSearch("TestSam*", 2, Set.of("TestSam1", "TestSam2"));
     }
 
     @Test
     public void testSearchUsersNoneFoundBySAMAccountName() throws Exception {
         Map<String, String> config = 
createConfigWithUserAttr("sAMAccountName");
-        ldapProxyBackend.initialize(config);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
         List<Entry> entries = ldapProxyBackend.searchUsers("nobody*", 
schemaManager);
         assertTrue(entries.isEmpty());
     }
@@ -339,7 +335,7 @@ public class LdapProxyBackendTest {
     @Test
     public void testGetRecursiveUserGroupsDepth2() throws Exception {
         Map<String, String> config = createRecursiveConfig(2);
-        ldapProxyBackend.initialize(config);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
         List<String> userGroups = 
ldapProxyBackend.getUserGroups("recursiveUser");
         assertEquals(4, userGroups.size());
@@ -352,7 +348,7 @@ public class LdapProxyBackendTest {
     @Test
     public void testGetRecursiveUserGroupsDepth4() throws Exception {
         Map<String, String> config = createRecursiveConfig(4);
-        ldapProxyBackend.initialize(config);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
         List<String> userGroups = 
ldapProxyBackend.getUserGroups("recursiveUser");
         assertEquals(6, userGroups.size());
@@ -367,7 +363,7 @@ public class LdapProxyBackendTest {
     @Test
     public void testGetRecursiveUserGroupsWithCycle() throws Exception {
         Map<String, String> config = createRecursiveConfig(10);
-        ldapProxyBackend.initialize(config);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
         List<String> userGroups = 
ldapProxyBackend.getUserGroups("recursiveUser");
         assertTrue(userGroups.contains("cycleGroupA"));
@@ -377,7 +373,7 @@ public class LdapProxyBackendTest {
     @Test
     public void testGetUserRecursiveGroups() throws Exception {
         Map<String, String> config = createRecursiveConfig(5);
-        ldapProxyBackend.initialize(config);
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
         Entry entry = ldapProxyBackend.getUser("recursiveUser", schemaManager);
         validateMemberOf(entry, Set.of(
@@ -394,7 +390,7 @@ public class LdapProxyBackendTest {
         Map<String, String> config = createRecursiveConfig(5);
 
         final AtomicInteger cacheHits = new AtomicInteger(0);
-        ldapProxyBackend = new LdapProxyBackend() {
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config) {
             @Override
             protected Map<String, Set<Entry>> createResolvedParentsCache() {
                 return new HashMap<>() {
@@ -408,7 +404,6 @@ public class LdapProxyBackendTest {
                 };
             }
         };
-        ldapProxyBackend.initialize(config);
 
         // Search for all recursive users (recursiveUser and recursiveUser2)
         // They share level1Group, cycleGroupA, and all their ancestors.
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
new file mode 100644
index 000000000..828937074
--- /dev/null
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+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.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.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.filtering.EntryFilteringCursorImpl;
+import org.apache.directory.server.core.api.interceptor.BaseInterceptor;
+import 
org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
+import org.apache.knox.gateway.security.ldap.SimpleDirectoryService;
+import org.apache.knox.gateway.services.ldap.SchemaManagerFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+/**
+ * Unit tests for DuplicateUserFilteringInterceptor.
+ */
+public class DuplicateUserFilteringInterceptorTest {
+
+    private static final String TEST_INTERCEPTOR = "TEST";
+    private static final String NEXT_INTERCEPTOR = "NEXT";
+
+    private DuplicateUserFilteringInterceptor interceptor;
+
+    private DirectoryService directoryService;
+    private SchemaManager schemaManager;
+    private ConfigurableEntriesTestInterceptor nextInterceptor;
+    private SearchOperationContext ctx;
+    private CoreSession session;
+
+    @Before
+    public void setUp() throws Exception {
+        directoryService = new SimpleDirectoryService();
+        directoryService.setShutdownHookEnabled(false);
+        schemaManager = SchemaManagerFactory.createSchemaManager();
+        directoryService.setSchemaManager(schemaManager);
+
+        interceptor = new DuplicateUserFilteringInterceptor(TEST_INTERCEPTOR);
+        interceptor.init(directoryService);
+        directoryService.addLast(interceptor);
+
+        nextInterceptor = new 
ConfigurableEntriesTestInterceptor(NEXT_INTERCEPTOR);
+        nextInterceptor.init(directoryService);
+        directoryService.addLast(nextInterceptor);
+
+        session = directoryService.getSession();
+
+        ctx = new SearchOperationContext(session);
+        ctx.setInterceptors(List.of(TEST_INTERCEPTOR, NEXT_INTERCEPTOR));
+
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        directoryService.shutdown();
+    }
+
+    @Test
+    public void testEmptyCursor() throws Exception {
+        nextInterceptor.setEntries(List.of());
+
+        try (EntryFilteringCursor results = interceptor.search(ctx)) {
+            assertFalse("Results should be empty", results.next());
+        }
+
+        EntryFilteringCursor nextInterceptorCursor = 
nextInterceptor.getCursor();
+        assertTrue("Cursor must be closed", nextInterceptorCursor.isClosed());
+    }
+
+    @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));
+
+        try (EntryFilteringCursor results = interceptor.search(ctx)) {
+            assertNextEntryUid(results, "user1");
+            assertNextEntryUid(results, "user2");
+            assertFalse("No more entries expected", results.next());
+        }
+
+        EntryFilteringCursor nextInterceptorCursor = 
nextInterceptor.getCursor();
+        assertTrue("Cursor must be closed", nextInterceptorCursor.isClosed());
+    }
+
+    @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));
+
+        try (EntryFilteringCursor results = interceptor.search(ctx)) {
+            assertNextEntryUid(results, "user1");
+            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());
+    }
+
+    private static class ConfigurableEntriesTestInterceptor extends 
BaseInterceptor {
+        private List<Entry> entries;
+        private EntryFilteringCursor cursor;
+
+        ConfigurableEntriesTestInterceptor(String name) {
+            super(name);
+        }
+
+        public void setEntries(List<Entry> entries) {
+            this.entries = entries;
+        }
+
+        public EntryFilteringCursor getCursor() {
+            return cursor;
+        }
+
+        @Override
+        public EntryFilteringCursor search(SearchOperationContext 
searchContext) throws LdapException {
+            cursor = new EntryFilteringCursorImpl(new ListCursor<>(entries), 
searchContext, schemaManager);
+            return cursor;
+        }
+    }
+}
\ No newline at end of file
diff --git 
a/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
 
b/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
index 80ab4e0c8..745002ae3 100644
--- 
a/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
+++ 
b/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
@@ -1246,8 +1246,8 @@ public class GatewayTestConfig extends Configuration 
implements GatewayConfig {
   }
 
   @Override
-  public String getLDAPBackendType() {
-    return "file";
+  public List<String> getLDAPInterceptorNames() {
+    return List.of("testinterceptor");
   }
 
   @Override
@@ -1261,7 +1261,7 @@ public class GatewayTestConfig extends Configuration 
implements GatewayConfig {
   }
 
   @Override
-  public Map<String, String> getLDAPBackendConfig(String backendType) {
+  public Map<String, String> getLDAPInterceptorConfig(String backendType) {
     return Collections.emptyMap();
   }
 
diff --git 
a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java 
b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
index 6116a3d4c..7ecb2980d 100644
--- 
a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
+++ 
b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
@@ -128,7 +128,7 @@ public interface GatewayConfig {
   String LDAP_ENABLED = "gateway.ldap.enabled";
   String LDAP_PORT = "gateway.ldap.port";
   String LDAP_BASE_DN = "gateway.ldap.base.dn";
-  String LDAP_BACKEND_TYPE = "gateway.ldap.backend.type";
+  String LDAP_INTERCEPTOR_NAMES = "gateway.ldap.interceptor.names";
   String LDAP_BACKEND_DATA_FILE = "gateway.ldap.backend.data.file";
   String LDAP_RECURSIVE_GROUP_RESOLUTION = 
"gateway.ldap.recursive.group.resolution";
   String LDAP_RECURSIVE_GROUP_RESOLUTION_MAX_DEPTH = 
"gateway.ldap.recursive.group.resolution.max.depth";
@@ -1069,10 +1069,10 @@ public interface GatewayConfig {
    */
   String getLDAPBaseDN();
 
-  /**
-   * @return the backend type for LDAP (file, ldap, jdbc, etc.)
-   */
-  String getLDAPBackendType();
+    /**
+     * @return the list of interceptor names for LDAP server
+     */
+  List<String> getLDAPInterceptorNames();
 
   /**
    * @return the path to the data file for file-based backend
@@ -1080,14 +1080,14 @@ public interface GatewayConfig {
   String getLDAPBackendDataFile();
 
   /**
-   * Get backend-specific configuration properties.
-   * Returns all properties with prefix "gateway.ldap.backend.{backendType}."
+   * Get interceptor-specific configuration properties.
+   * Returns all properties with prefix 
"gateway.ldap.interceptor.{interceptorName}."
    * with the prefix stripped from the keys.
    *
-   * @param backendType the backend type (e.g., "file", "ldap", "database")
+   * @param interceptor the interceptor name
    * @return map of configuration key-value pairs for the specified backend
    */
-  Map<String, String> getLDAPBackendConfig(String backendType);
+  Map<String, String> getLDAPInterceptorConfig(String interceptor);
 
   /**
    * @return true if recursive group resolution is enabled for LDAP service
diff --git a/knox-site/docs/service_ldap_server.md 
b/knox-site/docs/service_ldap_server.md
index 51080f69a..1ad0f1ab0 100644
--- a/knox-site/docs/service_ldap_server.md
+++ b/knox-site/docs/service_ldap_server.md
@@ -7,7 +7,8 @@ The Knox LDAP Service provides an embedded LDAP server within 
the Knox Gateway.
 Introduced in [KNOX-3247](https://issues.apache.org/jira/browse/KNOX-3247), 
the Knox LDAP Service leverages Apache Directory Server (ApacheDS) to provide a 
lightweight, embedded LDAP server. Its primary goal is to provide a consistent 
LDAP interface for authentication and group lookups, even when the underlying 
identity store is not a traditional LDAP server or is a remote server that 
requires proxying.
 
 Key features include:
-- **Pluggable Backends**: Support for different data sources (JSON files, 
remote LDAP/AD).
+- **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`.
 
@@ -16,13 +17,13 @@ Key features include:
 The Knox LDAP Service is integrated as a core gateway service. It consists of 
the following components:
 
 1.  **KnoxLDAPServerManager**: Manages the lifecycle of the ApacheDS instance.
-2.  **GroupLookupInterceptor**: A custom ApacheDS interceptor that captures 
search requests. If an entry is not found in the local ApacheDS partitions, it 
delegates the lookup to the configured backend.
-3.  **LdapBackend**: A pluggable interface for fetching user and group data.
+2.  **UserLookupInterceptor**: A custom ApacheDS interceptor that captures 
search requests. If an entry is not found in the local ApacheDS partitions, it 
delegates the lookup to the configured backend.
+3.  **LdapBackend**: A pluggable interface for fetching user and group data. 
Implementations are included for reading users from a file or connecting to a 
remote LDAP server.
 4.  **SchemaManagerFactory**: Programmatically extends the ApacheDS schema to 
include AD-specific attributes like `sAMAccountName`.
 
 When a client performs an LDAP search:
 1.  The request hits the embedded ApacheDS server.
-2.  The `GroupLookupInterceptor` intercepts the search.
+2.  The `UserLookupInterceptor` intercepts the search.
 3.  The interceptor checks the results of the local search.
 4.  If not found, it queries the configured `LdapBackend`.
 5.  Results from the backend are converted into LDAP entries and returned to 
the client.
@@ -38,17 +39,39 @@ The service is configured in `gateway-site.xml`.
 | `gateway.ldap.enabled` | `false` | Enables or disables the embedded LDAP 
service. |
 | `gateway.ldap.port` | `3890` | The port on which the LDAP server listens. |
 | `gateway.ldap.base.dn` | `dc=proxy,dc=com` | The base DN for the LDAP 
server. |
-| `gateway.ldap.backend.type` | `file` | The type of backend to use (`file` or 
`proxy`). |
+| `gateway.ldap.interceptor.names` | N/A | A comma separated list of 
interceptors to use. A separate interceptor configuration block will be used 
for each name. |
+
+### Interceptor Types
+
+#### Common Interceptor Properties
+
+| Property | Default Value | Description |
+| :--- | :--- | :--- |
+| `gateway.ldap.interceptor.<name>.interceptorType` | N/A | The type of 
interceptor to use (`backend` or `duplicateuserfilter`). |
+
+#### Duplicate User Filter Interceptor (`backend`)
+
+The duplicate user filter interceptor ensures that each `Entry` has a unique 
`uid`. This interceptor removes entries that have a duplicate `uid`. 
+
+#### User Search 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.
 
 ### Backend Types
 
+#### Common Backend Properties
+
+| Property | Default Value | Description |
+| :--- | :--- | :--- |
+| `gateway.ldap.interceptor.<name>.backendType` | N/A | The type of backend to 
use (`file` or `ldap`). |
+
 #### File Backend (`file`)
 
 The file backend reads user and group definitions from a JSON file. This is 
ideal for testing or small, static environments.
 
 | Property | Default Value | Description |
 | :--- | :--- | :--- |
-| `gateway.ldap.backend.data.file` | `${GATEWAY_DATA_HOME}/ldap-users.json` | 
Path to the JSON data file. |
+| `gateway.ldap.interceptor.<name>.file` | 
`${GATEWAY_DATA_HOME}/ldap-users.json` | Path to the JSON data file. |
 
 **JSON File Format Example:**
 ```json
@@ -67,24 +90,24 @@ The file backend reads user and group definitions from a 
JSON file. This is idea
 }
 ```
 
-#### Proxy Backend (`proxy`)
+#### LDAP Proxy Backend (`ldap`)
 
 The proxy backend delegates lookups to a remote LDAP or Active Directory 
server.
 
 | Property | Default Value | Description |
 | :--- | :--- | :--- |
-| `gateway.ldap.backend.proxy.url` | N/A | Remote LDAP URL (e.g., 
`ldap://remote-host:389`). |
-| `gateway.ldap.backend.proxy.host` | N/A | Host of remote LDAP (used if `url` 
is not provided). |
-| `gateway.ldap.backend.proxy.port` | N/A | Port of remote LDAP (used if `url` 
is not provided). |
-| `gateway.ldap.backend.proxy.remoteBaseDn` | N/A | **Required**. The base DN 
of the remote LDAP server. |
-| `gateway.ldap.backend.proxy.systemUsername` | N/A | Bind DN for the remote 
server (alias: `bindDn`). |
-| `gateway.ldap.backend.proxy.systemPassword` | N/A | Password for the bind DN 
(alias: `bindPassword`). |
-| `gateway.ldap.backend.proxy.userSearchBase` | `ou=people,{remoteBaseDn}` | 
Base DN for user searches on the remote server. |
-| `gateway.ldap.backend.proxy.groupSearchBase` | `ou=groups,{remoteBaseDn}` | 
Base DN for group searches on the remote server. |
-| `gateway.ldap.backend.proxy.userIdentifierAttribute` | `uid` | Attribute 
used for user lookup (e.g., `sAMAccountName` for AD). |
-| `gateway.ldap.backend.proxy.groupMemberAttribute` | `memberUid` | Attribute 
used for group membership (e.g., `member` for AD). |
-| `gateway.ldap.backend.proxy.useMemberOf` | `false` | If `true`, use the 
`memberOf` attribute for efficient group lookups. |
-| `gateway.ldap.backend.proxy.poolMaxActive` | `8` | Maximum number of active 
connections in the pool. |
+| `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>.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>.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. |
 
 ## Active Directory (AD) Integration
 
@@ -95,11 +118,12 @@ The Knox LDAP Service includes several optimizations for 
working with Active Dir
 - **Schema Extensions**: AD-specific attributes (`memberOf`, `sAMAccountName`) 
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 (AD Proxy)
+## Usage Example
 
-To configure Knox to act as an LDAP proxy for an Active Directory server:
+To configure Knox to act as an LDAP proxy for a local file and an Active 
Directory server:
 
 ```xml
+<!-- LDAP Proxy Service Configuration -->
 <property>
     <name>gateway.ldap.enabled</name>
     <value>true</value>
@@ -109,37 +133,74 @@ To configure Knox to act as an LDAP proxy for an Active 
Directory server:
     <value>389</value>
 </property>
 <property>
-    <name>gateway.ldap.backend.type</name>
-    <value>proxy</value>
+    <name>gateway.ldap.base.dn</name>
+    <value>dc=proxy,dc=com</value>
+</property>
+
+<property>
+    <name>gateway.ldap.interceptor.names</name>
+    <value>localfile,adexample,duplicatefilter</value>
+</property>
+
+<!-- File-based LDAP backend -->
+<property>
+    <name>gateway.ldap.interceptor.localfile.interceptorType</name>
+    <value>backend</value>
+</property>
+<property>
+    <name>gateway.ldap.interceptor.localfile.backendType</name>
+    <value>file</value>
+</property>
+<property>
+    <name>gateway.ldap.interceptor.localfile.file</name>
+    <value>${GATEWAY_DATA_HOME}/ldap-users.json</value>
+</property>
+
+<!-- LDAP backend proxy configuration -->
+<property>
+    <name>gateway.ldap.interceptor.adexample.interceptorType</name>
+    <value>backend</value>
+</property>
+<property>
+    <name>gateway.ldap.interceptor.adexample.backendType</name>
+    <value>ldap</value>
 </property>
 <property>
-    <name>gateway.ldap.backend.proxy.url</name>
+    <name>gateway.ldap.interceptor.adexample.url</name>
     <value>ldap://ad.example.com:389</value>
 </property>
 <property>
-    <name>gateway.ldap.backend.proxy.remoteBaseDn</name>
+    <name>gateway.ldap.interceptor.adexample.remoteBaseDn</name>
     <value>dc=example,dc=com</value>
 </property>
 <property>
-    <name>gateway.ldap.backend.proxy.systemUsername</name>
-    <value>CN=ReadOnlyUser,CN=Users,DC=example,DC=com</value>
+    <name>gateway.ldap.interceptor.adexample.systemUsername</name>
+    <value>cn=ReadOnlyUser,cn=Users,dc=example,dc=com</value>
 </property>
 <property>
-    <name>gateway.ldap.backend.proxy.systemPassword</name>
-    <value>secret-password</value>
+    <name>gateway.ldap.interceptor.adexample.systemPassword</name>
+    <value>guest-password</value>
 </property>
 <property>
-    <name>gateway.ldap.backend.proxy.userIdentifierAttribute</name>
+    <name>gateway.ldap.interceptor.adexample.userIdentifierAttribute</name>
     <value>sAMAccountName</value>
 </property>
 <property>
-    <name>gateway.ldap.backend.proxy.groupMemberAttribute</name>
+    <name>gateway.ldap.interceptor.adexample.groupMemberAttribute</name>
     <value>member</value>
 </property>
 <property>
-    <name>gateway.ldap.backend.proxy.useMemberOf</name>
+    <name>gateway.ldap.interceptor.adexample.useMemberOf</name>
     <value>true</value>
 </property>
+
+<!-- Duplicate Filter Interceptor -->
+<property>
+    <name>gateway.ldap.interceptor.duplicatefilter.interceptorType</name>
+    <value>duplicateuserfilter</value>
+</property>
+
+<!-- End LDAP Proxy Service Configuration -->
 ```
 
 ## Troubleshooting

Reply via email to