Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #3775: apoc.load.ldap doesn't work with SSL #4010

Merged
merged 1 commit into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
With 'apoc.load.ldap' you can execute queries on any LDAP v3 enabled directory, the results are turned into a streams of entries.
The entries can then be used to update or create graph structures.

Note this utility requires to have the link:https://mvnrepository.com/artifact/com.novell.ldap/jldap/2009-10-07[jldap] library to be placed the plugin directory.

[separator=¦,opts=header,cols="5,1m,1m"]
|===
¦Qualified Name¦Type¦Release
Expand All @@ -24,6 +22,7 @@ include::example$generated-documentation/apoc.load.ldap.adoc[]
|{connectionMap} | ldapHost | the ldapserver:port if port is omitted the default port 389 will be used
| | loginDN | This is the dn of the ldap server user who has read access on the ldap server
| | loginPW | This is the password used by the loginDN
| | ssl | Boolean which indicates whether to use SSL connection or not (default false)
|{searchMap} | searchBase | From this entry a search is executed
| | searchScope | SCOPE_ONE (one level) or
SCOPE_SUB (all sub levels) or
Expand Down
3 changes: 2 additions & 1 deletion extended/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ dependencies {

// These will be dependencies packaged with the .jar
implementation project(':common')
implementation group: 'com.novell.ldap', name: 'jldap', version: '2009-10-07'
implementation group: 'com.unboundid', name: 'unboundid-ldapsdk', version: '6.0.11'
implementation group: 'org.jsoup', name: 'jsoup', version: '1.15.3'
implementation group: 'com.opencsv', name: 'opencsv', version: '5.7.1'
implementation group: 'us.fatehi', name: 'schemacrawler', version: '15.04.01'
Expand Down Expand Up @@ -144,6 +144,7 @@ dependencies {
testImplementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-csv', version: '2.13.2'
testImplementation group: 'com.sun.mail', name: 'javax.mail', version: '1.6.0'
testImplementation group: 'org.postgresql', name: 'postgresql', version: '42.1.4'

testImplementation group: 'org.zapodot', name: 'embedded-ldap-junit', version: '0.9.0'
testImplementation group: 'org.mockito', name: 'mockito-core', version: '5.4.0'
testImplementation group: 'org.apache.parquet', name: 'parquet-hadoop', version: '1.13.1', withoutServers
Expand Down
180 changes: 73 additions & 107 deletions extended/src/main/java/apoc/load/LoadLdap.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package apoc.load;

import apoc.Extended;
import com.novell.ldap.*;
import apoc.util.Util;
import com.unboundid.ldap.sdk.*;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.util.ssl.SSLUtil;
import com.unboundid.util.ssl.TrustAllTrustManager;
import org.neo4j.logging.Log;
import org.neo4j.procedure.Context;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Mode;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.Procedure;

import java.io.UnsupportedEncodingException;
import javax.net.ssl.SSLSocketFactory;
import java.security.GeneralSecurityException;
import java.util.*;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static apoc.ApocConfig.apocConfig;

Expand Down Expand Up @@ -78,6 +83,7 @@ public static class LDAPManager {
private static final String LDAP_HOST_P = "ldapHost";
private static final String LDAP_LOGIN_DN_P = "loginDN";
private static final String LDAP_LOGIN_PW_P = "loginPW";
private static final String LDAP_SSL = "ssl";
private static final String SEARCH_BASE_P = "searchBase";
private static final String SEARCH_SCOPE_P = "searchScope";
private static final String SEARCH_FILTER_P = "searchFilter";
Expand All @@ -88,10 +94,10 @@ public static class LDAPManager {
private static final String SCOPE_SUB = "SCOPE_SUB";

private int ldapPort;
private int ldapVersion = LDAPConnection.LDAP_V3;
private String ldapHost;
private String loginDN;
private String password;
private boolean ssl;
private LDAPConnection lc;
private List<String> attributeList;

Expand All @@ -108,46 +114,76 @@ public LDAPManager(Map<String, Object> connParms) {

this.loginDN = (String) connParms.get(LDAP_LOGIN_DN_P);
this.password = (String) connParms.get(LDAP_LOGIN_PW_P);
this.ssl = Util.toBoolean(connParms.get(LDAP_SSL));
}

public Stream<LDAPResult> executeSearch(Map<String, Object> search) {
try {
Iterator<Map<String, Object>> supplier = new SearchResultsIterator(doSearch(search), attributeList);
Spliterator<Map<String, Object>> spliterator = Spliterators.spliteratorUnknownSize(supplier, Spliterator.ORDERED);
return StreamSupport.stream(spliterator, false).map(LDAPResult::new).onClose(() -> closeIt(lc));
return doSearch(search).getSearchEntries()
.stream()
.map(i -> getMapFromEntry(i, attributeList))
.map(LDAPResult::new)
.onClose(() -> closeIt(lc));
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public LDAPSearchResults doSearch(Map<String, Object> search) {
private Map<String, Object> getMapFromEntry(SearchResultEntry entry, List<String> attributes) {
Map<String, Object> map = new LinkedHashMap<>(attributes.size() + 1);
map.put("dn", entry.getDN());

if (attributes.isEmpty()) {
entry.getAttributes()
.forEach(i -> {
Object value = readValue(i);
map.put(i.getName(), value);
});
} else {
for (String attribute : attributes) {
Object value = readValue(entry.getAttribute(attribute));
if (value != null) map.put(attribute, value);
}
}

return map;
}

private Object readValue(Attribute att) {
if (att == null) return null;
if (att.size() == 1) {
// single value
// for now everything is string
return att.getValue();
} else {
return att.getValues();
}
}

public SearchResult doSearch(Map<String, Object> search) {
// parse search parameters
String searchBase = (String) search.get(SEARCH_BASE_P);
String searchFilter = (String) search.get(SEARCH_FILTER_P);
String searchFilter = (String) search.getOrDefault(SEARCH_FILTER_P, "(objectClass=*)");
String sScope = (String) search.get(SEARCH_SCOPE_P);
attributeList = (List<String>) search.get(SEARCH_ATTRIBUTES_P);
if (attributeList == null) attributeList = new ArrayList<>();
int searchScope = LDAPConnection.SCOPE_SUB;
if (sScope.equals(SCOPE_BASE)) {
searchScope = LDAPConnection.SCOPE_BASE;
} else if (sScope.equals(SCOPE_ONE)) {
searchScope = LDAPConnection.SCOPE_ONE;
} else if (sScope.equals(SCOPE_SUB)) {
searchScope = LDAPConnection.SCOPE_SUB;
} else {
throw new RuntimeException("Invalid scope:" + sScope + ". value scopes are SCOPE_BASE, SCOPE_ONE and SCOPE_SUB");
}

int searchScope = switch (sScope) {
case SCOPE_BASE -> SearchScope.BASE_INT_VALUE;
case SCOPE_ONE -> SearchScope.ONE_INT_VALUE;
case SCOPE_SUB -> SearchScope.SUB_INT_VALUE;
default -> throw new RuntimeException("Invalid scope:" + sScope + ". value scopes are SCOPE_BASE, SCOPE_ONE and SCOPE_SUB");
};
// getting an ldap connection
try {
lc = getConnection();
// execute query
LDAPSearchConstraints cons = new LDAPSearchConstraints();
cons.setMaxResults(0); // no limit
LDAPSearchResults searchResults = null;
if (attributeList == null || attributeList.size() == 0) {
searchResults = lc.search(searchBase, searchScope, searchFilter, null, false, cons);
SearchResult searchResults;
SearchScope scope = SearchScope.valueOf(searchScope);
if (attributeList.isEmpty()) {
searchResults = lc.search(searchBase, scope, searchFilter);
} else {
searchResults = lc.search(searchBase, searchScope, searchFilter, attributeList.toArray(new String[0]), false, cons);
searchResults = lc.search(searchBase, scope, searchFilter, attributeList.toArray(new String[0]));
}
return searchResults;
} catch (Exception e) {
Expand All @@ -157,99 +193,29 @@ public LDAPSearchResults doSearch(Map<String, Object> search) {

public static void closeIt(LDAPConnection lc) {
try {
lc.disconnect();
lc.close();
} catch (Exception e) {
// ignore
}
}

private LDAPConnection getConnection() throws LDAPException, UnsupportedEncodingException {
// LDAPSocketFactory ssf;
// Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
// String path ="C:\\j2sdk1.4.2_09\\jre\\lib\\security\\cacerts";
//op("the trustStore: " + System.getProperty("javax.net.ssl.trustStore"));
// System.setProperty("javax.net.ssl.trustStore", path);
// op(" reading the strustStore: " + System.getProperty("javax.net.ssl.trustStore"));
// ssf = new LDAPJSSESecureSocketFactory();
// LDAPConnection.setSocketFactory(ssf);

private LDAPConnection getConnection() throws GeneralSecurityException, LDAPException {

LDAPConnection lc = new LDAPConnection();
SSLSocketFactory socketFactory = getSocketFactory();
lc = new LDAPConnection(socketFactory);

lc.connect(ldapHost, ldapPort);

// bind to the server
lc.bind(ldapVersion, loginDN, password.getBytes("UTF8"));
// tbd
// LDAPConnection pooling here?
//
lc.bind(loginDN, password);

return lc;
}

}
private static class SearchResultsIterator implements Iterator<Map<String, Object>> {
private final LDAPSearchResults lsr;
private final List<String> attributes;
private Map<String,Object> map;
public SearchResultsIterator(LDAPSearchResults lsr, List<String> attributes) {
this.lsr = lsr;
this.attributes = attributes;
this.map = get();
}

@Override
public boolean hasNext() {
return this.map != null;
}

@Override
public Map<String, Object> next() {
Map<String,Object> current = this.map;
this.map = get();
return current;
}

public Map<String, Object> get() {
if (handleEndOfResults()) return null;
try {
Map<String, Object> entry = new LinkedHashMap<>(attributes.size() + 1);
LDAPEntry en = null;
en = lsr.next();
entry.put("dn", en.getDN());
if (attributes != null && attributes.size() > 0) {
for (int col = 0; col < attributes.size(); col++) {
Object val = readValue(en.getAttributeSet().getAttribute(attributes.get(col)));
if (val != null) entry.put(attributes.get(col),val );
}
} else {
// make it dynamic
Iterator<LDAPAttribute> iter = en.getAttributeSet().iterator();
while (iter.hasNext()) {
LDAPAttribute attr = iter.next();
Object val = readValue(attr);
if (val != null) entry.put(attr.getName(), readValue(attr));
}
}
return entry;

} catch (LDAPException e) {
throw new RuntimeException("Error getting next ldap entry " + e.getLDAPErrorMessage());
}
}

private boolean handleEndOfResults() {
if (!lsr.hasMore()) {
return true;
}
return false;
}
private Object readValue(LDAPAttribute att) {
if (att == null) return null;
if (att.size() == 1) {
// single value
// for now everything is string
return att.getStringValue();
private SSLSocketFactory getSocketFactory() throws GeneralSecurityException {
if (ssl || ldapPort == 636) {
SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
return sslUtil.createSSLSocketFactory();
} else {
return att.getStringValueArray();
return null;
}
}
}
Expand Down
74 changes: 74 additions & 0 deletions extended/src/test/java/apoc/load/LoadLdapContainerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package apoc.load;

import apoc.util.TestUtil;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.neo4j.test.rule.DbmsRule;
import org.neo4j.test.rule.ImpermanentDbmsRule;
import org.testcontainers.containers.GenericContainer;

import java.util.Map;

import static apoc.util.TestUtil.testCall;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;

public class LoadLdapContainerTest {
private static final int LDAP_DEFAULT_PORT = 389;
private static final int LDAP_DEFAULT_SSL_PORT = 636;

private static GenericContainer ldap;

@ClassRule
public static DbmsRule db = new ImpermanentDbmsRule();

@BeforeClass
public static void beforeClass() {
ldap = new GenericContainer("osixia/openldap:1.5.0")
.withEnv("LDAP_TLS_VERIFY_CLIENT", "try")
.withExposedPorts(LDAP_DEFAULT_PORT, LDAP_DEFAULT_SSL_PORT);
ldap.start();
TestUtil.registerProcedure(db, LoadLdap.class);
}

@AfterClass
public static void tearDown() {
ldap.stop();
db.shutdown();
}

@Test
public void testLoadLDAPWithSSLPort() {
int port = ldap.getMappedPort(LDAP_DEFAULT_SSL_PORT);
testLoadLDAPCommon(port, true);
}

@Test
public void testLoadLDAPWithDefaultPort() {
int port = ldap.getMappedPort(LDAP_DEFAULT_PORT);
testLoadLDAPCommon(port, false);
}

private static void testLoadLDAPCommon(int port, boolean ssl) {
Map<String, Object> conn = Map.of("ldapHost", "localhost:" + port,
"loginDN", "cn=admin,dc=example,dc=org",
"loginPW", "admin",
"ssl", ssl);

Map<String, Object> searchBase = Map.of("searchBase", "dc=example,dc=org",
"searchScope", "SCOPE_BASE");
testCall(db, "call apoc.load.ldap($conn, $search)",
Map.of("conn", conn, "search", searchBase),
r -> {
Map<String, Object> entry = (Map<String, Object>) r.get("entry");

String[] expectedObjectClass = {"top", "dcObject", "organization"};
assertArrayEquals(expectedObjectClass, (String[]) entry.get("objectClass"));
assertEquals("dc=example,dc=org", entry.get("dn"));
assertEquals("Example Inc.", entry.get("o"));
assertEquals("example", entry.get("dc"));
});
}
}
12 changes: 7 additions & 5 deletions extended/src/test/java/apoc/load/LoadLdapTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

import apoc.util.FileUtils;
import apoc.util.TestUtil;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPSearchResults;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchResultEntry;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
Expand Down Expand Up @@ -152,10 +152,12 @@ private void testLoadAssertionCommon(Map<String, Object> r) {
public void testLoadLDAPConfig() throws Exception {
LoadLdap.LDAPManager mgr = new LoadLdap.LDAPManager(LoadLdap.getConnectionMap(connParams, null));

LDAPSearchResults results = mgr.doSearch(searchParams);
LDAPEntry le = results.next();
SearchResult results = mgr.doSearch(searchParams);
List<SearchResultEntry> searchEntries = results.getSearchEntries();
assertEquals(1, searchEntries.size());
SearchResultEntry le = searchEntries.get(0);
assertEquals("uid=training,dc=example,dc=com", le.getDN());
assertEquals("training", le.getAttribute("uid").getStringValue());
assertEquals("training", le.getAttribute("uid").getValue());

}

Expand Down
Loading