Programming LDAP with Groovy
Join the DZone community and get the full member experience.
Join For FreeIt all started with a task to do:
Print all members of the group within Active Directory, including members of the nested groups.
And a deadline: 15 minutes.
Given the deadline, I had no chance to get it done in time. Having 15 minutes means you need to get it right from the first run. Googling for groovy ldap brought Gldapo. But after looking at it and seeing how much configuration has to be done, I searched for some alternatives.
Groovy LDAP was beautifully simple and had no external dependencies. I downloaded the jar, dropped it into my GROOVY_HOME/lib directory and started to write the script:
import org.apache.directory.groovyldap.LDAPAfter reading through the sample scripts, I already had the main part:
ldap =
LDAP.newInstance('ldap://ldap.mycompany.com:389/dc=mycompany,dc=com')
ldap.eachEntry ('&(objectClass=person)(memberOf=cn=mygroup') { person ->I saved it as listGroup.groovy and ran it from the command line:
println "${person.displayName} (${person.cn})"
}
groovy listGroupIt worked out of the box, printing on the console all the members of the group:
John Smith (smithj)Of course, the script was not printing members of the nested groups. In order to do that, I had to turn the snippet into the Groovy recurrent function and avoid hardcoding a group's name in favor of taking it as a command line parameter. Here is the entire script:
Amanda McDonald (mcdonaa)
Isabelle Dupre (duprei)
import org.apache.directory.groovyldap.LDAP
import org.apache.directory.groovyldap.SearchScope
List getMembersOfAGroup(connection, groupName) {
def members = []
def result = connection.searchUnique("cn=$groupName”);
connection.eachEntry("memberOf=${result.dn}") { member ->
if (member.objectclass.contains("group"))
members.addAll(getMembersOfAGroup(connection, member.cn))
else
members.add("${member.displayName} (${member.cn})")
}
return members
}
LDAP ldap = LDAP.newInstance("ldap://ldap.mycompany.com:389/dc=mycompany,dc=com")
getMembersOfAGroup(ldap, args[0]).each {
println it
}
If your directory contains circular group relations, the script has to be further adjusted. This detail
has been omitted for simplicity reasons.
Please note, that the examples in this article work only with Microsoft Active Directory, because
they use vendor specific structure and schema elements. In other directory solutions for instance,
group membership is often stored in group entries only, while in Active Directory it is stored in
both group and member object. But the examples can easily be adjusted to fit another directory's
solution, e.g. by modifying filter expressions.
What is this LDAP thing you're talking about?
LDAP 101: LDAP stands for Lightweight Directory Access Protocol. A directory is a storage
organized as a tree of directory entries. The tree usually reflects political, geographical and/or organizational boundaries. Every directory entry consists of a set of attributes (name/value pairs). These attributes are defined in the LDAP schema. Each directory entry has a unique identifier named DN (Distinguished Name). For more information please read Apache Directory introductory article.
Project background
Groovy LDAP is a small library started by Stefan Zoerner from the Apache Directory project. Its goal was to create minimalistic LDAP API for Groovy, with metaphors understood by the LDAP community (e.g. members of the Apache Directory team). As such, the only two dependencies of Groovy LDAP are:
- Java SE (5 or later)
- Groovy 1.0 or later
Under the hood, JNDI is used to perform LDAP queries, but fortunately Groovy LDAP hides it and lets you use a bunch of useful methods and objects, instead. It actually reminds me of the time when Netscape LDAP API was widely used. It defines a set of methods to perform basic LDAP operations: create, modify, delete, compare, search.
Groovy LDAP is written in Java, not Groovy. The only Groovy dependency is a reference to a Closure class, which is used as a parameter in a couple of search methods. So with the exception of the method taking the closure, others can be also used in Java programs.
How to get it
The simplest way is to get the binaries from the Groovy LDAP download page. After downloading and expanding the zip file you need to look for groovy-ldap.jar in the dist directory. Drop it into your GROOVY_HOME/lib directory and you’re ready to write your first script.
How to build it
If you want to build the library on your own, you will need:
After you download and install Ant, drop Ivy's jar (ivy-1.4.1.jar) into your ANT_HOME/lib directory.
Now you can check out the source files from Apache Directory sandbox Subversion repository.
Once the files are checked out, just type ant and wait until the distribution jar is built in the dist directory.
Connecting to the directory
The first thing you will want to do is to connect to the directory. Groovy LDAP offers here two types of connection: anonymous bind and simple bind.
Anonymous bind happens when you connect to the directory without providing your credentials. Many directories allow anonymous bind if the client is only reading from the directory. In corporations anonymous bind is often disabled for security reasons.
So, in order to connect you need to instantiate LDAP class using newInstance() method, with the following variants:
public LDAP newInstance()
public LDAP newInstance(url)
A non-parameter method connects to the default address, which is localhost:389. It proves to be useful for various short proof-of-concept scripts. The second method takes the url of the directory as a second parameter.
If anonymous bind is not allowed or not sufficient there is an equivalent method, taking additionally user credentials:
public LDAP newInstance(url, user, password)
Once the connection is established, you can perform any other actions.
One tip is to always provide a baseDN as a part of the connection url e.g.
ldap://ldap.mycompany.com:389/dc=mycompany,dc=com
By doing so you define the default base, upon which searches will be performed, which in turn allows you to use convenient one parameter search methods, instead of specifying a search base and scope each time.
Reading and searching directory entries
You may want to start with checking if a specific directory entry exists:
def found = ldap.exists('cn=smithj,dc=mycompany,dc=com')
exists() method is searching the directory by DN (Distinguished Name) and returning a boolean result detailing whether an entry was found. As a companion there is read() method, that reads directory entry, specified by its DN:
if (found)
def entry = ldap.read('cn=smithj,dc=mycompany,dc=com')
This method returns either a boolean value or a given entry, accordingly. But there might be cases when you do not want to search by DN, but by another attribute which is also unique. A good example of this is a userId attribute, which is usually unique within a company.
def entry = ldap.searchUnique('userId=smithj')
This method assumes uniqueness of an object. If more than one result is returned from the search, you will get an exception.
When more results are expected, you can use search() method: and then iterate over a result set:
results = ldap.search('(objectClass=user)')
println 'Found: $results.size entries'
results.each { entry ->
println entry.dn
}
Searches can be also performed with more compact and more Groovy method eachEntry() taking a closure as the last parameter:
ldap.eachEntry('(objectClass=user)') { entry ->
println entry.dn
}
As you see, when you have the entry object, you can reference all its properties using native map syntax e.g. entry.dn. This is possible, because all result objects returned from Groovy LDAP search methods are Maps or Lists of Maps.
But, how does Groovy LDAP know in which subtree you would like to perform your search? It doesn't, because you haven't specified anything else, but the basic query. So it assumed you want to search in baseDN (hopefully specified, when connecting to the directory).
When you want to have more control over how the query is performed, there is a different version
of search(), searchUnique() and eachEntry() methods that support it e.g.
public List<Object> search( String filter, String base,
SearchScope scope )
They define additional parameters such as base upon which a search is performed and search scope, being one of the three possible constants:
- SearchScope.BASE – searches only base
- SearchScope.ONE – searches one level below base, excluding base
- SearchScope.SUB – searches the entire subtree below base, including base
So an example search could look like:
ldap.search('objectclass=user', 'ou=hr,dc=mycompany,dc=com',
SearchScope.SUB)
There are also more sophisticated alternatives, taking Map<String, Object> or Search class instance as parameters, but we'll leave them as for now.
When you deal with LDAP directories as a part of your daily job, you may want to have a look at Apache Directory Studio, a full-fledged LDAP client tool, which allows you to connect, browse and modify any LDAP-compatible directory. It can also be used as diagnostic tool when your query in Groovy LDAP doesn't work as expected.
Adding, modifying and deleting directory entries
When you know how to search and read from the directory, it's time to do some modifications. Let's start from adding a new entry:
def attributes = [
objectclass: ['top', 'person'],
cn: 'smithc',
displayName: 'John Smith'
]
ldap.add('cn=smithc,dc=example,dc=com', attributes)
add() method takes DN and a Map with attributes as parameters. You need to remember not to put DN in the attributes map, as it is not an attribute but rather the unique identifier of an entry.
Removing a directory entry is even more straightforward:
ldap.delete('cn=smithc,dc=example,dc=com')
delete() method will throw an exception, if an object with the given DN does not exist.
Modifying a directory entry is not very Groovyish for the time being. Adding single attributes is still relatively easy:
def dn = 'cn=smithj,dc=mycompany,dc=com'
def email = [ email: 'john.smith@mycompany.com' ]
ldap.modify(dn, 'ADD', email)
Performing batch modifications could be more readable using Builder-like syntax.. The current way to do this is the following:
def modifications = [
[ 'REPLACE', [email: 'jsmith@mycompany.com'] ],
[ 'ADD', [phone: '+48 99 999 99 99'] ]
]
ldap.modify(dn, modifications)
The same operation, using more expressive syntax, would potentially look like:
ldap.modify ('cn=smithj,dc=mycompany,dc=com') {
replace(email: 'jsmith@mycompany.com')
add(phone: '+48 99 999 99 99')
}
Summary
As you can see, Groovy LDAP is a neat little library, delivering simple but convenient API to deal with LDAP directories, which makes it an ideal candidate to use in various administrator scripts and short programs.
As a project it resides in Apache Directory sandbox, so when you have a chance, contribute and help Groovy LDAP to become an official subproject of the Apache Directory.
Thanks
I would like to thank Stefan Zoerner and Carolyn Harman for thorough review of the article.
Resources
Opinions expressed by DZone contributors are their own.
Comments