Gruppenhierarchien in LDAP-Verzeichnissen darstellen

30. November 2009

Mit Hilfe des Dotnet-Frameworks von Microsoft lassen sich die Fähigkeiten der Powershell erweitern. Wer mit LDAP-Verzeichnissen zu tun hat und das Zusammenspiel mit dem Active Directory beherrschen muss, der sollte sich den Namespace System.DirectoryServices.Protocols (S.DS.P) genauer ansehen. In diesem Beitrag wird ein Powershell-Skript gezeigt, das mit diesen Namespace umgeht und dabei eine Komma-begrenzte Liste der Gruppen in einem Verzeichnis-Container aufführt. Dabei sind auch alle verschachtelten Gruppenmitgliedschaften aufgelistet. Das Skript funktioniert sowohl mit dem Active Directory (AD) als auch mit LDAP-Verzeichnissen.

Bild 1. Ausgabe mit einem Beispiel für verschachtelte Gruppenmitgliedschaften

Da das Skript aber keine Benutzerkonten liefern und weil es auch verschachtelte Strukturen erkennen sollte, waren zwei wesentliche Aktionen nötig:

Einmal das Filtern der Ergebnisse – damit werden die Benutzerkonten ausgeblendet. Und zum anderen ein rekursives Verhalten, um die Verschachtelungen in beliebiger Tiefe abzudecken. Da mit dem Skript aber auch Nicht-AD-Verzeichnisse zu unterstützen waren, musste man noch einen LDAP-konformen Namespace verwenden.

Dabei fiel die Wahl auf den Namespace System.DirectoryServices.Protocols (S.DS.P), der seit der Einführung des Dotnet-Frameworks 2.0 zur Verfügung steht. Dabei handelt es sich um einen der attraktivsten Namespaces, den das Directory-Services-Team vom Microsoft freigegeben hat.

Er arbeitet schneller und ist zudem weitaus mehr kompatibel zu heterogenen LDAP-Verzeichnissen als alle anderen Directory-Services-Namespaces. Die beiden Argumente für diese Lösung kommen vor allem aufgrund einer Design-Entscheidung: Die ADSI (Active Directory Services Interface) fehlen bei diesem Namespace.

Die ADSI-Ebene besteht aus einem Satz von COM-Interfaces, auf die alle anderen Verzeichnisdienste des Dotnet-Namespaces (wie System.DirectoryServices, System.DirectoryServices.ActiveDirectory und System.DirectoryServices.AccountManagement) aufsetzen.

Da System.DirectoryServices.Protocols keine ADSI mitschleppt, setzen die Klassen ihre Aufrufe direkt an die Win32-Klassen in der Bibliothek Wldap.dll ab.

Eine Architektur-Darstellung dieses Namespace ist auf der zugehörigen Webseite im MSDN („Introduction to System.DirectoryServices.Protocol (S.DS.P)“) zu finden.

Bild 1. Ausgabe mit einem Beispiel für verschachtelte Gruppenmitgliedschaften

Nun stellt sich bestimmt die Frage, warum irgend jemand einen anderen Namespace verwendet, wenn S.DS.P so enorme Vorteile sein eigen nennt. Das liegt in der Funktionalität der ADSI-Ebene begründet:

Trotz der Einschränkungen vereinfacht diese Ebene das Erstellen von Verzeichnisdienst-Code, es verringert den Wartungsaufwand für den Code und erlaubt den Zugriff über COM-basierte Skriptsprachen wie zum Beispiel VBScript.
Es ist zwar zutreffend, dass der Einsatz der Klassen von S.DS.P schwieriger ist als der Umgang mit den Klassen in anderen Namespaces zu den Verzeichnisdiensten.

Doch der Skripter wird schnell sehen, dass es immer bestimmte Code-Muster sind, die man mit zunehmender Erfahrung einbaut. Und die Fähigkeit, mit jedem LDAP-konformen Verzeichnis zusammenarbeiten zu können, macht den höheren anfänglichen Lernaufwand sicher schnell wett.

Die Verwendung von S.DS.P zusammen mit VBScript ist zwar nicht möglich, doch der Einsatz im Zusammenspiel mit der Powershell funktioniert. Deswegen wurde das gewünschte Skript dann auch mit Hilfe der Powershell realisiert. Das Ergebnis ist die Datei GetGroupRelationships.ps1. Es ist als eine ZIP-Datei mit diesem Beitrag als Download zu bekommen.

So wird das Skript eingesetzt

Nach dem Download des Skripts kann man es auf das System kopieren, auf dem es laufen soll (bitte immer erst ein Skript testen, ehe es auf eine Produktivumgebung losgelassen wird). Das Skript greift nach dem Start auf einen Domänen-Controller (DC) in der aktuellen Domäne zu. Deswegen sollte sich das System, auf dem das Skript läuft, auch in der gewünschten Domäne befinden. Zusätzliche Anpassungen an die eigene Domänen-Umgebung sind nicht notwendig.

