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.

Qu’est ce que LDAP ?

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.

Le Namespace System.DirectoryServices

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.

Classes

Class

Description

DirectoryEntries

Contient les nœuds fils de l’entrée d’un annuaire Active Directory.

DirectoryEntry

Encapsule la notion de noeud ou d’objet dans la hiérarchie Active Directory.

DirectorySearcher

Exécute une requête.

DirectoryServicesPermission

Contrôle les permissions de l’annuaire System.DirectoryServices.

DirectoryServicesPermissionAttribute

Contrôle les permissions concernant la modification d’attributs System.DirectoryServices

DirectoryServicesPermissionEntry

Plus petite unité de permission  System.DirectoryServices.

DirectoryServicesPermissionEntryCollection

Contient une collection fortement typée d’objets DirectoryServicesPermissionEntry.

PropertyCollection

Contient les propriétés d’un nœud DirectoryEntry.

PropertyValueCollection

Contient les valeurs des propriétés DirectoryEntry.

ResultPropertyCollection

Contient les propriétés du résultat d’une recherche SearchResult effectuée.

ResultPropertyValueCollection

Contient les valeurs  SearchResult.

SchemaNameCollection

Contient une liste de nom de schéma que le filtre SchemaFilter peut utiliser.

SearchResult

Encapsule un nœud renvoyé lors d’une requête avec DirectorySearcher.

SearchResultCollection

Contient une instance SearchResult lorsqu’une recherche a été effectuée.

SortOption

Spécifie les options de tri d’un résultat donné.

Enumerations

Enumeration

Description

AuthenticationTypes

Spécifie les types d’authentification possibles System.DirectoryServices.

DirectoryServicesPermissionAccess

Définit le niveau d’accès System.DirectoryServices.

SearchScope

Spécifie l’étendu (scope) de la recherche

SortDirection

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.

Créer, rechercher et modifier un utilisateur en C#

Les différentes étapes

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.

 

 Comment améliorer la conception de notre programme ?

Merci les Design Patterns …

 

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.

Que propose Java pour l’accès aux annuaires ?   

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.

Les points faibles   

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.

Conclusion

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