Michal has posted 2 posts at DZone. You can read more from them at their website. View Full User Profile

Programming LDAP with Groovy

02.16.2009
| 23270 views |
  • submit to reddit

It 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.LDAP
ldap =
LDAP.newInstance('ldap://ldap.mycompany.com:389/dc=mycompany,dc=com')
After reading through the sample scripts, I already had the main part:
ldap.eachEntry ('&(objectClass=person)(memberOf=cn=mygroup') { person ->
println "${person.displayName} (${person.cn})"
}
I saved it as listGroup.groovy and ran it from the command line:
groovy listGroup
It worked out of the box, printing on the console all the members of the group:
John Smith (smithj)
Amanda McDonald (mcdonaa)
Isabelle Dupre (duprei)
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:
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

Published at DZone with permission of its author, Michal Szklanowski.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Matt Stine replied on Mon, 2009/02/23 - 1:36pm

Very nice article. I'll keep this one in my tool belt. :-)

Kirk Remignanti replied on Wed, 2010/11/10 - 10:41am

I just found this article, tried to navigate to the Groovy LDAP download page and got a 404 error. Is Groovy LDAP still a viable method for connecting to an LDAP directory?

Update (11/10/10) - I found the download for Groovy LDAP here: http://directory.apache.org/api/1-download-groovy-ldap.html and I was able to implement it by dropping the .jar file into my GRAILS_HOME/lib directory.

The problem I ran in to though is when I tried to use the "import org.apache.directory.groovyldap.LDAP" code in my script to load the LDAP class. It might be because I'm running this script from a Grails environment but I needed to load the class manually using the "classLoader" method. Here's how I was able to get the script to run correctly:

includeTargets << grailsScript("Init")

target(main: "Test LDAP!") {

def loader = this.class.classLoader.rootLoader
def jardir = new File( System.getenv( 'GRAILS_HOME' ), 'lib' )
def jars = jardir.listFiles().findAll { it.name.equals('groovy-ldap.jar') }
jars.each { loader.addURL(it.toURI().toURL()) }

LDAP = Class.forName( 'org.apache.directory.groovyldap.LDAP' )
ldap = LDAP.newInstance('ldap://ldapURL')
ldap.eachEntry ('(sn=Remignanti)') { Remignanti ->
println "(${Remignanti.uid}) ${Remignanti.sn}, ${Remignanti.givenname}"}}

setDefaultTarget(main)


Does anyone know why I would get the error message "unable to assign a value to the class 'org.apache.directory.groovyldap.LDAP'" when I try to use "import org.apache.directory.groovyldap.LDAP" in my script? (My apologies if this is a dumb question, I'm fairly new to the Grails/Groovy programming environment.)

Punit Ashra replied on Tue, 2011/11/08 - 4:22am

Hi i have a problem regarding groovy ldap.I wanted to add already existing users in active directory to be a part of the group.It would me helpful if i get any information regarding this.

Vinod Damle replied on Thu, 2013/01/31 - 10:54pm

Over the past few days, I tried to get both Apache LDAP (M14) and Gldapo (0.8.2) to work with an OpenLdap server via Grails (2.2) . Apache LDAP doesn't work out of the box like the other user commented (duh! I read the comments after a day of wasted effort) and I heard the same on their mailer list. Gldapo has a plugin for grails sts but it is very poorly documented. Exceptions don't work if you have Java 7 (the plugin uses Java 6 if I'm not mistaken) due to some compatibility issue. I spent a couple of days to get a query working but at the end of it, there was still a problem with the schema mapping classes for which I never found an answer. Maybe its Murphy's law, I got hold of UnboundId's LDAP sdk right at the very end and it works like a charm. All I had to do was drop it in the <proj>/lib folder in grails and the examples/api guide in their javadoc are clear enough to get scenarios working. I would strongly recommend UnboundId's SDK.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.