Servicios de Directorio en .NET 3.5

En los últimos días he trabajado intensamente en una nueva versión de un viejo sistema de gestión de usuarios de Active Directory (AD).  Son dolorosos los recuerdos de los primeros pasos intentando manipular los objetos de AD a través de C#, sobre todo por la escasa documentación sobre el tema.

La opción que ofrece .NET desde su primera versión son las clases del espacio de nombres System.DirectoryServices, que constituyen una concha de código administrado (managed code) sobre el componente COM Active Directory Service Interfaces (ADSI).  Sin duda estas clases brindan una potente herramienta para manipular completamente cualquier instrumentación de AD con la desventaja de todas las herramientas de bajo nivel:  cualquier operación, hasta las más comunes de creación de cuentas, listar, actualizar y eliminar (CRUD), se traduce en largos y oscuros pedazos de código llenos de trucos y parches.

Así las cosas, bregando con DirectoryServices y la muy versátil DirectoryEntry, logré instrumentar varias operaciones hasta que encontré un excelente howto en codeproject http://www.codeproject.com/KB/system/everythingInAD.aspx, “Cómo hacerlo (casi) todo en Active Directory via C#”, es lo mejor y más práctico que hay en la web sobre el tema, incluso teniendo las nuevas clases de .NET 3.5 que muestro más adelante.  A partir de aquí todo fluyó mucho más fácil, tanto que en la nueva versión me planteé construir una biblioteca de clases que encapsulara gran parte de la “oscura” funcionalidad de trabajo con AD en .NET, a través de una interfaz más amistosa y cercana a los conceptos que usamos los administradores de redes, como cuenta de usuario, grupo de AD, etc; y que además utilizara la filosofía de la tipificación fuerte (strong typing), muy adecuada por las ventajas que todos conocemos: detección de errores en tiempo de compilación, etc. … y publicarla para que los que tuvieran que hacer lo mismo no pasaran el mismo trabajo.  Una vez pensado el diseño abrí Visual Studio, creé la primera clase e inmediatamente fui a añadir una referencia a nuestra amiga System.DirectoryServices y para sorpresa debajo hallé una nueva inquilina llamada System.DirectoryServices.AccountManagement !!!  

El nombre lo dice todo, en el momento comencé a buscar información sobre mi nueva amiga y … de pronto me sentí liberado por la dialéctica de Microsoft de la tarea de construir aquella biblioteca: ya “ellos” lo sacaron en el framework 3.5 … y una versión mejorada a mis humildes pensamientos. Realmente el trabajo con AccountManagement es cosa de niños.  Luego de leer el artículo publicado en la revista de MSDN en enero de 2008 http://msdn.microsoft.com/en-us/magazine/cc135979.aspx, no debemos tener ninguna dificultad para su utilización en proyectos que requieran la manipulación común de usuarios en Active Directory, el almacén local de Windows (SAM), o Active Directory Application Mode (ADAM).  Es este un tanto a favor de AccountManagement, el acceso de forma consistente a estos tres directorios, liberando al programador de los detalles propios de cada uno.

Por arribita …

El espacio de nombres System.DirectoryServices.AccountManagement es bien sencillo y todo gira alrededor del nuevo concepto de Principal.  Asi, encontramos la clase UserPrincipal que representa las cuentas de usuario, GroupPrincipal que representa los grupos y ComputerPrincipal representando a las computadoras.

La clase que se ocupa de la conexión al almacén de datos correspondiente es PrincipalContext que, a diferencia de su contrapartida en el DirectoryServices plano, hace una estricta separación de los parámetros que componían anteriormente la cadena de conección al directorio.  En base a esta cadena el proveedor tenía que hacerle un parsing y extraer por ejemplo el tipo de almacén, el nombre del dominio o el servidor de dominio específico, y el camino a conectar dentro del directorio.

Supongamos que debemos crear un usuario en un determinado controlador del dominio nombrado dc1.centrodeporte.com y adicionarlo a un grupo con permiso de actualización del sitio web institucional.  Es tan sencillo como:       

PrincipalContext contextoDominio = new PrincipalContext(ContextType.Domain, "dc1.centrodeportes.com", "usuario_privilegiado", "clave_usuario_privilegiado");
       
UserPrincipal usuario = new UserPrincipal(contextoDominio, "johnbarquin", "minuevaclave", true);
usuario.GivenName = "John";
usuario.Surname = "Barquin";
usuario.PasswordNeverExpires = true;
usuario.Save();

