Schon vor einiger Zeit habe ich mich mit der Nutzbarmachung von WebDAV zum Halten von verteilt genutzten Kalendern auseinandergesetzt. Nun sollte es der zweite Gral der Bueroorganisation folgen -- ein gemeinsames Adressbuch. Zunaechst hielt ich meine Anforderungen fuer machbar -- jeder Nutzer soll ueber LDAP auf die gemeinsamen Adressdaten zugreifen koennen, ungeachtet dessen, welches Programm er benutzt. Und da LDAP von den meisten Adressbuechern unterstuetzt und zudem in dem Ruf steht, "in" zu sein, habe ich mich mal damit auseinandergesetzt und einen LDAP-Server zum Experimentieren auf einem FreeBSD aufgesetzt.

Einfuehrung in LDAP in Anlehnung an OpenLDAP

Am Anfang stand bei mir nur die Kenntnis, dass es sich bei LDAP zum einen um ein Netzwerkprotokoll und zum anderen eine Datenbankrepraesentation handelt. Dann fing ich an, eine Einfuehrung zu suchen, insbesondere, wie man mit dem slapd, dem OpenLDAP-LDAP-Server eines aufsetzt, und was dabei was bedeutet. Hier nun also alles von Anfang: Zunaechst die Installation, dann eine Einfuehrung in das Konzept und anschliessend das Konfigurieren und ein wenig Experimentieren.
Wichtiger Hinweis: Ich bin selbst noch ein Neuling, was LDAP angeht. Wenn hier inhaltliche Verbrechen gegen die Richtigkeit vorliegen, dann bitte ich um einen Hinweis (per Mail an ad001@uni-rostock.de oder per IRC an ad001 im euIRC).

Installation des OpenLDAP-Servers auf FreeBSD

Der Server installiert sich, wie bei FreeBSD ueblich, aus den Ports oder einem Binaerpaket. Zur Installation durch den Port, reichen die folgenden Befehle aus: cd /usr/ports/net/openldap24-server/ && make install clean. Damit sollte das System einen OpenLDAP-Server in der momentan aktuellen Version 2.4 bekommen. Dieser wird als "slapd - Stand-alone LDAP Daemon" bezeichnet. Entsprechend ergibt sich auch der Name der Konfigurationsdatei /usr/local/etc/openldap/slapd.conf, um die es als Naechstes geht.

Das LDAP-Konzept

LDAP ist eine objektorientierte Datenbank. So weit, so gut. Wer das erste mal einen Dump aus einer ldif-Datei gesehen hat, der sieht verwundert irgendwelche Zeilen der Form ou=blubbs, o=irgendwas, c=wasanderes. Ich habe dann angefangen, mir Kenntnisse anzulesen. Irgendwie waren die Anleitungen alle nicht wirklich schlecht, aber sie konnten mir nicht erklaeren, warum es jetzt an der einen Stelle ein dc=... und nicht ein ou=... sein musste. Aber von Anfang - was sind o, ou und die anderen?

Knoten und Blaetter

Es handelt sich bei LDAP um eine hierarchische Datenbank. Um einen Baum. Dieser hat eine Wurzel, darunter haengen Knoten und ganz aussen (unten) sind die Blaetter.

                            (Wurzel)
                            /      \
                       (Knoten) (Knoten)
                        /        /   \
                       /        /     \ 
                  (Blatt) (Knoten)   (Knoten)
                             /        /  | \
                            /        /   |  \
                       (Blatt)   (Blatt) | (Blatt)
                                      (Blatt)
Die Knoten sind Mittel zum Zweck, die tatsaechlichen Daten befinden sich in den Blaettern. Soweit noch recht verstaendlich. Die Definition der Objekte, die sich in dem Baum aufhalten koennen, erfolgt in den sogenannten schema-Dateien, die bei der Konfiguration noch eine Rolle spielen werden.

Attribute

Daten werden in den Attributen eines Blattes (sprich, eines Objektes; auch innere Knoten koennen Attribute haben, sind aber nicht als primaere Datenspeicherorte gedacht) gespeichert. Hier trifft man nun wieder auf die oben schon angesprochenen einbuchstabigen Akteure. Hier soll nun eine kleine Tabelle ein paar davon erklaeren, die staendig wieder auftauchen werden:

