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:private void mConnect_Click(object sender, EventArgs e) {directoryEntry.Path = "LDAP://" + hostname;directoryEntry.Username = username; // optionaldirectoryEntry.Password = password;}
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
public class DirectoryEntryNode : TreeNode, IDisposable {private DirectoryEntry entry;public DirectoryEntryNode(DirectoryEntry entry) {this.entry = entry;this.Text = entry.Name;}public DirectoryEntry DirectoryEntry {get {return entry;}}public void Dispose() {entry.Dispose(); // unbedingt Speicher wieder freigeben}}
private void mConnect_Click(object sender, EventArgs e) {directoryEntry.Path = "LDAP://" + hostname;tvObjects.Nodes.Clear(); // TreeView leerenDirectoryEntryNode node = new DirectoryEntryNode(directoryEntry);tvObjects.Nodes.Add(node); // Root-Element hinzufügenAddChildren(node); // Rekursive Methode}private void AddChildren(DirectoryEntryNode node) {foreach(DirectoryEntry child in node.DirectoryEntry.Children) {node.Nodes.Add(new DirectoryEntryNode(child));}}
private void tvObjects_BeforeExpand(object sender, TreeViewCancelEventArgs e) {foreach(TreeNode child in node.Nodes) {if (child.Nodes.Count==0)AddChildren((DirectoryEntryNode)child);}}
private void tvObjects_AfterSelect(object sender, TreeViewEventArgs e) {DirectoryEntryNode node = (DirectoryEntryNode)e.Node;DirectoryEntry entry = node.DirectoryEntry;lvProperties.Items.Clear(); // ListView leerenforeach (string name in entry.Properties.PropertyNames) {PropertyValueCollection values = entry.Properties[name];foreach(object value in values) {string[] items = new string[2];items[0] = name;items[1] = value.ToString();lvProperties.Items.Add(new ListViewItem(items));}}}
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:DirectoryEntry entry = new DirectoryEntry("LDAP://weg004/CN=Users,DC=JMWegener,DC=com");directorySearcher.SearchRoot = entry;directorySearcher.Filter = "(&(objectClass=user)(sn=Jörg)(givenName=Wegener))";directorySearcher.PropertiesToLoad.Add("cn");SearchResultCollection results = directorySearcher.FindAll();pnlHint.Text = String.Format("{0} Ergebnisse", results.Count);
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.
|
(&(|(givenName=Hans)(givenName=Elfriede))(sn=Müller))
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:private void mConnect_Click(object sender, EventArgs e) {directoryEntry.Path = "IIS://" + hostname;}
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.
ActiveDs.IADs ads = (ActiveDs.IADs)entry.NativeObject;ActiveDs.IADsUser user = (ActiveDs.IADsUser) entry.NativeObject;Console.WriteLine(user.FullName);user.ChangePassword("alt", "neu");
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
- Jacob Hammer Pedersen et al., Data-Centric .NET Programming with C#, Wrox Press, 2001
- [2] Microsoft Platform SDK: msdn.microsoft.com/platformsdk/