GroupPrincipal grupo = GroupPrincipal.FindByIdentity(contextoDominio, "Actualizadores del sitio");
grupo.Members.Add(usuario);
grupo.Save();

En este ejemplo se muestran las características más relevantes del espacio de nombres. Uno de los constructores de PrincipalContext permite definir explícitamente las opciones de seguridad que deseamos usar para conectar al directorio (ej. autentificación básica, SSL, etc).

Limitaciones o … posibilidades de extensión

Creo que AccountManagement satisface muy bien el propósito para el que fue creado: brindar a los programadores una API de fácil uso para operaciones de uso frecuente sobre los directorios.  Sin embargo, jugando un poco con sus potencialidades, podemos lograr operaciones mucho más complejas, sin perder la sencillez y elegancia del código.

Como programador me hubiese gustado que el PrincipalContext se comportara de forma más similar a, digamos, las Connection a bases de datos relacionales … me explico: retomemos el ejemplo anterior de crear un usuario en Active Directory e insertarlo en un grupo.  En nuestra institución, como en muchos lugares con cantidad de usuarios de mediana a alta, se modifica la instalación por defecto de AD, creando unidades organizacionales (OU) que respondan de forma más adecuada a la estructura institucional y permitan mantener una mejor organización de las cuentas y grupos de usuarios.  Por defecto, AD crea los usuarios y grupos en el contenedor predeterminado “Users”.  Supongamos que hemos creado nuevas OU llamadas “Cuentas” y “Grupos” para almacenar cuentas de usuarios y grupos para roles específicos de la institución respectivamente.  Cuando creamos usuarios y grupos usando new UserPrincipal y new GroupPrincipal,  AccountManagement los crea en el camino de AD que indica el PrincipalContext que hemos definido, y si no especificamos uno en el constructor con el parámetro “container”, entonces los creará en el implícito “Users”.  Para que cree las cuentas en “Cuentas” debemos modificar la definición del contextoDominio de manera que incluya el container:

PrincipalContext contextoDominio = new PrincipalContext(ContextType.Domain, "dc1.centrodeportes.com", "ou=Cuentas,dc=centrodeportes,dc=com", "usuario_privilegiado", "clave_usuario_privilegiado");

Siguiendo con el ejemplo, para buscar el grupo “Actualizadores del sitio”, debemos crear un segundo contexto que apunte a la OU que se creó para los grupos (nótese que el camino definido en el contexto se usa tanto para las inserciones como para las búsquedas):

PrincipalContext contextoGrupos = new PrincipalContext(ContextType.Domain, "dc1.centrodeportes.com", "ou=Grupos,dc=centrodeportes,dc=com", "usuario_privilegiado", "clave_usuario_privilegiado");

y el nuevo código para insertar el usuario e insertarlo en el grupo quedaría:

PrincipalContext contextoUsuarios = new PrincipalContext(ContextType.Domain, "dc1.centrodeportes.com", "ou=Cuentas,dc=centrodeportes,dc=com", "usuario_privilegiado", "clave_usuario_privilegiado");
       
UserPrincipal usuario = new UserPrincipal(contextoUsuarios, "johnbarquin", "minuevaclave", true);
usuario.GivenName = "John";
usuario.Surname = "Barquin";
usuario.PasswordNeverExpires = true;
usuario.Save();

PrincipalContext contextoGrupos = new PrincipalContext(ContextType.Domain, "dc1.centrodeportes.com", "ou=Grupos,dc=centrodeportes,dc=com", "usuario_privilegiado", "clave_usuario_privilegiado");
        GroupPrincipal grupo = GroupPrincipal.FindByIdentity(contextoGrupos, "Actualizadores del sitio");
        grupo.Members.Add(usuario);
        grupo.Save();

Si bien el código no ha aumentado su complejidad, a gusto personal hubiera preferido definir un solo contexto que apuntara a todo el dominio y que UserPrincipal me ofreciera un constructor extra donde especificar un camino en AD alternativo para la creación del usuario;  y que ese mismo contexto me sirviera para hacer la búsqueda del grupo en “Grupos” a través de una sobrecarga de FindByIdentity con un parámetro extra “container”.  De todas formas, la instrumentación tendría internamente que crear dos entradas de directorio (DirectoryEnty) una para insertar al usuario y otra para buscar el grupo, pero sería transparente para el usuario.  En un final, estamos hablando de abstracción, o no?

