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:
dn | Distinguished Name. Siehe naechster Abschnitt. |
c | Country. Gibt eine Landesinformation, zumeist in Form der TLD-Abkuerzungen (DE fuer Deutschland, DK fuer Daenemark, PL fuer Polen usw. wieder.) |
o | Organization. Gibt die Information wieder, mit welcher Organisation sich dieser Teil des Baumes befasst. |
ou | Organizational Unit. So etwas wie die Abteilung innerhalb der Organisation. |
cn | Common Name. Ein allgemeiner Name in irgendeiner Form. Das kann zum Beispiel ein Menschenname sein, der nicht nach Vor- und Nachnamen unterschieden ist. |
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: ad001Dieses 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: blurfIn 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.deHier 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: 42Hier 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.ldifWobei 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: ad001Da 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: businessHier 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: privateDas 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.ggSoweit 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:
- Abdeckung moeglichst aller gaengigen Plattformen fuer Clients (Haette ich eine reine Unix-Loesung gesucht, haette es auch gereicht, ein paar Katzen mit nem sed zusammenzubinden)
- Erfassung moeglichst vieler Daten
- Durchsuchen eines Teilbaums oder des ganzen Baums
- Semantische Aequivalenz zwischen verschiedenen Clients
- Kontact bzw. KAdressbook 3.5.10 auf FreeBSD current
- Evolution auf FreeBSD current
- Mozilla Thunderbird Addressbook auf FreeBSD current
- Microsoft Outlook 2000 im Firmen-Modus
- Microsoft Outlook Express 6 bzw. Windows Adressbuch
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" |