Johnbarquin’s Weblog

La bitácora del Barquín más barco de la Web

Servicios de Directorio en .NET 3.5

Publicado por johnbarquin en Junio 12, 2008

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.

Una respuesta para “Servicios de Directorio en .NET 3.5”

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

    [...] Comentarios (RSS) « Servicios de Directorio en .NET 3.5 [...]

Escribe un comentario

XHTML: Puedes usar estas etiquetas: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>