dnDistinguished Name. Siehe naechster Abschnitt.
cCountry. Gibt eine Landesinformation, zumeist in Form der TLD-Abkuerzungen (DE fuer Deutschland, DK fuer Daenemark, PL fuer Polen usw. wieder.)
oOrganization. Gibt die Information wieder, mit welcher Organisation sich dieser Teil des Baumes befasst.
ouOrganizational Unit. So etwas wie die Abteilung innerhalb der Organisation.
cnCommon Name. Ein allgemeiner Name in irgendeiner Form. Das kann zum Beispiel ein Menschenname sein, der nicht nach Vor- und Nachnamen unterschieden ist.
Attribute kommen zumeist nicht aus dem Nichts -- sie gehoeren zu Objekten, die angegeben werden muessen. Die Objekte haben must und may-Attribute.
Attribute werden (wie aus OO-Irgendwas gewohnt) vererbt. Wenn also Objekt obj1 das Attribut a hat, dann wird das von obj1 abgeleitete Objekt obj1_ diese Eigenschaft ebenfalls aufweisen. Allerdings muss bei der Angabe der Elternklassen diese Hierarchie der Klassen, deren Attribute genutzt werden, mit angegeben werden. Viele weitere Eigenschaften, die sich mit natuerlichen oder rechtlichen Personen beschaeftigen finden sich weiter unten.

Der DN

Jedes Objekt muss sich eindeutig identifizieren. Dazu gibt es den sogenannten "Distinguished Name" (kurz DN), der fuer jedes Objekt eindeutig sein muss. Meist wird dieser aus anderen Attributen zusammengesetzt. Ein Beispiel findet sich bei den Beispielen :)

Beispiele

Wenn ich das Ganze einmal zusammenziehe kann man also folgendes Beispiel bilden:

# ad001, de
dn: o=ad001,c=de
objectClass: top
objectClass: organization
o: ad001
Dieses Objekt hat schon einen gewissen Erklaerungsbedarf, trotz seiner Einfachheit.
Es implementiert die Objekte top und organization. Ersteres hat keine must-Attribute, letzteres hat o, also ist es angegeben.
Hier findet sich auch ein Beispiel fuer einen dn: Er ist hier gebildet aus den Angaben o=ad001,c=de. Aus der Angabe objectClass: top ergibt sich, dass alle Attribute angegeben werden muessen, die von top als Pflichtattribute deklariert sind. Als naechstes wird noch die Klasse organization eingebunden, die das Pflichtattribut o mitbringt, welches folgerichtig ausgefuellt werden muss.
dn: ou=blurf,o=ad001,c=de
objectClass: top
objectClass: organizationalUnit
ou: blurf
In diesem naechsten Beispiel ist es prinzipiell erst einmal nur ein anderes Objekt, dessen Pflichtattribut ein anderes ist, als im vorhergehenden Beispiel - also auch noch ziemlich unspektakulaer. Was noch gesagt werden sollte - der Wert von ou im dn und bei der Angabe als Attribut sollte uebereinstimmen ;) Die Beschreibung einer Person koennte z.B. so aussehen:
dn: cn=Andreas Daehn,ou=ad,o=ad001,c=de
objectClass: top
objectClass: inetOrgPerson
cn: Andreas Daehn
displayName: Andreas Daehn
givenName: Andreas
sn: Daehn
uid: iUc0Kks4ax
homePhone: 0 38 1 /...
mail: ad001@uni-rostock.de
Hier finden sich die Klassen top und inetOrgPerson. Auch nicht sehr spannend. Ein Beispiel, in dem mehrere Klassen in einem Objekt auftauchen (Mehrfachvererbung! Endlich!) koennte so aussehen:
dn: cn=Herr Heiko Hensen,ou=odd,o=ad001,c=de
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
cn: Herr Heiko Hensen
sn: Hensen
mail: heiko.hensen@irgendwo.tld
telephoneNumber: +49 4444 111111
homePhone: +49 4444 555
facsimileTelephoneNumber: +49 4444 11115
postalAddress: Alte Schorfstrasse 666 Folk 22222
o: Folk-eting
title: Leitender Folker
ou: odd
roomNumber: 42
Hier finden sich bunt gemischt Attribute aus den Klassen organizationalPerson (z.B. facsimileTelephoneNumber, ou) neben solchen aus inetOrgPerson (z.B. cn, sn). Die Reihenfolge, in der die Klassen aufgezaehlt sind, ist hier allerdings nicht willkuerlich gewaehlt, sondern entspricht der Vererbungshierarchie des Klassenbaumes.
Eine nett gemachte Auflistung der LDAP-Klassen (mit Klassendiagramm) findet sich unter http://www.it.ufl.edu/projects/directory/ldap-schema/objectclasses.html.

