-
Notifications
You must be signed in to change notification settings - Fork 495
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
added apoc.load.ldap #537
added apoc.load.ldap #537
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
= Load LDAP | ||
|
||
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="1,1m,5"] | ||
|=== | ||
include::../build/generated-documentation/apoc.load.csv[lines=9..9] | ||
|=== | ||
|
||
=== parameters | ||
[options="header",cols="a,3m,a"] | ||
|=== | ||
|Parameter | Property | Description | ||
|{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 | ||
|{searchMap} | searchBase | From this entry a search is executed | ||
| | searchScope | SCOPE_ONE (one level) or | ||
SCOPE_SUB (all sub levels) or | ||
SCOPE_BASE (only the base node) | ||
| | searchFilter | Place here a standard ldap search filter for example: (objectClass=*) means that the ldap entry must have an objectClass attribute. | ||
| | attributes | optional. If omitted all the attributes of the entries will be returned. | ||
When specified only the specified attributes will be returned. Regardless the attributes setting a returned entry will always have a "dn" property. | ||
|=== | ||
|
||
==== load ldap example | ||
|
||
.Retrieve group member information from the ldap server | ||
[source,cypher] | ||
--- | ||
call apoc.load.ldap({ldapHost : "ldap.forumsys.com", loginDN : "cn=read-only-admin,dc=example,dc=com", loginPW : "password"}, | ||
{searchBase : "dc=example,dc=com",searchScope : "SCOPE_SUB" | ||
,attributes : ["uniqueMember","cn","uid","objectClass"] | ||
,searchFilter: "(&(objectClass=*)(uniqueMember=*))"}) yield entry | ||
return entry.dn, entry.uniqueMember | ||
--- | ||
[options="header",cols="3m,a"] | ||
|=== | ||
| entry.dn | entry.uniqueMember | | ||
| "ou=mathematicians,dc=example,dc=com" | ["uid=euclid,dc=example,dc=com", "uid=riemann,dc=example,dc=com", "uid=euler,dc=example,dc=com", "uid=gauss,dc=example,dc=com", "uid=test,dc=example,dc=com"] | | ||
| "ou=scientists,dc=example,dc=com" | ["uid=einstein,dc=example,dc=com", "uid=galieleo,dc=example,dc=com", "uid=tesla,dc=example,dc=com", "uid=newton,dc=example,dc=com", "uid=training,dc=example,dc=com", "uid=jmacy,dc=example,dc=com"] | | ||
| "ou=italians,ou=scientists,dc=example,dc=com" | "uid=tesla,dc=example,dc=com" | | ||
| "ou=chemists,dc=example,dc=com" | ["uid=curie,dc=example,dc=com", "uid=boyle,dc=example,dc=com", "uid=nobel,dc=example,dc=com", "uid=pasteur,dc=example,dc=com"] | | ||
|=== | ||
--- | ||
|
||
.Retrieve group member information from the ldap server and create structure in Neo4j | ||
[source,cypher] | ||
--- | ||
call apoc.load.ldap({ldapHost : "ldap.forumsys.com", loginDN : "cn=read-only-admin,dc=example,dc=com", loginPW : "password"}, | ||
{searchBase : "dc=example,dc=com",searchScope : "SCOPE_SUB" | ||
,attributes : ["uniqueMember","cn","uid","objectClass"] | ||
,searchFilter: "(&(objectClass=*)(uniqueMember=*))"}) yield entry | ||
merge (g:Group {dn : entry.dn}) | ||
on create set g.cn = entry.cn | ||
foreach (member in entry.uniqueMember | | ||
merge (p:Person { dn : member }) | ||
merge (p)-[:IS_MEMBER]->(g) | ||
) | ||
--- | ||
|
||
|
||
=== credentials | ||
|
||
To protect credentials, you can configure aliases in `conf/neo4j.conf`: | ||
|
||
---- | ||
apoc.loadldap.myldap.config=<host>:<port> <loginDN> <loginPW> | ||
---- | ||
|
||
|
||
==== example neo4j.conf: | ||
|
||
---- | ||
apoc.loadldap.myldap.config=ldap.forumsys.com:389 cn=read-only-admin,dc=example,dc=com password | ||
---- | ||
|
||
Then | ||
|
||
---- | ||
call apoc.load.ldap({ldapHost : "ldap.forumsys.com", loginDN : "cn=read-only-admin,dc=example,dc=com", loginPW : "password"} | ||
, {searchBase : "dc=example,dc=com" | ||
,searchScope : "SCOPE_SUB" | ||
,attributes : ["cn","uid","objectClass"] | ||
,searchFilter: "(&(objectClass=*))" | ||
}) yield entry | ||
return entry.dn, entry | ||
---- | ||
|
||
becomes | ||
|
||
---- | ||
call apoc.load.ldap("myldap" | ||
,{searchBase : "dc=example,dc=com" | ||
,searchScope : "SCOPE_SUB" | ||
,attributes : ["cn","uid","objectClass"] | ||
,searchFilter: "(&(objectClass=*))" | ||
}) yield entry | ||
return entry.dn, entry | ||
---- |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package apoc.load; | ||
|
||
import java.util.Map; | ||
|
||
public class LDAPResult { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we use MapResult instead? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or are there other useful columns that makes sense to add? like data provenance ones? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I only did this to have the name 'entry' which is more reflecting the ldap name LDAPEntry. But technically we could also use MapResult. Then the examples have to be changed. |
||
public final Map<String, Object> entry; | ||
|
||
public LDAPResult(Map<String, Object> entry) { | ||
this.entry = entry; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,256 @@ | ||
package apoc.load; | ||
|
||
import apoc.ApocConfiguration; | ||
import com.novell.ldap.*; | ||
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 java.util.*; | ||
import java.util.stream.Stream; | ||
import java.util.stream.StreamSupport; | ||
|
||
public class LoadLdap { | ||
|
||
@Procedure(name = "apoc.load.ldap", mode = Mode.READ) | ||
@Description("apoc.load.ldap(\"key\" or {connectionMap},{searchMap}) Load entries from an ldap source (yield entry)") | ||
public Stream<LDAPResult> ldapQuery(@Name("connection") final Object conn, @Name("search") final Map<String,Object> search) { | ||
|
||
LDAPManager mgr = new LDAPManager(getConnectionMap(conn)); | ||
|
||
return mgr.executeSearch(search); | ||
} | ||
|
||
public static Map<String, Object> getConnectionMap(Object conn) { | ||
if (conn instanceof String) { | ||
//String value = "ldap.forumsys.com cn=read-only-admin,dc=example,dc=com password"; | ||
Object value = ApocConfiguration.get("loadldap").get(conn.toString() + ".config"); | ||
// format <ldaphost:port> <logindn> <loginpw> | ||
if (value == null) throw new RuntimeException("No apoc.loadldap."+conn+".config ldap access configuration specified"); | ||
Map<String, Object> config = new HashMap<>(); | ||
String[] sConf = ((String) value).split(" "); | ||
config.put("ldapHost", sConf[0]); | ||
config.put("loginDN", sConf[1]); | ||
config.put("loginPW", sConf[2]); | ||
|
||
return config; | ||
|
||
} else { | ||
return (Map<String,Object> ) conn; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. check if it is really a map? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could we use a ldap:// URL instead? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ldap:// url is not convenient it has also a kind of search syntax in it (so it is more than a connection alone) I only need the connection parameters, search we do via the 'searchMap' |
||
} | ||
} | ||
|
||
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 SEARCH_BASE_P = "searchBase"; | ||
private static final String SEARCH_SCOPE_P = "searchScope"; | ||
private static final String SEARCH_FILTER_P = "searchFilter"; | ||
private static final String SEARCH_ATTRIBUTES_P = "attributes"; | ||
|
||
private static final String SCOPE_BASE = "SCOPE_BASE"; | ||
private static final String SCOPE_ONE = "SCOPE_ONE"; | ||
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 LDAPConnection lc; | ||
private List<String> attributeList; | ||
|
||
public LDAPManager(Map<String, Object> connParms) { | ||
|
||
String sLdapHostPort = (String) connParms.get(LDAP_HOST_P); | ||
if (sLdapHostPort.indexOf(":") > -1) { | ||
this.ldapHost = sLdapHostPort.substring(0, sLdapHostPort.indexOf(":")); | ||
this.ldapPort = Integer.parseInt(sLdapHostPort.substring(sLdapHostPort.indexOf(":") + 1)); | ||
} else { | ||
this.ldapHost = sLdapHostPort; | ||
this.ldapPort = 389; // default | ||
} | ||
|
||
this.loginDN = (String) connParms.get(LDAP_LOGIN_DN_P); | ||
this.password = (String) connParms.get(LDAP_LOGIN_PW_P); | ||
} | ||
|
||
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)); | ||
} catch (Exception e) { | ||
e.printStackTrace(); | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
public LDAPSearchResults 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 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"); | ||
} | ||
// 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); | ||
} else { | ||
searchResults = lc.search(searchBase, searchScope, searchFilter, attributeList.toArray(new String[0]), false, cons); | ||
} | ||
return searchResults; | ||
} catch (Exception e) { | ||
e.printStackTrace(); | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
private LDAPEntry read(String dn) throws LDAPException, UnsupportedEncodingException { | ||
if (dn == null) return null; | ||
LDAPEntry r = null; | ||
op("read start for dn: " + dn); | ||
LDAPConnection lc = getConnection(); | ||
r = lc.read(dn); | ||
closeIt(lc); | ||
// op( r.toString()); | ||
op("read end"); | ||
return r; | ||
} | ||
|
||
private LDAPSchema getSchema() throws LDAPException, UnsupportedEncodingException { | ||
LDAPSchema r = null; | ||
LDAPConnection lc = getConnection(); | ||
r = lc.fetchSchema(lc.getSchemaDN()); | ||
closeIt(lc); | ||
//op( r.toString()); | ||
|
||
return r; | ||
} | ||
|
||
public static void closeIt(LDAPConnection lc) { | ||
try { | ||
lc.disconnect(); | ||
} catch (Exception e) { | ||
// ignore | ||
e.printStackTrace(); | ||
} | ||
} | ||
|
||
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); | ||
|
||
|
||
LDAPConnection lc = new LDAPConnection(); | ||
lc.connect(ldapHost, ldapPort); | ||
|
||
// bind to the server | ||
lc.bind(ldapVersion, loginDN, password.getBytes("UTF8")); | ||
// tbd | ||
// LDAPConnection pooling here? | ||
// | ||
return lc; | ||
} | ||
|
||
private void op(String s) { | ||
System.out.println("LDAPManager:>" + s); | ||
} | ||
|
||
} | ||
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)); | ||
} | ||
} | ||
//System.out.println("entry " + entry); | ||
return entry; | ||
|
||
} catch (LDAPException e) { | ||
e.printStackTrace(); | ||
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(); | ||
} else { | ||
return att.getStringValueArray(); | ||
} | ||
} | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be compileOnly + testOnly imho,
otherwise it's packaged into the jar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried it with this and I still needed the ldap.jar in the plugins folder. And yes it is mentioned in the doc