Otro requerimiento específico de nuestra institución es mantener sincronizados el nombre y apellido del usuario con su nombre distintivo (DN).  El DN es una propiedad cuyo valor es único para cada usuario y que refleja el camino para llegar desde la raíz del dominio hasta la cuenta del usuario.  El DN creado por AccountManagement para nuestro nuevo usuario sería:

cn=johnbarquin,ou=Cuentas,dc=centrodeportes,dc=com

y necesitamos que sea:

cn=John Barquin,ou=Cuentas,dc=centrodeportes,dc=com

La buena noticia es que UserPrincipal expone la propiedad DistinguishedName para el DN y la mala que es de solo lectura: imposible cambiarla por esta via.  Para ello debemos meter las manos en el lodo y extraer el DirectoryEntry subyacente:       

DirectoryEntry entry = (DirectoryEntry) user.GetUnderlyingObject();
entry.Rename("cn=John Barquin");
entry.CommitChanges();
entry.Close();

Nada complicado, cierto?  Note que solo hemos renombrado el nodo final del camino que representa el DN;  cambiar el DN completo equivale a cambiar la posición de la entrada en el árbol, para lo cual tendríamos que usar el método MoveTo de DirectoryEntry.

El método GetUnderlyingObject es el más importante de las Principal clases debido a que constituye la conexión entre las aristocráticas clases de AccountManagement y el mundo subterráneo de DirectoryServices.  El siguiente ejemplo muestra otra de sus aplicaciones, en este caso para leer/modificar una propiedad de AD no expuesta por UserPrincipal:

Como pudimos apreciar, nuestro usuario ha sido encargado de actualizar el sitio web institucional.  Para ello debemos habilitar su cuenta para acceso FTP en el servidor IIS que utiliza el aislamiento de usuarios basado en AD.  Esto implica que debemos asignar valores a dos propiedades de la cuenta del usuario en AD: msIIS-FTPRoot y msIIS-FTPDir. Nuevamente obtenemos la entrada subyacente y asignamos las propiedades requeridas:       

DirectoryEntry entry = (DirectoryEntry) user.GetUnderlyingObject();
entry.Properties["msIIS-FTPRoot"].Value = "E:";
entry.Properties["msIIS-FTPDir"].Value = "\WWWRoot";
entry.CommitChages();
entry.Close();

Concluyendo, el nuevo espacio constituye una poderosa plataforma de alto nivel para el trabajo con servicios de directorio en .NET, pero lo más importante es su potencialidad para ser ampliado. En un próximo artículo mostraré la forma “elegante” de extender AccountManagement, heredando de las clases principales.  Por ahora, me quedo con las ganas de poderlo usar con otros proveedores de servicios de directorio, especialmente con OpenLDAP.

Anuncios