Erste Konfiguration in der slapd.conf

In dieser Konfigurationsdatei liegen ein paar wenige, aber dennoch essenzielle Einstellungen fuer den LDAP-Server. Zum Beispiel, welchen dn der Wurzelknoten hat und was Passwort und Name des Administratornutzers sind. So sieht das bei mir aus (um Kommentare bereinigt, die Erklaerung folgt unten):

include         /usr/local/etc/openldap/schema/core.schema
include         /usr/local/etc/openldap/schema/cosine.schema
include         /usr/local/etc/openldap/schema/inetorgperson.schema

pidfile         /var/run/openldap/slapd.pid
argsfile        /var/run/openldap/slapd.args

# Load dynamic backend modules:
modulepath      /usr/local/libexec/openldap
moduleload      back_bdb

#######################################################################
# BDB database definitions
#######################################################################

database        bdb
directory       /var/db/openldap-data

suffix          "o=ad001, c=de"
rootdn          "cn=Manager, o=ad001 ,c=de"
rootpw          {SSHA}denKTeuchDochgeFAElligstSelbstWa

index   objectClass     eq

Konfiguriert wird mit dieser Daei folgendes:
Zunaechst einmal werden drei schema-Dateien eingebunden. Diese werden hier mit ihrem absoluten Pfad angegeben und beinhalten die Beschreibungen des Objekte, die spaeter genutzt werden sollen. In core.schema findet sich z.B. die Definition von top -- eine Antwort auf die Frage, wie man intuitiv darauf kommt, welche Schemata man benoetigt, habe ich momentan auch keine bessere antwort als die Benutzung von grep :/
In den naechsten Zeilen werden die Pfade der Datei zur Speicherung der Prozess-ID des Daemons und der Datei, aus der die Parameter fuer die Server-Prozesse entnommen werden sollen deklariert.
Nachdem nun die ganz groben organisatorischen Gegebenheiten klar sind, kann es langsam um die Daten gehen, die gespeichert werden sollen. Zunaechst einmal um die Frage, in welcher Form denn -- also mit welcher Datenbank.Ich habe es der Einfachheit bei einer bdb belassen, die Moeglichkeiten sind an dieser Stelle unbeschraenkt, auch "grosse" Datenbanksysteme wie mySQL oder PostgreSQL usw zu nutzen.
Es folgt noch der Ort im Daeisystem, wo die Daten gelagert werden sollen. Bei einer anderen Datenbank sollten hier andere Eintraege folgen.
Nachdem nun die Datenanbindung steht, kann es mit tatsaechlichen LDAP-Einstellungen weitergehen. Und zwar wird als naechstes das suffix gesetzt, Das Suffix ist ein gemeinsames Anhaengsel, welches sich an allem Finden wird, was in dieser Datenbank landet. Bei mir sind es ein o und ein c, es sind auch andere Eintraege denkbar, z.B. zwei dc oder nur einer, oder...
Die naechste Zeile ist wichtig, denn in ihr wird der rootdn (das ist sowas wie der Name des Root-Nutzers) definiert. Wenn es irgendwann spaeter um eine Anmeldung mit Nutzernamen/Passwort am LDAP-Server geht, dann ist das hier der erste definierte Eintrag. In der darauffolgenden Zeile befindet sich das Passwort, welches mit dem Hilfsprogramm ldappasswd gehasht wurde. Diese zwei Eintraege sind brisant, denn sie erlauben (nach einem Knacken des Passwortes, z.B. mit brute-force) den kompletten Zugriff auf alle Teile des LDAP-Baumes. Mit einer granulareren Rechtevergabe werde ich mich in diesem Dokument nicht auseinandersetzen, sie ist aber anzustreben; insbesondere, wenn im LDAP sensible Informationen wie Logininformationen gespeichert werden.
Schliesslich wird noch eine Option zur Indexerstellung gegeben. Indizes sind kleine Helfer, wenn es darum geht, die Geschwindigkeit, mit der eine Datenbank antworten kann, zu optimiere. Eine Optiierung bezueglich von Vergleichen ist sinnvoll, wenn in einem LDAP viel verglichen werden soll -- z.B., wenn es Login-Informationen enthaelt.

