Freitag, 25. Mai 2012


Artikel

März 2003 | Artikel

Aktive Daten

(Link zum Artikel: http://www.entwickler.de/dotnet//000321)

Verzeichnisdienste optimal einsetzen

Text: von Jörg Wegener
  • Teilen
  • kommentieren
  • empfehlen
  • Bookmark and Share
Die Nutzung von Informationen aus Verzeichnisdiensten kann für eigene Anwendungen ein nicht zu unterschätzender Vorteil sein. Besonders das Active Directory ist eine wahre Fundgrube, da hier an zentraler Stelle alle wichtigen Informationen einer Windows-Domäne gespeichert werden. Gerade diese Informationen lassen sich sehr gut in unseren Programmen verwenden, da der Zugriff auf Verzeichnisdienste unter .NET sehr gut und mit geringem Aufwand realisiert werden kann.

Verzeichnisdienste effektiv nutzen - auch so könnte die Überschrift dieses Artikels lauten. Denn trotz der zahlreichen Informationen, die man zum Beispiel aus einem Active Directory beziehen kann, und dem einfachen Zugriff in .NET bergen die Verzeichnisdienste scheinbar immer noch ungenutztes Potenzial. Die Idee, Informationen an zentraler Stelle des Netzwerks zu speichern, ist nicht gerade neu und begegnete uns bereits in verschiedenen Variationen. So konnte man in einer Windows NT Domäne bereits zahlreiche Daten über Benutzer einer Domäne abfragen, doch das starre Datenformat erwies sich als unflexibel. Applikationen wie Exchange Server mussten immer noch eine separate Benutzerdatenbank mit Informationen, die in den Benutzerkonten von Windows nicht aufgenommen werden konnten, pflegen. Das Active Directory ist an dieser Stelle flexibler. Es dient als zentraler Datenspeicher und ist somit die Informationsquelle einer Windows-Domäne schlechthin. Hier sind alle wichtigen Informationen über den Aufbau des Netzwerks gespeichert - vom Benutzer bis hin zum Netzwerkdrucker. Und da die Pflege von redundanten Daten nicht nur Inkonsistenzen hervorrufen kann, sondern auch eine undankbare Aufgabe für Administratoren ist, sollte man als Entwickler bevorzugt auf bestehende Informationsquellen zurückgreifen.
Neben dem Zugriff auf Active Directory wird auch der Zugriff auf LDAP-Server, Novell Netware-Directory Services und sogar IIS-Konfigurationen ermöglicht. Besonders der letzte Punkt sticht etwas hervor, da es sich hierbei nicht um einen gewohnten Verzeichnisdienst mit Benutzerdaten handelt. Doch kann man über diesen Weg elegant die Konfigurationsdaten des Webservers auslesen, wie wir später in diesen Artikel sehen werden. Auch wenn mit Windows Server 2003 der IIS seine Konfiguration in XML speichert, wird weiterhin der Zugriff über den Verzeichnisdienst möglich sein. In .NET wird uns der Zugriff auf Verzeichnisse im Namespace System.DirectoryServices zur Verfügung gestellt, wobei die am häufigsten genutzten Klassen DirectoryEntry und DirectySearcher sind. Sie kapseln die grundlegenden Funktionen und erleichtern uns somit den Zugriff auf Verzeichnisdienste.
Verzeichnisdienste sind stark auf Performance ausgelegt. Die Entwickler haben daher ein verstärktes Augenmerk auf die Antwortzeiten der Dienste gelegt und optimierten den Zugriff, soweit es möglich war. Lese-Abfragen sind die häufigste Form der Anfragen und so wundert es nicht, dass besonders hier auf schnelle Antwortzeiten Wert gelegt wurde. Wir können ebenfalls einen nicht zu unterschätzenden Einfluss auf die Performance ausüben, indem wir diese Philosophie auch in unseren Entwicklungen fortsetzen.

LDAP und Active Directory
Um eine Übersicht über den Aufbau eines Active Directories zu erhalten, können wir mit einfachen Bordmitteln einen Directory Browser programmieren. Dieser enthält ein TreeView-Control, das die hierarchische Struktur des Verzeichnisses darstellt, eine ListView, die die Eigenschaften jedes selektierten Eintrages darstellt, und einen DirectoryEntry. All diese Controls finden sich in der Toolbox von Visual Studio und können per Mausklick eingebunden werden. Den Anfang macht die Klasse DirectoryEntry, die nicht nur einen beliebigen Eintrag einer Baumstruktur darstellt, sondern zugleich für den Verbindungsaufbau zu einem Verzeichnisdienst zuständig ist. Denn anders als bei einem echten Datenbankserver benötigen wir hier keine zusätzliche Klasse, die sich um den reinen Verbindungsaufbau kümmert:
  1. private void mConnect_Click(object sender, EventArgs e) {
  2. directoryEntry.Path = "LDAP://" + hostname;
  3. directoryEntry.Username = username; // optional
  4. directoryEntry.Password = password;
  5. }
Die Pfadangabe könnte man mit dem ConnectionString für eine Datenbank vergleichen. Hier sind das genutzte Protokoll und der Hostname (oder die IP-Adresse) des Servers gesetzt, mit dem Verbindung aufgenommen werden soll. Da das Active Directory von außen wie ein LDAP angesprochen werden kann, nutzen wir an dieser Stelle das LDAP-Protokoll. Die Eigenschaften Username und Password sind optional und bieten die Möglichkeit, den Zugriff auch mit den Rechten eines anderen Benutzers durchzuführen. Das ist besonders dann wichtig, wenn Änderungen im Verzeichnis vorgenommen werden sollen. Damit sind im Prinzip bereits die grundlegenden Vorbereitungen getroffen. Üblicherweise könnte man nun eine Methode à la Connect erwarten, die explizit eine Verbindung zum Server aufbaut. Doch diese Methode wird uns hier nicht zur Verfügung gestellt, da der Verbindungsaufbau automatisch erfolgt - allerdings noch nicht an dieser Stelle. Erst wenn effektiv die Inhalte aus dem Verzeichnisdienst abgerufen werden, wird vom Client eine Verbindung zum Server aufgebaut. Das soll im Rahmen der Performancesteigerung mehr Leistung bringen, da wirklich nur dann kommuniziert wird, wenn es notwendig ist. Das bedeutet aber im Umkehrschluss, dass eine fehlerhafte Konfiguration der Pfadangabe sich auch erst später bemerkbar macht!
Den Aufbau des Verzeichnisses können wir nun komfortabel mit einer TreeView darstellen. Da jede Instanz von DirectoryEntry genau einen Eintrag des Verzeichnisbaums darstellt, habe ich in dem Beispielprojekt eine neue Klasse DirectoryEntryNode angelegt, die von TreeNode abgeleitet ist (siehe Listing 1). Diese nimmt zusätzlich eine Instanz von DirectoryEntry im Konstruktor auf.

Listing 1
  1. public class DirectoryEntryNode : TreeNode, IDisposable {
  2. private DirectoryEntry entry;
  3. public DirectoryEntryNode(DirectoryEntry entry) {
  4. this.entry = entry;
  5. this.Text = entry.Name;
  6. }
  7. public DirectoryEntry DirectoryEntry {
  8. get {
  9. return entry;
  10. }
  11. }
  12. public void Dispose() {
  13. entry.Dispose(); // unbedingt Speicher wieder freigeben
  14. }
  15. }
Die übergebene Instanz von DirectoryEntry wird intern abgelegt und kann jederzeit über eine Eigenschaft erreicht werden. Damit können wir später erneut auf die notwendigen Informationen zurückgreifen. Ausgerüstet mit dieser Klasse können wir nun die TreeView mit Leben füllen, indem wir rekursiv den Verzeichnisbaum durchgehen. Allerdings sollten wir beachten, dass ein Active Directory in einem Unternehmen mit mehreren Hundert oder Tausend Benutzern sehr groß werden kann. Das Durchsuchen eines gesamten Verzeichnisses kann daher sehr umfangreich werden - besonders wenn es sich wie in unserem Projekt nur um eine Sichtung der Daten handelt.
  1. private void mConnect_Click(object sender, EventArgs e) {
  2. directoryEntry.Path = "LDAP://" + hostname;
  3. tvObjects.Nodes.Clear(); // TreeView leeren
  4. DirectoryEntryNode node = new DirectoryEntryNode(directoryEntry);
  5. tvObjects.Nodes.Add(node); // Root-Element hinzufügen
  6. AddChildren(node); // Rekursive Methode
  7. }
  8. private void AddChildren(DirectoryEntryNode node) {
  9. foreach(DirectoryEntry child in node.DirectoryEntry.Children) {
  10. node.Nodes.Add(new DirectoryEntryNode(child));
  11. }
  12. }
In unserem Projekt gehen wir daher beispielhaft nicht rekursiv vor, sondern zeigen lediglich die erste sichtbare Ebene des Verzeichnisbaums an. Ist der Benutzer an weiteren Details interessiert und expandiert den Baum, so werden nur die neuen sichtbaren Unterelemente aktualisiert. Diese Methodik optimiert die Zugriffszeiten ungemein und der Benutzer hat zudem das Gefühl, bereits mit einer vollständigen Baumstruktur zu arbeiten. Damit wir rechtzeitig die gewünschten Daten nachladen können, müssen wir auf das Ereignis BeforeExpand der TreeView hören:
  1. private void tvObjects_BeforeExpand(object sender, TreeViewCancelEventArgs e) {
  2. foreach(TreeNode child in node.Nodes) {
  3. if (child.Nodes.Count==0)
  4. AddChildren((DirectoryEntryNode)child);
  5. }
  6. }
Da wir im TreeNode zusätzlich unsere Instanz von DirectoryEntry gespeichert haben, fällt uns somit der erneute Zugriff auf den Verzeichnisdienst nicht sonderlich schwer. So können wir über ihn alle untergeordneten Einträge aktualisieren und mit unserer privaten Methode AddChildren() mit Inhalten versorgen:
  1. private void tvObjects_AfterSelect(object sender, TreeViewEventArgs e) {
  2. DirectoryEntryNode node = (DirectoryEntryNode)e.Node;
  3. DirectoryEntry entry = node.DirectoryEntry;
  4. lvProperties.Items.Clear(); // ListView leeren
  5. foreach (string name in entry.Properties.PropertyNames) {
  6. PropertyValueCollection values = entry.Properties[name];
  7. foreach(object value in values) {
  8. string[] items = new string[2];
  9. items[0] = name;
  10. items[1] = value.ToString();
  11. lvProperties.Items.Add(new ListViewItem(items));
  12. }
  13. }
  14. }
Dass hier zwei foreach-Schleifen ineinander geschachtelt sind, hat seinen Grund. Jeder Eintrag im Verzeichnis kann potenziell mehrere Werte enthalten, die alle auf den selben Namen hören. Daher müssen wir hier alle möglichen Einträge ausgeben, die einem Eintrag zugeordnet sind. Das ist auch der Grund, warum hier nicht mit einer Hashtable gearbeitet wurde, da beispielsweise der Schlüssel objectClass mehrfach vorkommt. Damit ist der Directory Browser bereits fertig und der Benutzer kann sich in die Tiefen der Verzeichnisse stürzen. Doch ein Bild sagt mehr als Tausend Worte: Wie wir in Abpictureung 1 erkennen können, haben wir mit wenigen Mitteln bereits einen ansehnlichen Browser für Verzeichnisdienste programmiert.
Wenn Sie später in Ihrem Programm auch Änderungen im Active Directory vornehmen möchten, sollten Sie noch berücksichtigen, dass die Änderungen nicht direkt zum Server geschickt werden. Denn auch hier wurde aus Gründen der Performance die Kommunikation auf ein Wesentliches begrenzt. Damit nicht bei jeder kleinen Änderung dem Server diese auch mitgeteilt werden muss, werden erst mit der Methode CommitChanges alle vorgenommenen Änderungen zum Server übertragen.
Tipp: GUID verwenden
Benutzernamen können sich im Laufe eines Benutzerlebens ändern. Wenn Sie in Ihrer Applikation auf einen bestimmten Benutzer verweisen, verwenden Sie daher nicht den Benutzernamen als eindeutiges Identifizierungsmerkmal. Die GUID eines Benutzers, die auch intern von Microsoft verwendet wird, ist dazu besser geeignet, da diese eindeutig ist und selbst nach dem Umbenennen eines Benutzers konstant bleibt.
Das Active Directory durchsuchen
Das Suchen von Einträgen wäre nicht effizient, müsste man jeden Eintrag im Active Directory selbst durchforsten. Dafür ist das serverseitige Suchen, das wie bei einer relationalen Datenbank lediglich die Suchtreffer zurückgibt, wesentlich besser geeignet. Auch hier müssen wir lediglich die Suchabfrage zum Server schicken, damit dieser uns die interessanten Einträge zurückgibt. Diese Funktionalität wird uns mit der Klasse DirectorySearcher zur Verfügung gestellt. Haben wir diese Klasse aus der Toolbox ebenfalls in unser Programm aufgenommen, können wir damit anfangen, unsere Suche zu definieren. Und wie bei vielen Dingen, fängt alles mal wieder bei Root an: dem SearchRoot. Dieser gibt den Startpunkt im Verzeichnisbaum an, an dem die Suche beginnen soll. Alle weiteren Untereinträge werden dann vom Server rekursiv abgearbeitet. Wir können uns so auf den Teilbereich konzentrieren, der für uns relevant ist. Damit können wir selbst einen nicht unerheblichen Teil zur Performancesteigerung beisteuern:
  1. DirectoryEntry entry = new DirectoryEntry("LDAP://weg004/CN=Users,DC=JMWegener,DC=com");
  2. directorySearcher.SearchRoot = entry;
  3. directorySearcher.Filter = "(&(objectClass=user)(sn=Jörg)(givenName=Wegener))";
  4. directorySearcher.PropertiesToLoad.Add("cn");
  5. SearchResultCollection results = directorySearcher.FindAll();
  6. pnlHint.Text = String.Format("{0} Ergebnisse", results.Count);
In diesem Beispiel habe ich das Benutzerverzeichnis auf dem Server weg004 angesprochen. Die Domäne JMWegener.com steht ebenfalls im Pfad und ist jeweils in die einzelnen Elemente aufgeteilt. Wenn wir uns den Directory Browser aus dem vorherigen Beispiel ansehen, können wir nach Belieben auch andere Pfade wählen, um so beispielsweise auch nach Netzwerkdruckern oder Computern zu suchen. Unsere eigentlichen Suchkriterien sind in der Eigenschaft Filter definiert (auf die Syntax gehe ich gleich detaillierter ein). Mit dem Aufruf der Methode FindAll haben wir somit unsere erste Suche gestartet. Die Methode gibt über die Klasse SearchResultCollection alle gefundenen Einträge zurück. Diese sind hier in Form der Klasse SearchResult gespeichert und bieten uns nun über die Methode GetDirectoryEntry Zugriff auf jeden gefundenen Verzeichniseintrag. Dieser Ansatz wäre jedoch nicht auf Performance optimiert, wenn für jeden Sucheintrag der Verzeichniseintrag nachgeladen werden müsste. Stattdessen haben wir die Möglichkeit, bereits beim DirectorySearcher die Eigenschaften anzugeben, an denen wir interessiert sind. Somit können die Werte bereits beim Suchen in einem Rutsch ausgelesen werden.
Das Suchergebnis ist vom Active Directory serverseitig auf 1.000 Einträge begrenzt - was im Alltagsgebrauch bereits zu viel sein kann. Besser ist es, das Suchergebnis über die Eigenschaft SizeLimit auf ein normales Niveau zu setzen, sodass beispielsweise maximal 30 Treffer angezeigt werden. Zusätzlich steht uns noch die Paging-Technik zur Verfügung, die es uns erlaubt, wie in jeder guten Suchmaschine seitenweise durch das Ergebnis zu blättern. So wird der Benutzer nicht mit Daten erschlagen - kann aber bei Bedarf immer noch weitere Daten anfordern.
Hinweis: Das serverseitige Suchen wird nicht von allen Verzeichnisdiensten unterstützt. Starten wir zum Beispiel eine Suche in der Konfigurationsdatei des IIS, wird stattdessen die Exception NotImplementedException geworfen.
Die Abfragesprache formulieren
Die verwendete Abfragesprache mag etwas fremdartig anmuten. Anders als in SQL sind Suchbedingungen hier in Tokens aufgeteilt, die jeweils durch Klammern umfasst sind. Jedes Token beinhaltet lediglich eine Suchbedingung und gibt als Ergebnis entweder true oder false zurück. Damit auch komplexere Suchmuster möglich sind, können die Tokens mit logischen Operatoren verknüpft werden.
| logisches ODER
& logisches UND
! logische Negierung
 
  Die Syntax einer Suchbedingungen ist relativ einfach erklärt: Werden mehrere Suchbedingungen miteinander verknüpft, so steht der logische Operator vor der Auflistung der Tokens. Anders als bei der üblichen Syntax der Programmiersprachen verknüpfen die Operatoren also die Tokens nicht. Zur Verdeutlichung ein Beispiel: Wenn wir nach den Namen Hans Müller suchen, würde die Abfrage (&(givenName=Hans)(sn=Müller)) alle entsprechenden Einträge auflisten, die beiden Suchkriterien entsprächen. Da wir mit dem logischen UND (&) gearbeitet haben, müssen beide Bedingungen der Tokens zutreffen. Weitere Suchkriterien lassen sich so problemlos hinzufügen. Interessanter wird die Abfrage jedoch, wenn wir mehrere Suchkriterien mit Klammern verschachteln. Eine Abfrage, die sowohl Hans als auch Elfriede in der Benutzerdatenbank finden soll, könnte dann ausformuliert so aussehen:
  1. (&(|(givenName=Hans)(givenName=Elfriede))(sn=Müller))
Zu guter Letzt steht natürlich auch eine Wildcard zur Verfügung, die in der gewohnten Form des Sterns daherkommt. Sie steht für ein beliebiges Zeichen und kann Suchabfragen flexibler gestalten. Benutzt man in einem Suchkriterium ausschließlich eine Wildcard, so werden alle Einträge zurückgegeben, die diesen Schlüssel gesetzt haben. Bei den ganzen Möglichkeiten ist es jedoch ein wenig nachteilig, dass bereits bei einfachen Aufgaben die Abfrage durch die komplexe Syntax sehr unübersichtlich werden kann.
Internet Information Service
Wie eingangs erwähnt, kann man über die Verzeichnisdienste auch auf die Konfigurationsdaten des IIS zugreifen. Alle dafür notwendigen Schritte haben wir bereits vorhin beim LDAP kennen gelernt - lediglich das Protokoll muss in der Pfadangabe der Klasse DirectoryEntry modifiziert werden:
  1. private void mConnect_Click(object sender, EventArgs e) {
  2. directoryEntry.Path = "IIS://" + hostname;
  3. }
Damit haben wir unseren Directory Browser bereits für unsere Zwecke angepasst und können nun die Konfiguration eines beliebigen IIS im Netzwerk durchsuchen. Der automatisierte Zugriff aus unseren Programmen heraus vereinfacht die einheitliche Konfiguration mehrerer Webserver erheblich. Uns bieten sich somit dieselben Möglichkeiten, wie wir sie auch aus der Windows-Verwaltung kennen. Auch das Starten und Stoppen von Websites oder des Webservers kann hierüber erreicht werden. Dieses Feature ist besonders für Webhoster von Interesse, da man automatisiert die Websites der Kunden erstellen und bearbeiten kann.
Verzeichnisdienste und COM
Der Zugriff auf Verzeichnisdienste könnte für uns unter .NET nicht einfacher sein. Möchte man jedoch tiefer in die Materie eintauchen, kommt man um die Verwendung von COM kaum herum. Denn mit den COM Interfaces ist es wesentlich komfortabler, auf Funktionen und Eigenschaften zuzugreifen als mittels der Klasse DirectoryEntry. Hier stehen uns für die verschiedenen Objekte im Active Directory auch unterschiedliche Interfaces zur Verfügung - für einen Benutzer beispielsweise ist es das Interface IADsUser. Dieses enthält Eigenschaften wie FullName, auf die wir mit der gewohnten Manier der Objektorientiertheit zugreifen können. Optimal können wir auch eine Kombination beider Ansätze verfolgen, da die Klasse DirectoryEntry auch die Möglichkeit bietet, das jeweilige native Interface zurückzugeben.
Um Zugriff auf die Interfaces zu erhalten, müssen wir in Visual Studio .NET lediglich im Solution Explorer einen Verweis auf die Typenbibliothek Active DS Type Library hinzufügen (siehe Abb. 3). Damit stehen uns bereits deutlich mehr Möglichkeiten zur Verfügung.
  1. ActiveDs.IADs ads = (ActiveDs.IADs)entry.NativeObject;
  2. ActiveDs.IADsUser user = (ActiveDs.IADsUser) entry.NativeObject;
  3. Console.WriteLine(user.FullName);
  4. user.ChangePassword("alt", "neu");
Die Eigenschaft NativeObject gibt uns das Objekt des Verzeichniseintrags zurück, das wir mit unterschiedlichen Interfaces ansprechen können. Jeder Eintrag im Active Directory unterstützt das Interface IADs und stellt somit den kleinsten gemeinsamen Nenner dar. Damit bietet das Interface aber auch kaum mehr Möglichkeiten, als nur Codezeilen aufzufüllen. Interessanter wird es erst dann, wenn wir Benutzerkonten mit dem Interface IADsUser abfragen. Hier können wir auf Eigenschaften und Methoden direkt zugreifen, um beispielsweise über SetPassword das Passwort eines Benutzers zurückzustellen (übrigens ein beliebtes Spiel, um Anwender zu ärgern). Bei der scheinbaren Fülle an Eigenschaften sollte man jedoch beachten, dass diese nicht notwendigerweise auch im Active Directory gespeichert sein müssen. Viele Informationen über Benutzer sind nur optional und existieren nicht einmal als leerer String im Active Directory. Ein Zugriff auf eine nicht vorhandene Eigenschaft wirft daher bereits beim Abruf eine Exception.
Die vielen verschiedenen Möglichkeiten der Interfaces aufzuführen, würde den Umfang dieses Artikels sprengen. Es sei hier auf die Dokumentation des Platform SDK verwiesen, die ausführlich auf die Interfaces eingeht.
Fazit
Die Verwendung von Verzeichnisdiensten kann Programmen einen erheblichen Zusatznutzen bringen. Gerade der Nutzen eines zentralen Datenspeichers in einem Unternehmen gewinnt in der heutigen Zeit immer mehr an Bedeutung. Und dank der einfachen Umsetzung der Klassen im .NET Framework steht dem nichts mehr im Wege. Großes Thema war hier besonders die Performance, da man von sehr vielen Programmen und Systemen ausgeht, die Verzeichnisdienste nutzen.
Links und Literatur

Kommentare