Skip to content

Commit 8a1332b

Browse files
authored
KNOX-3341: LDAP proxy backend handles general searches (#1274)
This commit improves the behavior of the LdapProxyBackend to handle broader search requests such as retrieving all users, all groups, or filtering by attributes other than uid. A "search" method was added to the LdapBackend interface to support broader search requests. LdapProxyBackend implements this method by converting the search base, objectclass, and user identifier attribute from proxy values to values recognized by the remote LDAP backend. LdapProxyBackend.createProxyEntry was factored out into a new RemoteSchemaConverter class. This class also contains methods for converting the search filter and DNs. The conversions in the search filter are further supported by a new FilterMappingVisitor class which will traverse the ExprNode tree and replace values as needed. Result entries are likewise converted from the remote values to the proxy values. As part of the mapping, the AD 'userAccountControl' attribute type is mapped into the 'nsAccountLock' attribute type. A new DisabledUserInterceptor is implemented to perform this conversion. This interceptor can also be configured to remove disabled entries from the results. Group membership retrieval is modified to use 'getUserGroupEntries' as a common starting point then branch into separate codepaths for using 'memberOf' or not. FileBackend simply maps the filter back into user search.
1 parent d997844 commit 8a1332b

19 files changed

Lines changed: 1561 additions & 391 deletions

gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapMessages.java

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ public interface LdapMessages {
105105
text = "Loaded user from backend: {0}")
106106
void ldapUserLoaded(String username);
107107

108+
@Message(level = MessageLevel.ERROR,
109+
text = "LDAP Lookup failed: {0}, {1}")
110+
void ldapLookupFailed(String dn, @StackTrace(level = MessageLevel.DEBUG) Exception e);
111+
112+
@Message(level = MessageLevel.INFO,
113+
text = "AttributeType not found: {0}")
114+
void ldapAttributeTypeNotFound(String attribute);
115+
108116
@Message(level = MessageLevel.INFO,
109117
text = "Cleaning up old lock file: {0}")
110118
void ldapCleaningLockFile(String lockFile);
@@ -138,17 +146,20 @@ public interface LdapMessages {
138146
@Message(level = MessageLevel.DEBUG, text = "Recursive group search enabled: {0}, max depth: {1}")
139147
void ldapRecursiveGroupSearchConfig(boolean enabled, int maxDepth);
140148

141-
@Message(level = MessageLevel.DEBUG, text = "Recursive group search for user {0} found {1} group(s) ({2}) at depth {3}")
142-
void ldapRecursiveGroupSearchProgress(String user, int count, String groups, int depth);
149+
@Message(level = MessageLevel.DEBUG, text = "Recursive group search for entry {0} found {1} group(s) ({2}) at depth {3}")
150+
void ldapRecursiveGroupSearchProgress(String entryName, int count, String groups, int depth);
151+
152+
@Message(level = MessageLevel.DEBUG, text = "Recursive group search for entry {0} completed. Total groups found: {1}")
153+
void ldapRecursiveGroupSearchFinished(String entryName, int count);
143154

144-
@Message(level = MessageLevel.DEBUG, text = "Recursive group search for user {0} completed. Total groups found: {1}")
145-
void ldapRecursiveGroupSearchFinished(String user, int count);
155+
@Message(level = MessageLevel.WARN, text = "Recursive group search for entry {0} reached max depth {1}")
156+
void ldapRecursiveGroupSearchMaxDepthReached(String entryName, int maxDepth);
146157

147-
@Message(level = MessageLevel.WARN, text = "Recursive group search for user {0} reached max depth {1}")
148-
void ldapRecursiveGroupSearchMaxDepthReached(String user, int maxDepth);
158+
@Message(level = MessageLevel.DEBUG, text = "Cycle detected in recursive group search for entry {0} at group {1}")
159+
void ldapRecursiveGroupSearchCycleDetected(String entryName, String groupDn);
149160

150-
@Message(level = MessageLevel.DEBUG, text = "Cycle detected in recursive group search for user {0} at group {1}")
151-
void ldapRecursiveGroupSearchCycleDetected(String user, String groupDn);
161+
@Message(level = MessageLevel.WARN, text = "Expected entry for group {0} not found in entry cache")
162+
void ldapRecursiveGroupSearchExpectedGroupNotInCache(String groupDn);
152163

153164
@Message(level = MessageLevel.DEBUG, text = "Created skeleton group entry for {0} as actual group entry was not found in the backend")
154165
void ldapSkeletonGroupEntryCreated(String groupDn);

gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/SchemaManagerFactory.java

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@
1919

2020
import org.apache.directory.api.ldap.model.exception.LdapException;
2121
import org.apache.directory.api.ldap.model.schema.AttributeType;
22-
import org.apache.directory.api.ldap.model.schema.LdapSyntax;
23-
import org.apache.directory.api.ldap.model.schema.MatchingRule;
2422
import org.apache.directory.api.ldap.model.schema.ObjectClass;
2523
import org.apache.directory.api.ldap.model.schema.SchemaManager;
2624
import org.apache.directory.api.ldap.schema.loader.JarLdifSchemaLoader;
2725
import org.apache.directory.api.ldap.schema.manager.impl.DefaultSchemaManager;
2826

2927
import java.io.IOException;
28+
import java.util.ArrayList;
29+
import java.util.HashSet;
30+
import java.util.List;
31+
import java.util.Set;
3032

3133
/**
3234
* Factory class for creating SchemaManager instances.
@@ -39,29 +41,74 @@ public static SchemaManager createSchemaManager() throws IOException, LdapExcept
3941
SchemaManager schemaManager = new DefaultSchemaManager(loader);
4042
schemaManager.loadAllEnabled();
4143

42-
// Add Custom schemas
44+
List<AttributeType> userAttributeTypes = new ArrayList<>();
45+
List<AttributeType> groupAttributeTypes = new ArrayList<>();
46+
getCustomAttributeTypes(userAttributeTypes, groupAttributeTypes);
47+
getActiveDirectoryAttributeTypes(userAttributeTypes);
48+
49+
Set<AttributeType> allAttributeTypes = new HashSet<>();
50+
allAttributeTypes.addAll(userAttributeTypes);
51+
allAttributeTypes.addAll(groupAttributeTypes);
52+
for (AttributeType attributeType : allAttributeTypes) {
53+
schemaManager.add(attributeType);
54+
}
55+
56+
ObjectClass personObjectClass = schemaManager.lookupObjectClassRegistry("person");
57+
personObjectClass.unlock();
58+
for (AttributeType attributeType : userAttributeTypes) {
59+
personObjectClass.addMayAttributeTypes(attributeType);
60+
}
61+
personObjectClass.lock();
62+
63+
ObjectClass groupOfNamesObjectClass = schemaManager.lookupObjectClassRegistry("groupofnames");
64+
groupOfNamesObjectClass.unlock();
65+
for (AttributeType attributeType : groupAttributeTypes) {
66+
groupOfNamesObjectClass.addMayAttributeTypes(attributeType);
67+
}
68+
groupOfNamesObjectClass.lock();
69+
70+
return schemaManager;
71+
}
72+
73+
private static void getCustomAttributeTypes(List<AttributeType> userAttributes, List<AttributeType> groupAttributes) {
4374
AttributeType memberOfAttrType = new AttributeType("1.2.840.113556.1.2.102");
44-
memberOfAttrType.setNames(new String[]{"memberOf"});
75+
memberOfAttrType.setNames("memberOf");
4576
memberOfAttrType.setSchemaName("other");
46-
memberOfAttrType.setSyntax(new LdapSyntax("1.3.6.1.4.1.1466.115.121.1.12"));
77+
memberOfAttrType.setSyntaxOid("1.3.6.1.4.1.1466.115.121.1.12");
4778
memberOfAttrType.setDescription("attribute specifies the distinguished names of the groups to which this object belongs");
4879
memberOfAttrType.setSingleValued(false);
49-
schemaManager.add(memberOfAttrType);
80+
userAttributes.add(memberOfAttrType);
81+
groupAttributes.add(memberOfAttrType);
82+
83+
AttributeType nsAccountLockAttrType = new AttributeType("2.16.840.1.113730.3.1.610");
84+
nsAccountLockAttrType.setNames("nsAccountLock");
85+
nsAccountLockAttrType.setSchemaName("other");
86+
nsAccountLockAttrType.setSyntaxOid("1.3.6.1.4.1.1466.115.121.1.15");
87+
nsAccountLockAttrType.setEqualityOid("caseIgnoreMatch");
88+
nsAccountLockAttrType.setDescription("Operational attribute to administratively lock/inactivate accounts");
89+
nsAccountLockAttrType.setSingleValued(true);
90+
userAttributes.add(nsAccountLockAttrType);
91+
}
5092

93+
private static void getActiveDirectoryAttributeTypes(List<AttributeType> userAttributes) {
5194
AttributeType sAMAccountNameAttrType = new AttributeType("1.2.840.113556.1.4.221");
52-
sAMAccountNameAttrType.setNames(new String[]{"sAMAccountName"});
95+
sAMAccountNameAttrType.setNames("sAMAccountName");
5396
sAMAccountNameAttrType.setSchemaName("other");
54-
sAMAccountNameAttrType.setSyntax(new LdapSyntax("1.3.6.1.4.1.1466.115.121.1.15"));
55-
sAMAccountNameAttrType.setDescription("Microsoft sAMAccountName attribute for compatibility");
56-
sAMAccountNameAttrType.setEquality(new MatchingRule("caseignorematch"));
57-
sAMAccountNameAttrType.setSubstring(new MatchingRule("caseignoresubstringsmatch"));
58-
schemaManager.add(sAMAccountNameAttrType);
59-
60-
ObjectClass personObjectClass = schemaManager.lookupObjectClassRegistry("person");
61-
personObjectClass.unlock();
62-
personObjectClass.addMayAttributeTypes(memberOfAttrType, sAMAccountNameAttrType);
63-
personObjectClass.lock();
97+
sAMAccountNameAttrType.setSyntaxOid("1.3.6.1.4.1.1466.115.121.1.15");
98+
sAMAccountNameAttrType.setDescription("Microsoft Active Directory sAMAccountName attribute for compatibility");
99+
sAMAccountNameAttrType.setEqualityOid("2.5.13.2"); // caseignorematch
100+
sAMAccountNameAttrType.setSubstringOid("2.5.13.4"); // caseignoresubstringsmatch
101+
sAMAccountNameAttrType.setSingleValued(true);
102+
userAttributes.add(sAMAccountNameAttrType);
64103

65-
return schemaManager;
104+
AttributeType userAccountControlAttrType = new AttributeType("1.2.840.113556.1.4.8");
105+
userAccountControlAttrType.setNames("userAccountControl");
106+
userAccountControlAttrType.setSchemaName("other");
107+
userAccountControlAttrType.setSyntaxOid("1.3.6.1.4.1.1466.115.121.1.27");
108+
userAccountControlAttrType.setDescription("Microsoft Active Directory User Account Control attribute for compatibility");
109+
userAccountControlAttrType.setEqualityOid("2.5.13.14"); // integermatch
110+
userAccountControlAttrType.setOrderingOid("2.5.13.15"); // integerorderingmatch
111+
userAccountControlAttrType.setSingleValued(true);
112+
userAttributes.add(userAccountControlAttrType);
66113
}
67114
}

gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackend.java

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.gson.Gson;
2121
import org.apache.directory.api.ldap.model.entry.DefaultEntry;
2222
import org.apache.directory.api.ldap.model.entry.Entry;
23+
import org.apache.directory.api.ldap.model.message.SearchScope;
2324
import org.apache.directory.api.ldap.model.name.Dn;
2425
import org.apache.directory.api.ldap.model.schema.SchemaManager;
2526
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
@@ -33,14 +34,21 @@
3334
import java.util.Collections;
3435
import java.util.HashMap;
3536
import java.util.List;
37+
import java.util.Locale;
3638
import java.util.Map;
39+
import java.util.regex.Matcher;
40+
import java.util.regex.Pattern;
3741

3842
/**
3943
* File-based backend that reads user/group data from JSON
4044
*/
4145
public class FileBackend implements LdapBackend {
4246
private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class);
4347

48+
private static final Pattern UID_PATTERN = Pattern.compile(".*\\(uid=([^)]+)\\).*");
49+
private static final Pattern CN_PATTERN = Pattern.compile(".*\\(cn=([^)]+)\\).*");
50+
private static final Pattern SAMAACCOUNTNAME_PATTERN = Pattern.compile(".*\\(sAMAccountName=([^)]+)\\).*");
51+
4452
static final String TYPE = "file";
4553

4654
private Map<String, UserData> users = new HashMap<>();
@@ -136,7 +144,7 @@ public Entry getUser(String username, SchemaManager schemaManager) throws Except
136144
}
137145

138146
@Override
139-
public List<String> getUserGroups(String username) throws Exception {
147+
public List<String> getUserGroups(String username, SchemaManager schemaManager) throws Exception {
140148
UserData userData = users.get(username);
141149
return userData != null && userData.groups != null ? userData.groups : Collections.emptyList();
142150
}
@@ -145,9 +153,13 @@ public List<String> getUserGroups(String username) throws Exception {
145153
public List<Entry> searchUsers(String filter, SchemaManager schemaManager) throws Exception {
146154
List<Entry> results = new ArrayList<>();
147155

156+
String userFilter = extractUser(filter).toLowerCase(Locale.ROOT);
157+
148158
// Simple filter matching - just check if username matches
149159
for (String username : users.keySet()) {
150-
if (filter.contains("uid=" + username) || filter.contains("*")) {
160+
String usernameLowerCase = username.toLowerCase(Locale.ROOT);
161+
if (userFilter.equalsIgnoreCase(usernameLowerCase) ||
162+
(userFilter.contains("*") && userFilter.contains(usernameLowerCase))) {
151163
Entry entry = getUser(username, schemaManager);
152164
if (entry != null) {
153165
results.add(entry);
@@ -170,4 +182,28 @@ public boolean authenticate(Dn userDn, String password) {
170182

171183
return false;
172184
}
185+
186+
@Override
187+
public List<Entry> search(String searchBase, SearchScope searchScope, String filter, SchemaManager schemaManager) throws Exception {
188+
return searchUsers(filter, schemaManager);
189+
}
190+
191+
private String extractUser(String filter) {
192+
Matcher uidMatcher = UID_PATTERN.matcher(filter);
193+
if (uidMatcher.matches()) {
194+
return uidMatcher.group(1);
195+
}
196+
197+
Matcher cnMatcher = CN_PATTERN.matcher(filter);
198+
if (cnMatcher.matches()) {
199+
return cnMatcher.group(1);
200+
}
201+
202+
Matcher samaaccountnameMatcher = SAMAACCOUNTNAME_PATTERN.matcher(filter);
203+
if (samaaccountnameMatcher.matches()) {
204+
return samaaccountnameMatcher.group(1);
205+
}
206+
207+
return null;
208+
}
173209
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.knox.gateway.services.ldap.backend;
19+
20+
import org.apache.directory.api.ldap.model.entry.Value;
21+
import org.apache.directory.api.ldap.model.filter.BranchNode;
22+
import org.apache.directory.api.ldap.model.filter.ExprNode;
23+
import org.apache.directory.api.ldap.model.filter.FilterVisitor;
24+
import org.apache.directory.api.ldap.model.filter.LeafNode;
25+
import org.apache.directory.api.ldap.model.filter.SimpleNode;
26+
import org.apache.directory.api.ldap.model.schema.SchemaManager;
27+
28+
import java.util.ArrayList;
29+
import java.util.List;
30+
31+
/**
32+
* FilterVisitor that maps LDAP search filters from the proxy attributes to
33+
* remote attributes.
34+
*/
35+
public class FilterMappingVisitor implements FilterVisitor {
36+
37+
private final String userIdentifierAttribute;
38+
private final String userObjectClass;
39+
private final String groupObjectClass;
40+
private final SchemaManager schemaManager;
41+
42+
public FilterMappingVisitor(String userIdentifierAttribute, String userObjectClass, String groupObjectClass, SchemaManager schemaManager) {
43+
this.userIdentifierAttribute = userIdentifierAttribute;
44+
this.userObjectClass = userObjectClass;
45+
this.groupObjectClass = groupObjectClass;
46+
this.schemaManager = schemaManager;
47+
}
48+
49+
@Override
50+
public Object visit(ExprNode exprNode) {
51+
if (exprNode == null) {
52+
return null;
53+
}
54+
55+
if (exprNode.isLeaf()) {
56+
return handleLeafNode((LeafNode) exprNode);
57+
} else {
58+
return handleBranchNode((BranchNode) exprNode);
59+
}
60+
}
61+
62+
private Object handleLeafNode(LeafNode leafNode) {
63+
String currentAttribute = leafNode.getAttribute();
64+
65+
// Map the uid attribute search to the configured user identifier (e.g., sAMAccountName)
66+
if ("uid".equalsIgnoreCase(currentAttribute) && !"uid".equalsIgnoreCase(userIdentifierAttribute)) {
67+
leafNode.setAttribute(userIdentifierAttribute);
68+
leafNode.setAttributeType(schemaManager.getAttributeType(userIdentifierAttribute));
69+
}
70+
71+
// Map group or user object class values to the configured values
72+
if ("objectClass".equalsIgnoreCase(currentAttribute)) {
73+
if (leafNode instanceof SimpleNode) {
74+
SimpleNode valueNode = (SimpleNode) leafNode;
75+
Value currentValue = valueNode.getValue();
76+
if (currentValue != null) {
77+
if ("groupofnames".equalsIgnoreCase(currentValue.getString())) {
78+
valueNode.setValue(new Value(groupObjectClass));
79+
}
80+
if ("inetOrgPerson".equalsIgnoreCase(currentValue.getString())) {
81+
valueNode.setValue(new Value(userObjectClass));
82+
}
83+
}
84+
}
85+
}
86+
87+
return leafNode;
88+
}
89+
90+
private Object handleBranchNode(BranchNode branchNode) {
91+
// recursively call visit on all the children
92+
List<ExprNode> newChildren = new ArrayList<>();
93+
for (ExprNode child : branchNode.getChildren()) {
94+
Object newChild = child.accept(this);
95+
if (newChild instanceof ExprNode) {
96+
newChildren.add((ExprNode) newChild);
97+
}
98+
}
99+
branchNode.setChildren(newChildren);
100+
return branchNode;
101+
}
102+
103+
@Override
104+
public boolean canVisit(ExprNode exprNode) {
105+
return true;
106+
}
107+
108+
@Override
109+
public boolean isPrefix() {
110+
return true;
111+
}
112+
113+
@Override
114+
public List<ExprNode> getOrder(BranchNode branchNode, List<ExprNode> list) {
115+
return list;
116+
}
117+
}

gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackend.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.apache.knox.gateway.services.ldap.backend;
1919

2020
import org.apache.directory.api.ldap.model.entry.Entry;
21+
import org.apache.directory.api.ldap.model.message.SearchScope;
2122
import org.apache.directory.api.ldap.model.name.Dn;
2223
import org.apache.directory.api.ldap.model.schema.SchemaManager;
2324

@@ -59,9 +60,10 @@ public interface LdapBackend {
5960
/**
6061
* Get groups for a user
6162
* @param username The username
62-
* @return List of group names
63+
* @param schemaManager Schema manager for creating entries
64+
* @return List of group names or null if not found
6365
*/
64-
List<String> getUserGroups(String username) throws Exception;
66+
List<String> getUserGroups(String username, SchemaManager schemaManager) throws Exception;
6567

6668
/**
6769
* Search for users matching a filter
@@ -71,6 +73,16 @@ public interface LdapBackend {
7173
*/
7274
List<Entry> searchUsers(String filter, SchemaManager schemaManager) throws Exception;
7375

76+
/**
77+
* Search for entries matching a filter
78+
* @param searchBase The base DN for the search
79+
* @param searchScope The scope of the search
80+
* @param filter LDAP filter string (simplified)
81+
* @param schemaManager Schema manager for creating entries
82+
* @return List of matching entries
83+
*/
84+
List<Entry> search(String searchBase, SearchScope searchScope, String filter, SchemaManager schemaManager) throws Exception;
85+
7486
/**
7587
* Authenticate a user with password
7688
*

0 commit comments

Comments
 (0)