9 Responses to Servicios de Directorio en .NET 3.5

  1. Pingback: AccountManagement, aislamiento de FTP y acceso conmutado « Johnbarquin’s Weblog

  2. Jairo says:

    una pregunta de que forma puedo listar todos los usuarios del directorio activo? c#

    • johnbarquin says:

      static void ListAccounts()
      {
      DirectoryEntry entrada = new DirectoryEntry(@”LDAP://midominio.com/cn=Users,dc=midominio,dc=com”, “Administrator”, “password”);
      DirectorySearcher buscador = new DirectorySearcher(entrada);
      buscador.Filter = (“(&(objectCategory=Person)(objectClass=user))”);
      SearchResultCollection resultado = buscador.FindAll();
      foreach (SearchResult usuario in resultado)
      {
      Console.WriteLine(usuario.GetDirectoryEntry().Properties[“sAMAccountName”].Value.ToString());
      }
      Console.ReadKey();
      }

  3. Jairo says:

    Muchas gracias por la respuesta voy a probar a ver que tal me va.

  4. Jairo says:

    oye probe el cogigo y funciono muy bien, pero ahora tengo un inconveniente necesito listar los usuarios de un grupo especifico, pero no logro de funcione.

    DirectoryEntry entrada = new DirectoryEntry(@”LDAP://sumer.com/cn=Users,dc=sumer,dc=com”, “Administrador”, “clave”);
    DirectorySearcher buscador = new DirectorySearcher(entrada);
    buscador.Filter = (“(&(objectClass=group)(cn=Total_Sumer))”);
    SearchResultCollection resultado = buscador.FindAll();
    return resultado;

    El grupo al que quiero ver los miembros se llamma “Total_Sumer”.

    Que me hace falta?

    • johnbarquin says:

      Con AccountManagement (using System.DirectoryServices.AccountManagement;) es muy facil, seria algo asi:

      PrincipalContext contextoGrupos = new PrincipalContext(ContextType.Domain, “sumer.com”, “Administrador”, “clave”);
      GroupPrincipal grupo = GroupPrincipal.FindByIdentity(contextoGrupos, IdentityType.Name, “Total_Sumer”);
      foreach (Principal p in grupo.Members)
      Console.WriteLine(p.Name);

  5. Jairo says:

    Gracias de nuevo.

  6. Javier says:

    Estimados tengo una pregunta estoy haciendo mis primeros pasos con active directory, estoy haciendo un Web Service que tenga 2 funciones la primera autentificacion y la segunda creacion de usuarios, no tengo problemas en autentificar, cuando quiero crear me sale el siguiente error “System.UnauthorizedAccessException: Acceso denegado. ” el problema aparece en el objeto “deuser” cuando uso la funcion “Children.Add” en la propiedad “Guid” me sale el siguiente error “El objeto de directorio especificado no esta enlazado a un recurso remoto\r\n”

    algun consejo llevo dias estancado con este problema.. agradeciendo de antemano la ayuda brindada..

    string adPath = “LDAP://SRVROOTAD01/OU=test,OU=XXX,DC=XXX-XXXXX,DC=EDU,DC=PE”;

    string domainName = “xxx-xxxxxxx.edu.pe”;
    string userName = “XXXX”;
    string password = “XXXXX”;

    string newuser = “jsanchezz”;
    string userPassword = “jsanchezz”;

    string domainAndUsername = domainName + @”\” + userName;

    DirectoryEntry entry = new DirectoryEntry(adPath, domainAndUsername, password);

    DirectoryEntry contanier = new DirectoryEntry( adPath );

    try
    {
    DirectoryEntry deuser = contanier.Children.Add(“CN=” + newuser, “user”);

    deuser.Properties[“sAMAccountName”].Value = newuser;
    deuser.CommitChanges();

    //deuser.Invoke(“SetPassword”, new object[] { userPassword });
    //deuser.CommitChanges();

    entry.Close();
    deuser.Close();

    }
    catch (System.DirectoryServices.DirectoryServicesCOMException E)
    {
    E.Message.ToString();
    return false;
    }

  7. Adolfo says:

    Buenas tardes, quizás sea un post antiguo pero recién empiezo a trabajar con Active directory en C#.Net ando creando un aplicativo pero que ya realiza varias funciones como habilitar y deshabilitar o cambiar descripción, pero todo lo realiza siempre y cuando se corra la aplicación en el servidor mismo. Qué código podría agregarle para que funcione en cualquier Pc de la empresa conectada a la red? Agradezco de antemano vuestra ayuda.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    using System.DirectoryServices;
    using System.DirectoryServices.AccountManagement;
    using System.Configuration;

    namespace WindowsFormsApplication1
    {
    public partial class HabDeshab : Form
    {
    public HabDeshab()
    {
    InitializeComponent();
    }

    public bool ValidateCredentials(string sUserName, string sPassword)
    {
    PrincipalContext oPrincipalContext = GetPrincipalContext();
    return oPrincipalContext.ValidateCredentials(sUserName, sPassword);

    }

    PrincipalContext context = new
    PrincipalContext(ContextType.Domain, Form1.dominio, Form1.dc, Form1.usuario, Form1.clave);

    public UserPrincipal GetUser(string sUserName)
    {
    PrincipalContext oPrincipalContext = GetPrincipalContext();

    UserPrincipal oUserPrincipal = UserPrincipal.FindByIdentity(oPrincipalContext, sUserName);
    return oUserPrincipal;
    }
    public PrincipalContext GetPrincipalContext()
    {
    PrincipalContext oPrincipalContext = new PrincipalContext(ContextType.Domain, Form1.dominio, Form1.dc, Form1.usuario, Form1.clave);
    return oPrincipalContext;
    }

    Así es cómo me conecto con el AD y bueno establezco los métodos de ayuda, en la conexión “principalcontex” traigo las credenciales llenadas previamente en otro formulario de manera manual.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: