Accéder à un annuaire LDAP avec .NET en utilisant les Design Patterns
Avec l’avènement d’Internet, la sécurité est revenue au cœur des préoccupations
des entreprises. Pour mettre en place cette sécurité, les annuaires distribués jouent
un rôle de plus en plus stratégique et constituent une brique essentielle dans
l’architecture. Le mode d’accès, quant à lui, se résume par l’utilisation du
protocole LDAP comme standard de fait. A l’ère de l’interopérabilité et
du CRM (Relation Client), ce protocole et tous les outils qui lui sont associés,
apportent une bouffée d’air frais dont toutes les entreprise profiteront
inévitablement. Nous avons voulu à travers cet article vous donner un avant goût
des services proposés par .NET concernant l’accès aux annuaires LDAP, mais
aussi vour faire partager, à travers un exemple pratique, l’immense bénéfice
des modèles de conception ou encore Design Patterns.
LDAP est l’acronyme de Lightweight Directory Access Protocol (protocole léger d’accès à un annuaire). Basé sur X500, LDAP (RFC 2251 : ftp://ftp.isi.edu/in-notes/rfc2251.txt) est avant tout un modèle de communication client-serveur s’appuyant sur le protocole TCP/IP. L’avantage d’une telle démarche est de standardiser le mode d’accès aux annuaires distribués afin de supporter une large variété d’implémentations. Le principe est assez proche de technologies telles que http, SMTP ou encore NNTP à ceci près que LDAP propose également une structure d’informations hiérarchiques basée sur des collections d’attributs ou entries. L’attribut DN (Distinguished Name) joue le rôle de « Clé unique » et contient des propriétés pré-définies telles que CN (Common Name), Fax, Mail, etc.… . L’ensemble de ces attributs permettent de qualifier le contexte d’un utilisateur et de stocker des informations relatives à son profil. Il vous est possible de personnaliser à souhait le schéma de l’annuaire afin de le structurer par rapport à des attributs existants (C pour Country, DC, ..).
La difficulté majeure de la mise en place d’un annuaire LDAP réside dans la manière de concevoir son schéma organisationnel. A l’heure où les entreprises opèrent d’incessantes restructurations à tout va, quiconque tente aujourd’hui d’implémenter un modèle d’annuaire basé sur l’organisation d’une entreprise, prend le risque certain de ré-écrire les applications clientes tous les six mois. C’est pourquoi, l’une des méthodes les plus fiables consiste à structurer son annuaire sur un modèle géographique. Bien entendu, nous ne sommes pas à l’abri de voir le département Conseil déplacer ses activités dans une autre ville. Mais il est plus rare que la ville de Toulouse ait à se déplacer aux Etats-Unis, même si, en théorie, la tectonique des plaques ne nous met pas à l’abri de ce genre de mésaventures (j’en connais qui vont apprécier de manger du cassoulet au McDo ;-)).
Concernant l’aspect technique, un annuaire LDAP peut s’appuyer sur une base de données pour stocker ses informations. Cela garantit la fiabilité des informations mais aussi la possibilité de bénéficier des services proposés par les SGBD (Sécurité, Réplication, Synchronisation, ….). Il va de soi que le mapping entre modèle hiérarchique et modèle relationnel n’est pas des plus naturels, mais cette implémentation a d’ores et déjà prouvé son efficacité par le passé.

