Статьи

Программирование LDAP с Groovy

Все началось с задачи:
распечатать всех членов группы в Active Directory, включая членов вложенных групп.
И крайний срок: 15 минут .

Учитывая крайний срок, у меня не было шансов сделать это вовремя. Наличие 15 минут означает, что вы должны сделать это правильно с первого запуска. Погуглить для заводной LDAP принес Gldapo . Но, посмотрев на него и увидев, сколько нужно выполнить настройки, я искал несколько альтернатив.

Groovy LDAP был очень прост и не имел внешних зависимостей. Я скачал флягу, бросил ее в каталог GROOVY_HOME / lib и начал писать скрипт:

 

import org.apache.directory.groovyldap.LDAP
ldap =
LDAP.newInstance('ldap://ldap.mycompany.com:389/dc=mycompany,dc=com')

После прочтения примеров сценариев у меня уже была основная часть:

ldap.eachEntry ('&(objectClass=person)(memberOf=cn=mygroup') { person ->
println "${person.displayName} (${person.cn})"
}

Я сохранил его как
listGroup.groovy и запустил из командной строки:

groovy listGroup

Работало из коробки, печатая на консоли всех участников группы:

John Smith (smithj)
Amanda McDonald (mcdonaa)
Isabelle Dupre (duprei)

Разумеется, скрипт не печатал членов вложенных групп. Чтобы сделать это, мне пришлось превратить фрагмент в рекуррентную функцию Groovy и избежать жесткого кодирования имени группы в пользу его использования в качестве параметра командной строки. Вот весь сценарий:

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
}

Если ваш каталог содержит циклические групповые отношения, сценарий нуждается в дальнейшей корректировке. Эта деталь
была опущена из соображений простоты.

Обратите внимание, что примеры, приведенные в этой статье, работают только с Microsoft Active Directory, поскольку в
них используются специфические для поставщика элементы структуры и схемы. Например, в других решениях для каталогов
членство в группе часто сохраняется только в записях группы, а в Active Directory —
как в группе, так и в объекте-члене. Но примеры могут быть легко скорректированы, чтобы соответствовать
решению другого каталога , например, путем изменения выражений фильтра.

 

Что это за штука LDAP, о которой ты говоришь?

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: '[email protected]' ]
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: '[email protected]'] ],
[ '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: '[email protected]')
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