Bei Aufruf des Skripts sind zwei Kommandozeilen-Parameter anzugeben. Das ist zum einen der FQDN (Fully Qualified Domain Name) des AD- oder des LDAP-Servers, zu dem die Verbindung hergestellt werden soll. Zum Zweiten ist die Angabe des DN (Distinguished Name) der Organisationseinheit (OU, Organizational Unit) nötig, die untersucht werden soll. Ein Beispiel für den Aufruf könnte wie folgt aussehen:

GetGroupRelationships amer.corp.fabrikam.com ou=Groups,dc=amer,dc=corp,dc=fabrikam,dc=com

Nachdem das Skript seine Arbeit beendet hat, bekommt man eine Liste von Gruppen zurück geliefert. Im Bild 1 ist dazu ein Beispiel zu sehen.

Listing 1. Dieser Code lädt die S.DS.P-Assembly
Listing 2. Dieses Code-Segment fragt die Gruppen ab

So arbeitet das Skript

Die erste Aktion von GetGroupRelationships.ps1 ist das Laden der S.DS.P-Assembly. Diese Dotnet-Assembly enthält alle Properties und Methoden, die nötig sind, um Code für LDAP zu schreiben. Die Powershell setzt auf der Dotnet-Klasse System.Reflection.Assembly, um diese und andere Dotnet-Assemblies zu laden.

Es gibt verschiedene Möglichkeiten, um eine Assembly-Klasse zu verwenden und um damit eine Dotnet-Assembly zu laden. Zu den beiden am häufigsten im Umfeld der Powershell gängigen Ansätze sind zum einen der Einsatz der Load-Methode und zum anderen der Einsatz der Methode LoadWithPartialName der Assembly-Klasse.

Auch wenn die zweitgenannte Vorgehensweise – mit der Methode LoadWithPartialName – funktioniert, sollte man doch von ihr die Finger lassen. Es gibt Hinweise, die bis ins Jahr 2003 zurückreichen, die vorschlagen, immer die Load-Methode zu nehmen. Denn die Methode LoadWithPartialName sollte ab dem Dotnet-Framework 2.0 nicht mehr empfohlen werden. Daher ist der Einsatz der Methode Load die bessere Wahl – damit geht man künftigen Kompatibilitätsproblemen aus dem Weg.

Das Listing 1 zeigt, dass das hier gezeigte Skript diese Empfehlung beherzigt. Das Deklarieren [Void] wenn der Code die Load-Methode aufruft unterdrückt die zugehörige Ausgabe, die beim Laden ansonsten ausgegeben wird.

Als nächstes sind die Gruppen aus der OU oder dem über die Kommandozeile angegebenen Container abzufragen. Dazu wird der Code verwendet, der im Listing 2 zu sehen ist.

Das Einzigartige beim Einsatz von S.DS.P für Suchvorgänge in LDAP ist das Muster, wie man mit dem LDAP-Verzeichnis eine Verbindung herstellt, wie eine Suchanfrage erzeugt wird und wie man die Antwort auf diese Anfrage bekommt.

Verbindung mit dem LDAP-Verzeichnis herstellen: Das LdapConnection-Objekt von S.DS.P kommt zum Einsatz, um eine Verbindung mit dem Verzeichnis herzustellen. Später wird dann die Methode SendRequest des Objekts verwendet, um die Anfrage an den Verzeichnis-Server zu übertragen.

Erzeugen der Suchanfrage: das S.DS.P-Objekt SearchRequest wird genommen, um anzugeben, wo die Suche starten soll, welcher Suchfilter zum Einsatz kommt und für das Festlegen des Suchbereichs und die zurückzuliefernden Attribute. In diesem Fall beginnt die Suche in der OU oder bei dem Container, die über die Kommandozeile spezifiziert wurden.

Es sollen ja nur Gruppen aufgelistet werden. Daher ist der folgende Filter

(&(objectClass=group)(objectCategory=group))

zu verwenden. Der Suchbereich wird auf OneLevel gesetzt.

Damit werden auch die untergeordneten Objekte der angegebenen OU (oder des Containers) durchsucht. Als Attribut ist der Common Name (cn) gewünscht. Dabei ist noch darauf zu verweisen, dass die Variablen für den Suchfilter bereits weiter vorne im Code (und somit nicht im Rahmen des Listing 2 zu sehen) gesetzt sind.

Listing 3. Dieser Code überprüft die Suchergebnisse
Listing 4. Dieser Code gibt die Gruppenhierarchie aus.

Die Gruppen sollen nach ihrem Common Names sortiert werden. Daher kommt das Objekt SortRequestControl von S.DS.P zum Einsatz, mit dem ein Sortieranfrage-Control an die Suchanfrage angehängt wird. Dieses Hinzufügen von Controls an eine LDAP-Suchanfrage ist ein wichtiger Mechanismus, um sehr ausgetüftelte Anfragen an LDAP stellen zu können.

