Skip to content

Commit 18ab6c5

Browse files
authored
KNOX-3277 : LDAP Proxy improvements for working with AD backend (#1177)
The LdapProxyBackend and GroupLookupInterceptors are updated to work with sAMAccountName in addition to uid and cn for user and group lookup. This involved adding the memberOf and sAMAccountName attribute types to the schema used by the proxy. Group retrieval during user search is fixed to respect the useMemberOf flag. Tests for the LdapProxyBackend were added.
1 parent da88e24 commit 18ab6c5

8 files changed

Lines changed: 682 additions & 29 deletions

File tree

gateway-server/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,5 +611,11 @@
611611
<artifactId>zookeeper-jute</artifactId>
612612
<scope>test</scope>
613613
</dependency>
614+
615+
<dependency>
616+
<groupId>org.apache.mina</groupId>
617+
<artifactId>mina-core</artifactId>
618+
<scope>test</scope>
619+
</dependency>
614620
</dependencies>
615621
</project>

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public class GroupLookupInterceptor extends BaseInterceptor {
4444
private LdapBackend backend;
4545
private static final Pattern UID_PATTERN = Pattern.compile(".*\\(uid=([^)]+)\\).*");
4646
private static final Pattern CN_PATTERN = Pattern.compile(".*\\(cn=([^)]+)\\).*");
47+
private static final Pattern SAMAACCOUNTNAME_PATTERN = Pattern.compile(".*\\(sAMAccountName=([^)]+)\\).*");
4748

4849
public GroupLookupInterceptor(DirectoryService directoryService, LdapBackend backend) {
4950
this.directoryService = directoryService;
@@ -101,6 +102,9 @@ public EntryFilteringCursor search(SearchOperationContext ctx) throws LdapExcept
101102
if (backendEntry != null) {
102103
// Return backend result directly without caching
103104
entries.add(backendEntry);
105+
LOG.ldapUserEntry(backendEntry.toString());
106+
} else {
107+
LOG.ldapUserNull(username);
104108
}
105109
}
106110
} catch (Exception e) {
@@ -127,7 +131,9 @@ public void bind(BindOperationContext ctx) {
127131
}
128132

129133
private boolean isUserSearch(String filter) {
130-
return UID_PATTERN.matcher(filter).matches() || CN_PATTERN.matcher(filter).matches();
134+
return UID_PATTERN.matcher(filter).matches()
135+
|| CN_PATTERN.matcher(filter).matches()
136+
|| SAMAACCOUNTNAME_PATTERN.matcher(filter).matches();
131137
}
132138

133139
private String extractUser(String filter) {
@@ -141,6 +147,11 @@ private String extractUser(String filter) {
141147
return cnMatcher.group(1);
142148
}
143149

150+
Matcher samaaccountnameMatcher = SAMAACCOUNTNAME_PATTERN.matcher(filter);
151+
if (samaaccountnameMatcher.matches()) {
152+
return samaaccountnameMatcher.group(1);
153+
}
154+
144155
return null;
145156
}
146157
}

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
import org.apache.directory.api.ldap.model.entry.Entry;
2222
import org.apache.directory.api.ldap.model.name.Dn;
2323
import org.apache.directory.api.ldap.model.schema.SchemaManager;
24-
import org.apache.directory.api.ldap.schema.loader.JarLdifSchemaLoader;
25-
import org.apache.directory.api.ldap.schema.manager.impl.DefaultSchemaManager;
2624
import org.apache.directory.server.core.DefaultDirectoryService;
2725
import org.apache.directory.server.core.api.DirectoryService;
2826
import org.apache.directory.server.core.api.InstanceLayout;
@@ -92,10 +90,8 @@ public void start() throws Exception {
9290
directoryService = new DefaultDirectoryService();
9391
directoryService.setInstanceLayout(new InstanceLayout(workDir));
9492

95-
// Create and load schema manager manually
96-
JarLdifSchemaLoader loader = new JarLdifSchemaLoader();
97-
SchemaManager schemaManager = new DefaultSchemaManager(loader);
98-
schemaManager.loadAllEnabled();
93+
// Create SchemaManager
94+
SchemaManager schemaManager = SchemaManagerFactory.createSchemaManager();
9995
directoryService.setSchemaManager(schemaManager);
10096

10197
// Initialize schema partition

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,16 @@ public interface LdapMessages {
8484
@Message(level = MessageLevel.INFO,
8585
text = "Cleaning up old lock file: {0}")
8686
void ldapCleaningLockFile(String lockFile);
87+
88+
@Message(level = MessageLevel.DEBUG,
89+
text = "Backend user found: {0}")
90+
void ldapUserEntry(String user);
91+
92+
@Message(level = MessageLevel.DEBUG,
93+
text = "Backend user not found: {0}")
94+
void ldapUserNull(String username);
95+
96+
@Message(level = MessageLevel.ERROR,
97+
text = "Failed to copy attribute: {0}")
98+
void ldapAttributeCopyError(@StackTrace(level = MessageLevel.DEBUG) Exception e);
8799
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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;
19+
20+
import org.apache.directory.api.ldap.model.exception.LdapException;
21+
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;
24+
import org.apache.directory.api.ldap.model.schema.ObjectClass;
25+
import org.apache.directory.api.ldap.model.schema.SchemaManager;
26+
import org.apache.directory.api.ldap.schema.loader.JarLdifSchemaLoader;
27+
import org.apache.directory.api.ldap.schema.manager.impl.DefaultSchemaManager;
28+
29+
import java.io.IOException;
30+
31+
/**
32+
* Factory class for creating SchemaManager instances.
33+
*/
34+
public class SchemaManagerFactory {
35+
36+
public static SchemaManager createSchemaManager() throws IOException, LdapException {
37+
// Create and load schema manager manually
38+
JarLdifSchemaLoader loader = new JarLdifSchemaLoader();
39+
SchemaManager schemaManager = new DefaultSchemaManager(loader);
40+
schemaManager.loadAllEnabled();
41+
42+
// Add Custom schemas
43+
AttributeType memberOfAttrType = new AttributeType("1.2.840.113556.1.2.102");
44+
memberOfAttrType.setNames(new String[]{"memberOf"});
45+
memberOfAttrType.setSchemaName("other");
46+
memberOfAttrType.setSyntax(new LdapSyntax("1.3.6.1.4.1.1466.115.121.1.12"));
47+
memberOfAttrType.setDescription("attribute specifies the distinguished names of the groups to which this object belongs");
48+
memberOfAttrType.setSingleValued(false);
49+
schemaManager.add(memberOfAttrType);
50+
51+
AttributeType sAMAccountNameAttrType = new AttributeType("1.2.840.113556.1.4.221");
52+
sAMAccountNameAttrType.setNames(new String[]{"sAMAccountName"});
53+
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();
64+
65+
return schemaManager;
66+
}
67+
}

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

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
import java.io.IOException;
3737
import java.util.ArrayList;
38+
import java.util.Collection;
3839
import java.util.Collections;
3940
import java.util.List;
4041
import java.util.Locale;
@@ -63,6 +64,14 @@ public class LdapProxyBackend implements LdapBackend {
6364
private String groupMemberAttribute = "memberUid"; // member for AD, memberUid for POSIX
6465
private boolean useMemberOf; // Use memberOf attribute for group lookup (efficient for AD)
6566

67+
private List<String> proxyEntryAttributeTypes = List.of(
68+
// "uid" will always be filled
69+
"cn",
70+
"dn",
71+
"mail",
72+
"description");
73+
private final String proxyEntryGroupMembershipAttributeType = "memberOf";
74+
6675
// Connection pool for efficient connection reuse
6776
private LdapConnectionPool connectionPool;
6877

@@ -120,7 +129,6 @@ public void initialize(Map<String, String> config) throws Exception {
120129

121130
// Configure attribute mappings for AD/LDAP compatibility
122131
userIdentifierAttribute = config.getOrDefault("userIdentifierAttribute", "uid");
123-
config.getOrDefault("userDnTemplate", "uid={username},ou=Users,{baseDn}");
124132
groupMemberAttribute = config.getOrDefault("groupMemberAttribute", "memberUid");
125133
useMemberOf = Boolean.parseBoolean(config.getOrDefault("useMemberOf", "false"));
126134

@@ -260,7 +268,6 @@ public Entry getUser(String username, SchemaManager schemaManager) throws Except
260268
cursor.close();
261269
return entry;
262270
}
263-
264271
cursor.close();
265272
return null;
266273
} finally {
@@ -284,7 +291,7 @@ public List<String> getUserGroups(String username) throws Exception {
284291
if (cursor.next()) {
285292
String userDn = cursor.get().getDn().toString();
286293
cursor.close();
287-
return getUserGroupsInternal(connection, userDn, username);
294+
return getCnsFromEntries(getUserGroupsInternal(connection, userDn, username));
288295
}
289296

290297
cursor.close();
@@ -333,8 +340,8 @@ private String extractGroupNameFromDn(String groupDn) {
333340
return null;
334341
}
335342

336-
private List<String> getUserGroupsInternal(LdapConnection connection, String userDn, String username) throws LdapException, CursorException, IOException {
337-
List<String> groups = new ArrayList<>();
343+
private List<Entry> getUserGroupsInternal(LdapConnection connection, String userDn, String username) throws LdapException, CursorException, IOException {
344+
List<Entry> groups = new ArrayList<>();
338345

339346
// Search for groups where user is a member - build filter based on configuration
340347
String filter;
@@ -356,26 +363,32 @@ private List<String> getUserGroupsInternal(LdapConnection connection, String use
356363
EntryCursor cursor = connection.search(groupSearchBase, filter, SearchScope.SUBTREE, "cn");
357364

358365
while (cursor.next()) {
359-
Entry groupEntry = cursor.get();
360-
Attribute cnAttr = groupEntry.get("cn");
361-
if (cnAttr != null) {
362-
groups.add(cnAttr.getString());
363-
}
366+
groups.add(cursor.get());
364367
}
365368

366369
cursor.close();
367370
return groups;
368371
}
369372

373+
private List<String> getCnsFromEntries(Collection<Entry> entries) throws LdapException {
374+
List<String> cns = new ArrayList<>();
375+
for (Entry entry : entries) {
376+
Attribute cnAttr = entry.get("cn");
377+
if (cnAttr != null) {
378+
cns.add(cnAttr.getString());
379+
}
380+
}
381+
return cns;
382+
}
383+
370384
@Override
371385
public List<Entry> searchUsers(String filter, SchemaManager schemaManager) throws Exception {
372386
List<Entry> results = new ArrayList<>();
373387
LdapConnection connection = null;
374388

375389
try {
376390
connection = getConnection();
377-
String searchValue = filter.contains("*") ? "*" : filter;
378-
String ldapFilter = "(" + userIdentifierAttribute + "=" + searchValue + ")";
391+
String ldapFilter = "(" + userIdentifierAttribute + "=" + filter.trim() + ")";
379392
EntryCursor cursor = connection.search(userSearchBase, ldapFilter, SearchScope.SUBTREE, "*");
380393

381394
while (cursor.next()) {
@@ -425,16 +438,17 @@ private Entry createProxyEntry(Entry sourceEntry, String username, LdapConnectio
425438
}
426439
}
427440

428-
copyAttribute(sourceEntry, entry, "cn");
429-
copyAttribute(sourceEntry, entry, "sn");
430-
copyAttribute(sourceEntry, entry, "mail");
431-
copyAttribute(sourceEntry, entry, "description");
432-
copyAttribute(sourceEntry, entry, "memberOf"); // Preserve group memberships
441+
for (String attributeType : proxyEntryAttributeTypes) {
442+
copyAttribute(sourceEntry, entry, attributeType);
443+
}
433444

434-
// Get user's groups
435-
List<String> groups = getUserGroupsInternal(connection, sourceEntry.getDn().toString(), username);
436-
if (!groups.isEmpty()) {
437-
entry.add("description", "Groups: " + String.join(", ", groups));
445+
if (useMemberOf) {
446+
copyAttribute(sourceEntry, entry, proxyEntryGroupMembershipAttributeType);
447+
} else {
448+
List<Entry> groups = getUserGroupsInternal(connection, sourceEntry.getDn().toString(), username);
449+
for (Entry groupEntry : groups) {
450+
entry.add(proxyEntryGroupMembershipAttributeType, groupEntry.getDn().getName());
451+
}
438452
}
439453

440454
return entry;
@@ -445,7 +459,12 @@ private void copyAttribute(Entry source, Entry target, String attributeName) thr
445459
if (attr != null) {
446460
// Copy all values of the attribute (important for multi-valued attributes like objectClass)
447461
for (org.apache.directory.api.ldap.model.entry.Value value : attr) {
448-
target.add(attributeName, value.getString());
462+
try {
463+
target.add(attributeName, value.getString());
464+
} catch (LdapException e) {
465+
LOG.ldapAttributeCopyError(e);
466+
throw e;
467+
}
449468
}
450469
}
451470
}

0 commit comments

Comments
 (0)