Einstellungen der ldap.conf

Die Datei ldap.conf (unter FreeBSD in /usr/local/etc/openldap/ zu finden, gehoert streng genommen nicht zur Konfiguration des LDAP-Servers, sondern der OpenLDAP-Clients, also der Programme ldapsearch, ldapadd, ldapdelete und so weiter. Die hier gezeigte Konfiguration vorzunehmen hat aber einen Vorteil - wenn man auf Shell-Ebene am LDAP arbeitet muss man so nicht staendig die Grunddaten in der Befehlszeile mit sich herumtragen.

host 172.16.0.84
base o=ad001, c=de

LDAP-Daemon starten

Jetzt sind alle grundsaetzlichen serverseitigen Einstellungen vorgenommen, der LDAP-Daemon kann seinen Dienst aufnehmen. Hierzu gibt es auf FreeBSD ein entsprechendes Startskript in /usr/local/etc/rc.d, welches slapd heisst.

Anlegen der Baumstruktur im LDAP

Der LDAP-Server laeuft mittlerweile, aber er enthaelt weder Daten noch eine Struktur. Das soll sich jetzt aendern. Und zwar mit zielfuehrendem Blick in Richtung adressbuch. Angenommen, ich wollte zwei Adressbuecher halten: Eines fuer private und eines fuer geschaeftliche Kontakte. In diesem Falle wuerde sich ja eine solche Baumstruktur anbieten:

                (o=ad001,c=de)
                  /        \
                 /          \
           (ou=private) (ou=business)
Diese Struktur hat (dem OOP-Ansatz sei Dank) beim Durchsuchen noch einen weiteren Vorteil: Je nachdem, ob als Suchbasis ou=private, o=ad001, c=de oder ou=business, o=ad001, c=de oder o=ad001, c=de angegeben wird, durchsucht das Programm entweder den linken oder den rechten Teilbaum oder das Gesamtkonstrukt. Nette Sache, das.
Nun muss diese Idee nur noch in das LDAP gepruegelt werden... Dazu bedien man sich am einfachsten des Programmes ldapadd, mit dem dem LDAP etwas (man ahnt es schon) hinzugefuegt werden kann. Zum Beispiel ein Objekt. Das praktische Vorgehen dabei ist simpel:
[ad001@glas]$ ldapadd -D 'cn=Manager, o=ad001, c=de' -W -f datei.ldif
Wobei mit dem Parameter -D 'cn=Manager, o=ad001, c=de' der root-DN spezifiziert wird; -W gibt an, dass der Nutzer nach dem Passwort gefragt werden soll und -f datei.ldif gibt an, dass die hinzuzufuegenden Dinge in der entsprechenden Datei zu finden sind. Alternativ zu -W koennte man auch -w password spezifizieren, wenn man es schaetzt, dass das LDAP-Manager-Passwort in der shell-History landet oder auf -f datei.ldif verzichten und die entsprechenden Eingaben direkt in das STDIN von ldapadd machen (was zum Testen durchaus angenehm ist). Hinweis zur Syntax: Ein "Block" von LDAP-Kommandos (der ein Objekt beschreibt) endet mit einer Leerzeile, ein Kommentar beginnt mit einem Hash (#) in der ersten Spalte und Zeilen, deren Daten erst in der zweiten Spalte beginnen (sprich, die mit einem Leerzeichen anfangen) werden als Fortsetzung der letzten Zeile interpretiert.
Also zunaechst das Grundgeruest:
dn: o=ad001,c=de
objectClass: top
objectClass: organization
o: ad001
Da in meiner Base ein o vorkommt, ist es wohl notwendig, dieses auch mal zu spezifizieren. Hiermit geschehen, die organization namens "ad001" ist gegruendet.
dn: ou=business,o=ad001,c=de
objectClass: top
objectClass: organizationalUnit
ou: business
Hier wird nun der business-Teil des Adressbaumes begonnen. Ich erinnere nochmal daran, dass Atributte, die in einem Objekt spezifiziert werden und im dn vorkommen, dort den identischen Wert haben muessen. Was an dieser Stelle spaetestens auffaellt: LDAP ist nicht normalisiert, Redundanzen sind gewollt. Das hat einen einfachen Grund: Es spart Rechenzeit, die fuer das Normalisieren und das Rekombinieren bei einer Abfrage notwendig ist und z.B. bei Logins unnoetig lange dauern wuerde. Ansonsten auch noch nicht spekatkulaer.
dn: ou=private,o=ad001,c=de
objectClass: top
objectClass: organizationalUnit
ou: private
Das gleiche Spielchen fuer den privaten Teil.
Das wars auch schon, nun steht der Baum zur Befuellung bereit. Die ersten zwei (Test-) Kontakte kann man noch von Hand einfuegen, um spaeter zu sehen, ob die Adressbuchsoftware funktioniert.
dn: cn=Pritta Privatlich,ou=private,o=ad001,c=de
objectClass: top
objectClass: inetOrgPerson
cn: Pritta Privatlich
givenName: Pritta
sn: Privatlich
homePhone: 0 44 44 / 11 11
mail: pr.pd@ppp.pp

dn: cn=Gregor Geschaeftlich,ou=business,o=ad001,c=de
objectClass: top
objectClass: inetOrgPerson
cn: Gregor Geschaeftlich
givenName: Gregor
sn: Geschaeftlich
homePhone: 0 66 66 / 33 33
mail: gr.g@ggg.gg
Soweit die Einrichtung des LDAPs.

Nutzbarmachung als Adressdatenquelle

Jetzt soll es um die tatsaechliche Nutzung des soeben erstellten LDAPs als Adressbuch gehen. Dabei habe ich die folgenden Ansprueche an das Gesamtkonstrukt gestellt:

Die von mir getesteten Plattformen sind dabei die Folgenden gewesen: Das Einrichten war ein mal einfacher, mal eher grausamer Vorgang, der sich im wesentlichen darauf beschraenkte, dass man den LDAP-Server sowie die dn der Suchbasis angab. Optional (wenn auch schreibend auf das LDAP zugegriffen werden soll, erzwungenermassen) konnten ein LDAP-Nutzer und ein Passwort angegeben werden. Meist klappte es dann sehr schnell, drei Adressbuecher anzulegen, die sich dann durchsuchen liessen.

Schwaechen

So schoen das Ganze in der Thorie klang, so schnell ernuechterte es mich bei den ersten Tests. Ja, LDAP wird unterstuetzt. Aber jeder Entwickler einer Adressbuchimplementation hat sich anscheinend andere Attribute gesucht, die er auszufuellen gedenkt. Das fuehrt dann dazu, dass ein mit Evolution erstellter Kontakt andere Felder ausgefuellt hat, als ein Kontakt, der mit Kontact angelegt wurde. Schade. Jammerschade, dass an der Stelle kein Entwickler die Zeit gefunden hat, mal in die RFCs zu schauen, in denen sogar kommentiert ist, wozu welcher Eintrag gedacht ist. Aber zuvor fielen mir noch banalere Maengel auf: Die meisten Clients koennen nicht ins LDAP schreiben und viele nicht alle im LDAP vorhandenen Kontakte anzeigen. Stattdessen wird das LDAP so verwendet, wie man normalerweise ein Telefonbuch nutzt: Es liegt bereit, man schaut einen Datensatz nach, kopiert ihn sich in ein lokales Notizbuechlein und legt das grosse Buch dann wieder weg. Entsprechend kann man es durchsuchen, aber nicht auflisten. Auch schade.
Das ganze Fazit nochmal in Tabellenform, wobei ich die Klassen nicht ganz richtig aufgeloest habe O:)

ldap-eigenschaft kontact thunderbird Address Book Evolution MS OL 2000 OLE 6 Addr. Book
organizationalPerson.destinationIndicator
organizationalPerson.facsimileTelephoneNumber "Fax" "Fax" "Business Fax" "Fax (geschaeftlich)"
organizationalPerson.internationalSDNNumber
organizationalPerson.l "Locality (Home)" "City (Work)" "City (Work)" "Ort (geschaeftlich)"
organizationalPerson.ou "Department (Job)" "Abteilung" "Abteilung"
organizationalPerson.physicalDeliveryOfficeName "Buero" "Buero"
organizationalPerson.postOfficeBox "Address (Work)" "PO Box (Work)"
organizationalPerson.postalAddress "Address (Work)" "Zusatzinformation" "Strasse (geschaeftlich)"
organizationalPerson.postalCode "Postal Code (Home)" "ZIP/Postal Code (Work)" "ZIP/Postal Code ( Work)" "Postleitzahl (geschaeftlich)"
organizationalPerson.preferredDeliveryMethod
organizationalPerson.registeredAddress
organizationalPerson.st "Region (Home)" "State/Province (Work)" "State/Province (W ork)" "Bundesland (geschaeftlich)
organizationalPerson.street "Street (Home)"
organizationalPerson.teletexTerminalIdentifier
organizationalPerson.telexNumber
organizationalPerson.title "Title" "Title (Work)" "Title (Job)" "Position" "Position"
organizationalPerson.x121Address
inetOrgPerson.carLicense
inetOrgPerson.departmentNumber "Department (Work)"
inetOrgPerson.displayName "Formatted name (custom)"
inetOrgPerson.employeeNumber
inetOrgPerson.employeeType
inetOrgPerson.jpegPhoto
inetOrgPerson.preferredLanguage
inetOrgPerson.userSMIMECertificate
inetOrgPerson.userPKCS12
inetOrgPerson.businessCategory
inetOrgPerson.cn "Display" "Full Name" "Angezeigter Name" "Name"
inetOrgPerson.description "Note" "Notes"
inetOrgPerson.givenName "Given name" "First" "Vorname"
inetOrgPerson.initials "2. Vorname"
inetOrgPerson.objectClass
inetOrgPerson.o "Organization" "Organization (Work)" "Company (Job)" "Firmenname"
inetOrgPerson.seeAlso
inetOrgPerson.sn "Family names" "Last" "Nachname"
inetOrgPerson.telephoneNumber "Work" "Work" "Business Phone" "Telefon" "Rufnummer"
inetOrgPerson.userCertificate
inetOrgPerson.userPassword
inetOrgPerson.x500UniqueIdentifier
inetOrgPerson.name
inetOrgPerson.distinguishedName
inetOrgPerson.audio
inetOrgPerson.homePhone "Home" "Home" "Home Phone" "Rufnummer (privat)
inetOrgPerson.homePostalAddress "Adress (Home)" "Strasse (privat)
inetOrgPerson.mail "Email" "Email" "Email Other" "E-Mail-Adresse" "E-Mail-Adresse"
inetOrgPerson.manager
inetOrgPerson.mobile "Mobile" "Mobile" "Mobile" "Mobiltelefon"
inetOrgPerson.pager "Pager" "Pager" "Pager" "Pager"
inetOrgPerson.photo
inetOrgPerson.roomNumber "Office (Misc)"
inetOrgPerson.secretary
inetOrgPerson.uid
inetOrgPerson.labeledURI "Home Page"

Stichworte:


Impressum