Abholen der Suchantwort: Die SendRequest-Methode des LdapConnection-Objekts sendet die Suchanfrage an den LDAP-Server. In diesem Fall wird die Antwort in der Variablen $searchResponse aufgenommen.

Nachdem die Suchanfrage an den LDAP-Server gesendet ist, sollte man einige Dinge prüfen. Dazu zählen die folgenden Aspekte:

  • Der Server hat erfolgreich geantwortet.
  • Zumindest ein Eintrag wurde zurückgeliefert.
  • Der Server hat auf abgesendete Control-Anfragen geantwortet.

Ehe nun durch die Ergebnisse in der Variablen $searchResponse schrittweise gegangen wird, gilt es diese drei Überprüfungen auszuführen. Den Code dazu zeigt das Listing 3 mit den beiden Callout-Bereichen A und B.

Zuerst wird im Code geprüft, ob der LDAP-Server eine Antwort geliefert hat, die auf einen Erfolg der Aktion schließen lässt. Danach ermittelt das Programm, ob zumindest ein Eintrag in der Suchantwort eingetragen ist. Und zum Schluss bleibt noch der Test übrig, ob der Server ein „Sort Response Control“ zurück gegeben hat. Ist das nicht der Fall, ist der Server unter Umständen nicht dazu in der Lage, die Ergebnisse zu sortieren.

Nachdem diese Test alle ausgeführt sind, muss das Programm schrittweise durch die zurückgelieferten Einträge gehen. Den dafür zuständigen Code-Bereich zeigt der Callout B im Listing 3. Hier werden die Stack-Mechanismen (Last-in, First-out) genutzt: Die zuletzt auf dem Stack abgelegten Werte werden als erstes wieder zurückgegeben.

Damit lassen sich die strukturellen Beziehungen in der Hierarchie – also Parent/Child-Konzept – beibehalten. Wenn diese Beziehungen dargestellt werden sollen, liegen sie bereits in der richtigen Reihenfolge vor. Dann muss das Skript nur mehr die Einträge vom Stack holen. Nachdem das Programm die untergeordnete Gruppenmitgliedschaft auf dem Stack abgelegt hat, ruft es die Funktion GetMembers für jede Gruppe auf.

Diese Funktion setzt eine Suchanfrage ab, die der aus dem Listing 2 weitgehend ähnelt. Allerdings sind die Angaben für den Suchbereich sowie das Control anders. Anstelle den Suchbereich mit OneLevel zu belegen, muss hier der Wert Base zum Einsatz kommen.

Damit werden die Mitgliedsattribute eines jeden Gruppenobjekts im Ad untersucht. Um die Mitgliedattribute für jede Gruppe zu suchen, verwendet diese Funktion ein ASQ-Control (Attribute Scoped Query) anstelle des bereits erklärten Sort-Request-Controls.

Wie es schon bei der ersten Suchoperation der Fall war, prüft das Programm danach, ob der LDAP-Server einen Ergebnis-Code liefert, der den Erfolg der Operation signalisiert, ob er zumindest einen Eintrag in der Antwort zurückgibt und ob er ein ASQ-Control zurückgeliefert hat. Wenn diese Antwort Einträge enthält, kann man die Funktion GetMembers rekursiv aufrufen.

Liegen dagegen keine Einträge in der Antwort vor, wird die Ausgabe dargestellt. Der Code dazu ist im Listing 4 zu sehen. Um die Gruppenhierarchie richtig anzuzeigen, muss der Stack invertiert werden (siehe Callout A im Listing 4). Nachdem diese Aktion abgeschlossen ist, wird mit Hilfe eines Dotnet-String-Objekts der Ausgabe-String zusammengebaut.

Dazu werden die Einträge in der Variablen $stackOutputter aneinandergefügt und zwar in der Reihenfolge wie sie vom Stack entnommen werden. Den Code dazu zeigt der Callout B im Listing 4.

Dieser Programabschnitt stellt die Ausgabe auf dem Bildschirm dar. Doch sie lässt sich auch leicht abändern und zum Beispiel in eine Datei gesendet werden beziehungsweise über eine Pipe woanders hin geleitet werden.

Nutzen aus dem Dotnet-Framework ziehen

Wie das Skript GetGroupRelationships.ps1 zeigt, lassen sich die Fähigkeiten der Powershell mit Hilfe des Dotnet-Frameworks deutlich erweitern. Wenn man dann noch mit LDAP-Verzeichnissen, die nicht von Microsoft stammen, zusammenarbeiten muss, empfiehlt sich das S.DS.P als ein besonders nützlicher Ansatz. Und zudem entledigt er einen von den Eigenheiten, die die ADSI mit sich bringen – und bringt auch noch eine bessere Kompatibilität, werden doch alle LDAP-basierten Verzeichnistypen damit unterstützt.

Stefan Kowalewski/rhh

Lesen Sie auch