diff --git a/docs/asciidoc/modules/ROOT/pages/database-integration/load-ldap.adoc b/docs/asciidoc/modules/ROOT/pages/database-integration/load-ldap.adoc index 99f8d71ac9..de7e7a8b77 100644 --- a/docs/asciidoc/modules/ROOT/pages/database-integration/load-ldap.adoc +++ b/docs/asciidoc/modules/ROOT/pages/database-integration/load-ldap.adoc @@ -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 @@ -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 diff --git a/extended/build.gradle b/extended/build.gradle index 7b4fc51b79..86f69aafb5 100644 --- a/extended/build.gradle +++ b/extended/build.gradle @@ -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' @@ -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 diff --git a/extended/src/main/java/apoc/load/LoadLdap.java b/extended/src/main/java/apoc/load/LoadLdap.java index 154490c0b0..af7e80558b 100644 --- a/extended/src/main/java/apoc/load/LoadLdap.java +++ b/extended/src/main/java/apoc/load/LoadLdap.java @@ -1,7 +1,12 @@ 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; @@ -9,10 +14,10 @@ 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; @@ -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"; @@ -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 attributeList; @@ -108,46 +114,76 @@ public LDAPManager(Map 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 executeSearch(Map search) { try { - Iterator> supplier = new SearchResultsIterator(doSearch(search), attributeList); - Spliterator> 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 search) { + private Map getMapFromEntry(SearchResultEntry entry, List attributes) { + Map 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 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) 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) { @@ -157,99 +193,29 @@ public LDAPSearchResults doSearch(Map 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> { - private final LDAPSearchResults lsr; - private final List attributes; - private Map map; - public SearchResultsIterator(LDAPSearchResults lsr, List attributes) { - this.lsr = lsr; - this.attributes = attributes; - this.map = get(); - } - - @Override - public boolean hasNext() { - return this.map != null; - } - - @Override - public Map next() { - Map current = this.map; - this.map = get(); - return current; - } - - public Map get() { - if (handleEndOfResults()) return null; - try { - Map 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 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; } } } diff --git a/extended/src/test/java/apoc/load/LoadLdapContainerTest.java b/extended/src/test/java/apoc/load/LoadLdapContainerTest.java new file mode 100644 index 0000000000..4b22de663e --- /dev/null +++ b/extended/src/test/java/apoc/load/LoadLdapContainerTest.java @@ -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 conn = Map.of("ldapHost", "localhost:" + port, + "loginDN", "cn=admin,dc=example,dc=org", + "loginPW", "admin", + "ssl", ssl); + + Map 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 entry = (Map) 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")); + }); + } +} diff --git a/extended/src/test/java/apoc/load/LoadLdapTest.java b/extended/src/test/java/apoc/load/LoadLdapTest.java index 46aaacbce9..3a6c3c803f 100644 --- a/extended/src/test/java/apoc/load/LoadLdapTest.java +++ b/extended/src/test/java/apoc/load/LoadLdapTest.java @@ -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; @@ -152,10 +152,12 @@ private void testLoadAssertionCommon(Map 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 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()); }