This is an automated email from the ASF dual-hosted git repository.
smolnar82 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git
The following commit(s) were added to refs/heads/master by this push:
new 1e257783b KNOX-3341: LDAP proxy general search - review fixes and
hardening (#1284)
1e257783b is described below
commit 1e257783be8e8b2186bf05dad6cb65c573d63031
Author: Sandor Molnar <[email protected]>
AuthorDate: Wed Jul 1 09:21:26 2026 +0200
KNOX-3341: LDAP proxy general search - review fixes and hardening (#1284)
---
.github/workflows/tests/requirements.txt | 3 +-
.../workflows/tests/test_knox_ldap_proxy_search.py | 95 ++++++++++++++++++++++
.../gateway/services/ldap/backend/FileBackend.java | 8 +-
.../services/ldap/backend/LdapProxyBackend.java | 18 ++--
.../ldap/backend/RemoteSchemaConverter.java | 91 +++++++++++++++------
.../ldap/interceptor/DisabledUserInterceptor.java | 2 -
.../ldap/interceptor/UserSearchInterceptor.java | 23 +++++-
.../ldap/backend/LdapProxyBackendTest.java | 27 ++++++
.../src/test/resources/ldap-recursive-test.ldif | 4 +-
knox-site/docs/service_ldap_server.md | 3 +-
10 files changed, 232 insertions(+), 42 deletions(-)
diff --git a/.github/workflows/tests/requirements.txt
b/.github/workflows/tests/requirements.txt
index 9f223fca6..7d2d5b5fd 100644
--- a/.github/workflows/tests/requirements.txt
+++ b/.github/workflows/tests/requirements.txt
@@ -1,3 +1,4 @@
requests==2.32.4
pytest==8.3.4
-pylint==4.0.5
\ No newline at end of file
+pylint==4.0.5
+ldap3==2.9.1
\ No newline at end of file
diff --git a/.github/workflows/tests/test_knox_ldap_proxy_search.py
b/.github/workflows/tests/test_knox_ldap_proxy_search.py
new file mode 100644
index 000000000..9d015551e
--- /dev/null
+++ b/.github/workflows/tests/test_knox_ldap_proxy_search.py
@@ -0,0 +1,95 @@
+# 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.
+
+"""Integration tests for general LDAP search through the embedded Knox LDAP
proxy.
+
+These exercise the LdapProxyBackend.search() path (KNOX-3341): clients can
query
+the embedded Knox LDAP service - which proxies to the demo LDAP backend - by
+objectClass, cn and uid (including wildcards), not just by a single uid lookup.
+"""
+
+from __future__ import annotations
+
+import os
+import unittest
+from urllib.parse import urlparse
+
+import ldap3
+
+# The embedded Knox LDAP service port (see gateway-site.xml:
gateway.ldap.port).
+KNOX_LDAP_PORT = 33390
+
+BASE_DN = "dc=hadoop,dc=apache,dc=org"
+PEOPLE_BASE = f"ou=people,{BASE_DN}"
+GROUPS_BASE = f"ou=groups,{BASE_DN}"
+
+# A valid backend user used to bind to the proxy before searching.
+BIND_DN = f"uid=guest,{PEOPLE_BASE}"
+BIND_PASSWORD = "guest-password"
+
+
+def knox_host() -> str:
+ """Derive the Knox host from KNOX_GATEWAY_URL (defaults to localhost)."""
+ url = os.environ.get("KNOX_GATEWAY_URL", "https://localhost:8443/")
+ return urlparse(url).hostname or "localhost"
+
+
+class TestKnoxLdapProxySearch(unittest.TestCase):
+ """Verify general search requests are proxied to the demo LDAP backend."""
+
+ def setUp(self) -> None:
+ server = ldap3.Server(knox_host(), port=KNOX_LDAP_PORT,
get_info=ldap3.NONE)
+ self.connection = ldap3.Connection(
+ server, user=BIND_DN, password=BIND_PASSWORD, auto_bind=True
+ )
+
+ def tearDown(self) -> None:
+ self.connection.unbind()
+
+ def rdn_values(self, base: str, ldap_filter: str) -> list[str]:
+ """Run a subtree search and return the leading RDN value of each
entry."""
+ self.connection.search(base, ldap_filter, search_scope=ldap3.SUBTREE)
+ values = []
+ for entry in self.connection.entries:
+ # entry_dn looks like "uid=guest,ou=people,..." or
"cn=level1,ou=groups,..."
+ first_rdn = entry.entry_dn.split(",", 1)[0]
+ values.append(first_rdn.split("=", 1)[1])
+ return values
+
+ def test_search_all_users_by_objectclass(self) -> None:
+ """All inetOrgPerson entries under ou=people are returned."""
+ users = self.rdn_values(PEOPLE_BASE, "(objectClass=inetOrgPerson)")
+ for expected in ("guest", "admin", "sam", "tom", "recursiveUser"):
+ self.assertIn(expected, users)
+
+ def test_search_all_groups_by_objectclass(self) -> None:
+ """All groupOfNames entries under ou=groups are returned."""
+ groups = self.rdn_values(GROUPS_BASE, "(objectClass=groupOfNames)")
+ for expected in ("analyst", "scientist", "admin", "level1", "level2",
"level3"):
+ self.assertIn(expected, groups)
+
+ def test_search_groups_by_cn_wildcard(self) -> None:
+ """A cn wildcard filter returns only the matching groups."""
+ groups = self.rdn_values(GROUPS_BASE, "(cn=level*)")
+ self.assertEqual({"level1", "level2", "level3"}, set(groups))
+
+ def test_search_user_by_uid(self) -> None:
+ """A single user can still be looked up by uid."""
+ users = self.rdn_values(PEOPLE_BASE, "(uid=sam)")
+ self.assertIn("sam", users)
+
+
+if __name__ == "__main__":
+ unittest.main()
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 9568f2471..872d1c764 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
@@ -153,7 +153,13 @@ public class FileBackend implements LdapBackend {
public List<Entry> searchUsers(String filter, SchemaManager schemaManager)
throws Exception {
List<Entry> results = new ArrayList<>();
- String userFilter = extractUser(filter).toLowerCase(Locale.ROOT);
+ final String extractedUser = extractUser(filter);
+ if (extractedUser == null) {
+ // The filter does not target a recognized user identifier
(uid/cn/sAMAccountName),
+ // so there is nothing for this user-only backend to match.
+ return results;
+ }
+ String userFilter = extractedUser.toLowerCase(Locale.ROOT);
// Simple filter matching - just check if username matches
for (String username : users.keySet()) {
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 2c8be212e..300acff39 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
@@ -399,7 +399,7 @@ public class LdapProxyBackend implements LdapBackend {
results.add(remoteSchemaConverter.convertRemoteEntryToProxyEntry(entry,
schemaManager));
}
} catch (LdapException e) {
- LOG.ldapAttributeCopyError(e);
+ LOG.ldapSearchFailed(remoteSearchBase, remoteFilter, e);
}
return results;
} finally {
@@ -502,14 +502,19 @@ public class LdapProxyBackend implements LdapBackend {
groupEntry = entryCache.get((groupDn));
} else {
try {
- groupEntry = connection.lookup(groupDn, "memberOf");
+ // Request "cn" alongside "memberOf" so the cached entry can
be resolved to a
+ // group name later (e.g. by getUserGroups); a memberOf-only
lookup omits it.
+ groupEntry = connection.lookup(groupDn, "cn", "memberOf");
} catch (LdapException e) {
- // assume group doesn't exist and has no parent groups
+ groupEntry = null;
+ }
+ if (groupEntry == null) {
+ // Entry not found or lookup failed — synthesise a skeleton so
cn is still known.
groupEntry = createSkeletonGroupEntry(groupDn);
}
entryCache.put(groupDn, groupEntry);
}
- Attribute memberOf = groupEntry.get("memberOf");
+ Attribute memberOf = groupEntry == null ? null :
groupEntry.get("memberOf");
if (memberOf != null) {
for (Value value : memberOf) {
parents.add(value.getNormalized());
@@ -683,7 +688,6 @@ public class LdapProxyBackend implements LdapBackend {
return null;
}
- // tODO reuse this method in recursive calls
private List<Entry> getUserGroupsInternal(LdapConnection connection, Dn...
dns) throws LdapException, CursorException, IOException {
List<Entry> groups = new ArrayList<>();
if (dns.length == 0) {
@@ -736,6 +740,10 @@ public class LdapProxyBackend implements LdapBackend {
Attribute cnAttr = entry.get("cn");
if (cnAttr != null) {
cns.add(cnAttr.getString());
+ } else if (entry.getDn() != null && entry.getDn().getRdn() !=
null) {
+ // Fall back to the CN carried in the DN when the entry was
fetched without the
+ // cn attribute, so resolved groups are not silently dropped
from the result.
+ cns.add(entry.getDn().getRdn().getValue());
}
}
return cns;
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/RemoteSchemaConverter.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/RemoteSchemaConverter.java
index bd55f58ba..a4a5f07f6 100644
---
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/RemoteSchemaConverter.java
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/RemoteSchemaConverter.java
@@ -28,9 +28,24 @@ import
org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.services.ldap.LdapMessages;
+import java.text.ParseException;
+import java.util.Locale;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
public class RemoteSchemaConverter {
private static final LdapMessages LOG =
MessagesFactory.get(LdapMessages.class);
+ // Credential-bearing attributes that must never be surfaced through the
proxy entry.
+ private static final Set<String> SENSITIVE_ATTRIBUTES = Set.of(
+ "userpassword", "unicodepwd", "userpkcs12");
+
+ // Attributes whose values are distinguished names and therefore need
remote->proxy
+ // DN rewriting. Other attribute values (mail, description, ...) are
copied verbatim.
+ private static final Set<String> DN_VALUED_ATTRIBUTES = Set.of(
+ "member", "uniquemember", "memberof", "manager", "owner",
"seealso");
+
// Proxy configuration
private final String proxyBaseDn; // Base DN for proxy entries (e.g.,
dc=proxy,dc=com)
private final String proxyUserSearchBase;
@@ -80,8 +95,12 @@ public class RemoteSchemaConverter {
Entry entry = new DefaultEntry(schemaManager);
entry.setDn(sourceEntry.getDn());
- // Copy all known AttributeTypes as-is from backend response
+ // Copy attributes from the backend response, skipping
credential-bearing ones so
+ // they are never exposed through the proxy.
for (Attribute attribute : sourceEntry.getAttributes()) {
+ if
(SENSITIVE_ATTRIBUTES.contains(attribute.getId().toLowerCase(Locale.ROOT))) {
+ continue;
+ }
copyAttribute(sourceEntry, entry, attribute.getId());
}
@@ -95,13 +114,15 @@ public class RemoteSchemaConverter {
// replace userObjectClass and groupObjectClass object classes
Attribute objectClassAttribute = sourceEntry.get("objectclass");
- if (objectClassAttribute.contains(remoteGroupObjectClass)) {
- entry.remove("objectclass", remoteGroupObjectClass);
- entry.add("objectclass", "groupofnames");
- }
- if (objectClassAttribute.contains(remoteUserObjectClass)) {
- entry.remove("objectclass", remoteUserObjectClass);
- entry.add("objectclass", "inetOrgPerson");
+ if (objectClassAttribute != null) {
+ if (objectClassAttribute.contains(remoteGroupObjectClass)) {
+ entry.remove("objectclass", remoteGroupObjectClass);
+ entry.add("objectclass", "groupofnames");
+ }
+ if (objectClassAttribute.contains(remoteUserObjectClass)) {
+ entry.remove("objectclass", remoteUserObjectClass);
+ entry.add("objectclass", "inetOrgPerson");
+ }
}
return entry;
@@ -112,9 +133,9 @@ public class RemoteSchemaConverter {
* @param filter the filter
* @param schemaManager the schema manager
* @return the converted filter
- * @throws Exception if the filter cannot be parsed
+ * @throws ParseException if the filter cannot be parsed
*/
- public String convertProxyFilterToRemoteFilter(String filter,
SchemaManager schemaManager) throws Exception {
+ public String convertProxyFilterToRemoteFilter(String filter,
SchemaManager schemaManager) throws ParseException {
FilterMappingVisitor filterMappingVisitor = new
FilterMappingVisitor(remoteUserIdentifierAttribute, remoteUserObjectClass,
remoteGroupObjectClass, schemaManager);
// Filter likely has already been annotated by other interceptors.
@@ -135,9 +156,12 @@ public class RemoteSchemaConverter {
public void copyAttribute(Entry source, Entry target, String
attributeName) {
final Attribute attribute = source.get(attributeName);
if (attribute != null) {
+ // Only rewrite DNs for DN-valued attributes; other values (e.g.
mail, description)
+ // are copied verbatim so they are not corrupted if they happen to
contain a base DN.
+ final boolean dnValued =
DN_VALUED_ATTRIBUTES.contains(attributeName.toLowerCase(Locale.ROOT));
// Copy all values of the attribute (important for multi-valued
attributes like objectClass)
for (Value value : attribute) {
- String valueString =
convertRemoteDnToProxyDn(value.toString());
+ String valueString = dnValued ?
convertRemoteDnToProxyDn(value.toString()) : value.toString();
if (!target.contains(attributeName, valueString)) {
try {
target.add(attributeName, valueString);
@@ -151,27 +175,44 @@ public class RemoteSchemaConverter {
/**
* Converts a remote dn string to a proxy dn string. This also works for
search base strings.
- * @param string the remote dn or search base
+ * @param attributeValue the remote dn or search base
* @return the proxy search base
*/
- public String convertRemoteDnToProxyDn(String string) {
- return string == null ?
- null :
- string.replaceAll("(?i)" + remoteGroupSearchBase,
proxyGroupSearchBase)
- .replaceAll("(?i)" + remoteUserSearchBase, proxyUserSearchBase)
- .replaceAll("(?i)" + remoteBaseDn, proxyBaseDn);
+ private String convertRemoteDnToProxyDn(String attributeValue) {
+ if (attributeValue == null) {
+ return null;
+ }
+ // Replace the most specific bases first; they contain the base DN as
a suffix.
+ String result = replaceIgnoreCase(attributeValue,
remoteGroupSearchBase, proxyGroupSearchBase);
+ result = replaceIgnoreCase(result, remoteUserSearchBase,
proxyUserSearchBase);
+ result = replaceIgnoreCase(result, remoteBaseDn, proxyBaseDn);
+ return result;
}
/**
* Converts a proxy dn string to a remote dn string. This also works for
search base strings.
- * @param string the proxy dn or search base
+ * @param attributeValue the proxy dn or search base
* @return the remote search base
*/
- public String convertProxyDnToRemoteDn(String string) {
- return string == null ?
- null :
- string.replaceAll("(?i)" + proxyGroupSearchBase,
remoteGroupSearchBase)
- .replaceAll("(?i)" + proxyUserSearchBase, remoteUserSearchBase)
- .replaceAll("(?i)" + proxyBaseDn, remoteBaseDn);
+ public String convertProxyDnToRemoteDn(String attributeValue) {
+ if (attributeValue == null) {
+ return null;
+ }
+ // Replace the most specific bases first; they contain the base DN as
a suffix.
+ String result = replaceIgnoreCase(attributeValue,
proxyGroupSearchBase, remoteGroupSearchBase);
+ result = replaceIgnoreCase(result, proxyUserSearchBase,
remoteUserSearchBase);
+ result = replaceIgnoreCase(result, proxyBaseDn, remoteBaseDn);
+ return result;
+ }
+
+ /**
+ * Case-insensitive literal replacement of all occurrences of {@code
search} with
+ * {@code replacement}. Both operands are treated as literals (not regular
expressions),
+ * so DNs containing regex metacharacters are handled safely.
+ */
+ private String replaceIgnoreCase(String input, String search, String
replacement) {
+ return Pattern.compile(Pattern.quote(search), Pattern.CASE_INSENSITIVE)
+ .matcher(input)
+ .replaceAll(Matcher.quoteReplacement(replacement));
}
}
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DisabledUserInterceptor.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DisabledUserInterceptor.java
index 2f05ad4df..c4ffc92f5 100644
---
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DisabledUserInterceptor.java
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DisabledUserInterceptor.java
@@ -71,8 +71,6 @@ public class DisabledUserInterceptor extends BaseInterceptor {
} catch (IOException e) {
// IOException would only occur after finishing iterating over
results
// we can ignore this exception and return the filtered entries
- } catch (Exception e) {
- throw e;
}
return new EntryFilteringCursorImpl(new ListCursor<>(filteredEntries),
ctx, schemaManager);
}
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java
index 014cd2bb8..1c9edb988 100644
---
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java
@@ -39,6 +39,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import static java.util.Locale.ROOT;
+
/**
* Interceptor for LDAP operations to proxy user searches to backends when not
found locally
*/
@@ -95,16 +97,29 @@ public class UserSearchInterceptor extends BaseInterceptor {
} catch (Exception e) {
// If we get an error or no results, try the backends
}
- try {
- entries.addAll(backend.search(baseDn, ctx.getScope(), filter,
schemaManager));
- } catch (Exception e) {
- LOG.ldapSearchFailed(baseDn, filter, e);
+
+ // Only forward to the backend when the search base is under the
backend's namespace.
+ // System/operational searches (ou=schema, cn=config, root-DSE) must
not be forwarded.
+ if (isUnderBackendBaseDn(baseDn)) {
+ try {
+ entries.addAll(backend.search(baseDn, ctx.getScope(), filter,
schemaManager));
+ } catch (Exception e) {
+ LOG.ldapSearchFailed(baseDn, filter, e);
+ }
}
// Return cursor with our results - use a simple approach
return new EntryFilteringCursorImpl(new ListCursor<>(entries), ctx,
schemaManager);
}
+ private boolean isUnderBackendBaseDn(String searchBase) {
+ final String backendBase = backend.getBaseDn();
+ if (searchBase == null || searchBase.isEmpty() || backendBase == null
|| backendBase.isEmpty()) {
+ return false;
+ }
+ return
searchBase.toLowerCase(ROOT).endsWith(backendBase.toLowerCase(ROOT));
+ }
+
@Override
public void bind(BindOperationContext ctx) throws LdapException {
LOG.ldapBind(ctx.getDn() != null ? ctx.getDn().toString() :
"anonymous");
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 acf8a786d..e814735f4 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
@@ -293,6 +293,33 @@ public class LdapProxyBackendTest {
assertTrue(userGroups.isEmpty());
}
+ @Test
+ public void testGetUserGroupsUseMemberOfRecursive() throws Exception {
+ Map<String, String> config = createRecursiveConfigForMemberOf(10);
+ config.put("useMemberOf", "true");
+ ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+
+ List<String> userGroups =
ldapProxyBackend.getUserGroups("memberOfUser", schemaManager);
+ assertTrue(userGroups.contains("memberOflevel1"));
+ assertTrue(userGroups.contains("memberOflevel2"));
+ assertTrue(userGroups.contains("memberOflevel3"));
+ assertTrue(userGroups.contains("memberOflevel4"));
+ assertTrue(userGroups.contains("memberOfCycleA"));
+ assertTrue(userGroups.contains("memberOfCycleB"));
+ }
+
+ @Test
+ public void testGetUserGroupsUseMemberOfRecursiveDepth2() throws Exception
{
+ Map<String, String> config = createRecursiveConfigForMemberOf(2);
+ config.put("useMemberOf", "true");
+ ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+
+ List<String> userGroups =
ldapProxyBackend.getUserGroups("memberOfUser", schemaManager);
+ assertTrue(userGroups.contains("memberOflevel1"));
+ assertTrue(userGroups.contains("memberOflevel2"));
+ assertFalse("Level 3 should not appear at max depth 2",
userGroups.contains("memberOflevel3"));
+ }
+
@Test
public void testSearchUsers() throws Exception {
ldapProxyBackend = new LdapProxyBackend("testbackend",
ldapBackendConfig);
diff --git a/gateway-server/src/test/resources/ldap-recursive-test.ldif
b/gateway-server/src/test/resources/ldap-recursive-test.ldif
index da8732759..c1ea55fd2 100644
--- a/gateway-server/src/test/resources/ldap-recursive-test.ldif
+++ b/gateway-server/src/test/resources/ldap-recursive-test.ldif
@@ -143,10 +143,10 @@ member:
uid=memberOfUser2,ou=recursiveMemberOfPeople,dc=hadoop,dc=apache,dc=org
dn: cn=memberOflevel2,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
objectclass:top
objectclass: groupofnames
-cn: memberOflevel2level2
+cn: memberOflevel2
memberOf:
cn=memberOflevel3,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
memberOf:
cn=memberOfCycleA,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
-member: cn=memberOflevel1,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org
+member: cn=memberOflevel1,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
# memberOf Level 3 Group (Member is Level 2)
dn: cn=memberOflevel3,ou=recursiveMemberOfGroups,dc=hadoop,dc=apache,dc=org
diff --git a/knox-site/docs/service_ldap_server.md
b/knox-site/docs/service_ldap_server.md
index 4dedf9468..63495195c 100644
--- a/knox-site/docs/service_ldap_server.md
+++ b/knox-site/docs/service_ldap_server.md
@@ -108,7 +108,7 @@ remains available with its default credentials.
| Property | Default Value | Description |
| :--- | :--- | :--- |
-| `gateway.ldap.interceptor.<name>.interceptorType` | N/A | The type of
interceptor to use (`backend` or `duplicateuserfilter`). |
+| `gateway.ldap.interceptor.<name>.interceptorType` | N/A | The type of
interceptor to use (`backend`, `duplicateuserfilter`, `disableduserfilter`, or
`rolesLookup`). |
#### User Search Interceptor (`backend`)
@@ -198,7 +198,6 @@ The proxy backend delegates lookups to a remote LDAP or
Active Directory server.
| `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.ldap.interceptor.<name>.proxy.poolMaxActive` | `8` | Maximum number
of active connections in the pool. |
-| `gateway.ldap.interceptor.<name>.proxy.poolMaxActive` | `8` | Maximum number
of active connections in the pool. |
## Active Directory (AD) Integration