Pour résumer, un annuaire LDAP est constitué d’une structure arborescente permettant de classer les utilisateurs par catégories, lesquelles sont personnalisables. La difficulté consiste à trouver la bonne conception en fonction de l’organisation de l’entreprise. Techniquement, un annuaire LDAP peut s’appuyer sur une base de données quelconque (Relationnelle, Objet, fichiers textes, …) afin d’assurer le stockage des utilisateurs de la manière la plus fiable, mais les API n’ont en aucun cas à se soucier de l’implémentation sous-jacente utilisée.
Les apports de .NET Microsoft propose dans sa gamme de produits (Commerce Server, Exchange Server, …) le support du protocole LDAP à travers son annuaire Active Directory. Cela permet de vérifier par exemple l’existence d’un compte donné à partir d’un navigateur ou d’un client de messagerie supportant le protocole LDAP (exemple ici).
Avant l’apparition de .NET, les développeurs s’appuyaient sur l’API ADSI (Active Directory Services Interface) ou sur le modèle ADO pour réaliser les opérations de lectures et de modifications.
Aujourd’hui, le namespace System.DirectoryServices a pris le relais en proposant un ensemble de fonctionnalités allant de la création d’utilisateurs jusqu’à l’administration du serveur Web IIS (Internet Information Server). .NET tire parti du fait que toute structure arborescente peut être représentée par une API abstraite. Ainsi, dans la pratique, l’ajout d’un utilisateur dans un domaine NT n’est pas sensiblement différent de l’ajout d’un répertoire virtuel sous la hiérarchie IIS.
Certaines caractéristiques présentes dans ADSI ont été portées sous le Framework .NET avec une utilisation entièrement managée. Les habitués d’ADSI trouveront vite leurs repères tant .NET simplifie les opérations de création, recherche et suppression d’objets.
Les classes suivantes constituent l’essentiel de l’API
DirectoryServices. Vous remarquerez la simplicité de l’API, pas de fioritures,
un nœud est représenté par la classe DirectoryEntry contenant des
propriétés ResultPropertyCollection.
|
Class |
Description |
|
Contient
les nœuds fils de l’entrée d’un annuaire Active Directory. |
|
|
Encapsule
la notion de noeud ou d’objet dans la hiérarchie Active Directory. |
|
|
Exécute
une requête. |
|
|
Contrôle
les permissions de l’annuaire System.DirectoryServices. |
|
|
Contrôle
les permissions concernant la modification d’attributs System.DirectoryServices |
|
|
Plus
petite unité de permission System.DirectoryServices. |
|
|
Contient
une collection fortement typée d’objets DirectoryServicesPermissionEntry. |
|
|
Contient
les propriétés d’un nœud DirectoryEntry. |
|
|
Contient
les valeurs des propriétés DirectoryEntry. |
|
|
Contient
les propriétés du résultat d’une recherche SearchResult
effectuée. |
|
|
Contient
les valeurs SearchResult. |
|
|
Contient
une liste de nom de schéma que le filtre SchemaFilter
peut utiliser. |
|
|
Encapsule
un nœud renvoyé lors d’une requête avec DirectorySearcher. |
|
|
Contient
une instance SearchResult
lorsqu’une recherche a été effectuée. |
|
|
Spécifie
les options de tri d’un résultat donné. |
|
Enumeration |
Description |
|
Spécifie
les types d’authentification possibles System.DirectoryServices. |
|
|
Définit
le niveau d’accès System.DirectoryServices. |
|
|
Spécifie
l’étendu (scope) de la recherche |
|
|
Spécifie
comment trier un résultat donné |
Pour accéder à un nœud de l’arbre, il convient d’utiliser des expressions du type en fonction de la source de données manipulée:
· LDAP://MyDomain.dotnetguru.org/CN=Thomas,DC=DEV,DC=Valtech,DC=COM,O=Internet (LDAP)
·
LDAP://CN=TopHat,DC=DEV,DC=MSFT,DC=COM,O=Internet
(LDAP)
· WinNT://MyNTDomain/Group (Base des comptes NT)
· WinNT://MyNTDomain/MyComputer/aPrinter/ (Base des comptes NT)
· NDS://ndsServer/O=Internet/DC=COM/DC=MSFT/DC=DEV/CN=TopHat (annuaire LDAP de Novell)
· NWCOMPAT://binderyServer/TopHat
Dans l’exemple suivant, nous nous appuierons sur ces classes afin d’implémenter un programme permettant de créer un utilisateur dans un annuaire et de le modifier aisément. Nous discuterons ensuite de la conception générale de notre code.
Le programme suivant permet de rechercher un utilisateur à partir d’un nom donné. Remarquez la simplicité du modèle de développement. Nous commençons tout d’abord par créer une entrée DirectoryEntry pointant sur le nœud à partir duquel la recherche sera effectuée. Nous spécifions ensuite le filtre par l’intermédiaire de la propriété searcher.Filter, puis nous lançons la recherche avec la méthode FindAll(). Le résultat nous est renvoyé sous forme d’une collection d’objets de type SearchResult.
Il ne reste plus au client qu’à itérer sur la collection et afficher les différents champs ou attributs comme illustré dans le code suivant. Vous remarquerez les similitudes avec la manière d’accéder à une table dans une base de données relationnelle.
using System.DirectoryServices
;
using
System ;
// DotNetGuru Client.cs
// Author : SJ
public class Client
{
// Rechercher un utilisateur dans
l’annuaire
static SearchResultCollection SearchUser(string
name)
{
DirectoryEntry
root = new DirectoryEntry("LDAP://DotNetGuru/CN=Utilisateurs,DC=Toulouse,DC=DotNetGuru,DC=org");
DirectorySearcher
searcher=new DirectorySearcher(root);
// Recherche par
nom
searcher.Filter = "(anr=" +
name + ")";
// On veut
l'ensemble de ces propriétés
searcher.PropertiesToLoad.Add("cn");
searcher.PropertiesToLoad.Add("title");
searcher.PropertiesToLoad.Add("department");
searcher.PropertiesToLoad.Add("physicalDeliveryOfficeName");
searcher.PropertiesToLoad.Add("telephoneNumber");
return (searcher.FindAll());
}
// Créer un utilisateur dans
l’annuaire
static void CreateUser(string
newuser)
{
DirectoryEntry
DirEntry = new DirectoryEntry("LDAP://DotNetGuru/CN=”
+ newuser + ”,DC=Toulouse,DC=DotNetGuru,DC=org");
PropertyCollection pcoll =
DirEntry.Properties;
// Mettre ici les valeurs des différents attributs
pcoll["UserName"].Value=XXX
pcoll["Tel"].Value=XXX
DirEntry.commitChanges();
}
// Méthode cliente
public static void
Main()
{
string[] args = Environment.GetCommandLineArgs();
string name = args[1];
// Appel de
notre méthode recheche d'utilisateur
SearchResultCollection
results = SearchUser(name) ;
// Affichage du résultat
foreach(SearchResult result in
results)
{
Console.WriteLine("Nom de
l'utilisateur" + result.Properties["cn"][0]);
Console.WriteLine(result.Properties["title"][0]);
Console.WriteLine(result.Properties["department"][0]);
Console.WriteLine(result.Properties["physicalDeliveryOfficeName"][0]);
Console.WriteLine(result.Properties["telephoneNumber"][0]);
}
}
}
Le code précédent convient tout à fait pour des besoins simples, mais présente un inconvénient majeur dans certain cas. Si vous observez de plus près le code du Main() correspondant au client, vous remarquerez qu’il est intimement lié au fait que l’annuaire sous-jacent est du type LDAP. De ce fait, le jour où l’entreprise décide de porter l’ensemble de son fichier utilisateurs sous un format de stockage différent de type SGBD, Fichiers textes XML ou propriétaire, il vous faudra ré-écrire entièrement le code de l’application cliente. De plus, la notion d’utilisateur n’est pas vraiment exploitée sous la forme d’objet métier.
Il y a donc deux problèmes à résoudre :
1. Masquer au client le fait qu’un utilisateur est stocké dans un annuaire de type LDAP
2. Renvoyer un objet métier et assurer que les opérations de création, recherche et suppression soient portables avec n’importe quel source de données. Allons plus loin, ceci sans avoir à modifier une seule ligne de code existant ! quel programme …
Le schéma suivant nous illustre la problématique d’une telle situation.

Rappelez-vous la phrase précédente : « Masquer au client le fait que les utilisateurs soient stockés dans une base (…) ». Cette phrase qui pourrait paraître anodine vient pourtant de nous donner une information cruciale. En effet, ce genre de question a sûrement déjà été soulevée par d’autres personnes ou d’autres projets et il est difficile de croire qu’il n’existe pas, à travers le monde, un modèle de conception existant permettant d’y répondre. Vous l’aurez deviné, il en existe effectivement un, connu sous le nom de Factory ou Fabrique de classes. Ce Design Pattern permet de masquer totalement les opérations de création, recherche et modification d’un objet au client. Voyons de plus près son mode de fonctionnement.
Le schéma suivant nous illustre le principe, très simple dans la pratique. Vous créez une classe chargée d’effectuer toutes ces opérations à la place du client en lui assurant qu’il récupèrera toujours un objet métier, en l’occurrence un objet de type Utilisateur (User). Gardez à l’esprit que le client ne doit jamais créer l’objet User à l’aide de l’opération New(), cette action est de la responsabilité de la Factory. Pour assurer cette condition, il vous suffit de protéger le constructeur de l’objet métier.
Voyons le diagramme de classes associé :

Le client s’adresse donc dans un premier temps à l’usine de classes d’utilisateurs par l’intermédiaire de son interface : IUserFactory. Quel est l’avantage de cette technique ? Rendre le client indépendant de l’implémentation utilisée grâce au polymorphisme. Les différentes fabriques spécifiques devront fournir une implémentation des méthodes de création, recherche et modification d’un objet User. Voici le code du client précédent adapté à notre nouvelle conception :
using
System;
using
System.Reflection ;
using
System.DirectoryServices ;
namespace DotNetGuru
{
/// <summary>
/// La fabrique
d’utilisateurs, l’interface la plus importante
/// </summary>
public interface
IUserFactory
{
public User Create(UserInfo Info) ;
User
SearchUser(string Name) ;
System.Collections.ICollection
SearchAll();
}
// Cet objet sera à ré-écrire
en cas de changement du type d'annuaire
// Remarquez que
l'ensemble des traitements spécifiques sont confinés dans cette classe
public class UserLDAP : IUserFactory
{
public User Create(UserInfo
Info)
{
//
Création d'un utilisateur LDAP
}
public User SearchUser(string
Name)
{
SearchResultCollection
results = LDAPUtility.SearchUser(Name) ;
// On creer
l'objet metier ici et on initialise ses champs
User
u = new User();
foreach(SearchResult result in
results)
{
u.Name
=
result.Properties["cn"][0].ToString();
//u.title = result.Properties["title"][0];
//u.department =
result.Properties["department"][0];
//u.tel =
result.Properties["telephoneNumber"][0];
}
return u ;
}
public System.Collections.ICollection SearchAll()
{
// On
retourne une collection de tous les utilisateurs de l'annuaire
}
}
// Autre
implémentation de la fabrique d’utilisateurs
// La méthode de
recherche et de création est différente de la précédente (LDAP) mais le client
// ne doit en aucun
cas être impacté par ce choix d’implémentation interne
public class UserSGBD : IUserFactory
{
public User Create(UserInfo
Info)
{
// Appel
à ADO.NET pour réaliser la requête SQL d’insertion
// insert into TABLE_USER values (Name=$ParamName, …
}
public User SearchUser(string
Name)
{
// Appel
à ADO.NET pour effectuer un select * from TABLE_USER where name=$Name
}
public
System.Collections.ICollection SearchAll()
{
// On réalise
un select * from TABLE_USERS qu’on renvoie sous la forme de collection
d’objets;
}
}
public class User
{
// Classe User
simple, on aurait pu rajouter d'autres attributs
// mais aussi
des méthodes de contrôles
string name ;
public string Name
{
get { return name; }
set { Name=value ; }
}
}
public class Client
{
public static void Main()
{
// Ce
client utilise la fabrique LDAP d’utilisateurs
// Notez
que la chaîne de la classe UserLDAP pourrait être placée dans un fichier de
configuration
IUserFactory
UserFactory = (IUserFactory) Activator.CreateInstance("DotNetGuru.UserLDAP");
User
u = UserFactory.SearchUser("Dupont");
// Vous pouvez
décider de changer de structure de stockage en cours de route, aucun problème
IUserFactory
UserFactory = (IUserFactory) Activator.CreateInstance("DotNetGuru.UserSGBD");
User
u = UserFactory.SearchUser("Dupont");
}
}
}
Séduits ? Il faut bien le reconnaître, les Design Patterns sont des modèles de conception qui nous facilitent énormément la tâche. S’en priver reviendrait à ré-inventer la poudre en prenant le risque de faire moins bien que l’original.
Sans le savoir, vous utilisez à travers les différentes API de .NET de multiples fabriques en tout genre. Le meilleur exemple se trouve dans l’API ObjectSpaces (cf article DotNetGuru), en fonction du type de stockage utilisé pour les objets métier (XML ou SQL) un DataSpace est créé jouant le rôle de fabrique.
Java adopte à travers JNDI (Java Naming Directory Interface) une démarche assez proche de .NET concernant l’accès aux annuaires. La différence fondamentale provient du fait que JNDI est une spécification alors que System.DirectoryServices est une implémentation. Il existe énormément de fournisseurs (providers) JNDI dans le monde Java :
- File System (permet d’accéder aux fichiers du disques par l’intermédiaire d’une API standard)
- LDAP
- Annuaires d’Objets de type Corba ou RMI
- NIS (Network Information System de Sun) ou DNS (Domain Name Server)
Malheureusement, que ce soit Java ou .NET, trouver une API unifiée permettant d’accéder à n’importe quelle source de donnée n’est pas chose facile tellement il existe de langages d’interrogations différents. Ainsi, LDAP utilise la notation (&(objectclass=Person)(cn=Nico*)), les bases de données, SQL et les fichiers XML, Xpath ou XSL. Sans compter les fournisseurs DNS (Domain Name Server), NIS ou File System.
L’API d’accès aux annuaires de .NET présente tout de même un inconvénient majeur. L’implémentation de la plupart des classes est réalisée au dessus de l’API ADSI (Active Directory Service Interface). Celle ci est entièrement écrite en code natif. D’ailleurs, il suffit de provoquer la moindre exception pour s’apercevoir que System.Interop est utilisée à de multiples endroits afin de gérer l’interaction avec les composants COM d’ADSI. Bien que cela n’impacte pas le client qui s’adresse aux API .NET, il peut y avoir des répercussions sur les performances car des wrappers .NET/COM sont sollicités. Les équipes de Redmond n’ont vraisemblablement pas eu assez de temps pour porter entièrement ADSI en code managé, il faudra patienter pour voir les prochaines versions du Framework combler cette lacune.
Cet article vous a donné un aperçu de la puissance de l’API System.DirectoryServices. A travers le sujet, nous avons essayé de vous montrer l’intérêt de la conception à base de Design Patterns. Il ne tient qu’à vous de les utiliser à bon escient. N’hésitez pas à demander conseil aux plus expérimentés qui sauront vous guider à travers leur mise en œuvre, quelque fois plus ardue qu’une simple Fabrique de classes.
Auteur : Sami Jaber
Copyright : DotNetGuru Ó 2002