HowTo: Obtener TODOS los usuarios de un grupo del Directorio Activo

Siguiendo con el tema de las últimas entradas, vamos a ver cómo obtener TODOS los usuarios que pertenecen a un grupo del directorio activo. Y cuando digo TODOS los usuarios, me refiero a TODOS (por algo lo he puesto en mayúsculas :-D). Es decir, dentro de un grupo podemos tener otros grupos, que a su vez contengan otros grupos y así succesivamente… y nuestro objetivo es obtener todos los usuarios de forma recursiva.

ADGroups

Para obtener los datos de estos usuarios vamos a crear una clase, para ir almacenando las propiedades que deseamos obtener de cada usuario. Y una función que obtenga los usuarios de un grupo, y se llame a sí misma de forma recursiva en caso que este grupo contenga otros grupos.

El código de la clase:

public class ADUser
{
    public byte[] Sid { get; set; }
    public string Name { get; set; }
    public string DistinguishedName { get; set; }
    public string SAMAccountName { get; set; }
    
    public int RoleType { get; set; }
 
    public ADUser(byte[] sid, string name, 
        string distinguishedName, string sAMAccountName)
    {
        Sid = sid;
        Name = name;
        DistinguishedName = distinguishedName;
        SAMAccountName = sAMAccountName;
    }
 
    public string sIDtoString()
    {
        SecurityIdentifier sid = new SecurityIdentifier(Sid, 0);
        return sid.ToString();
    }
}


Y el método que devuelve todos los usuarios de un grupo de forma recursiva:



namespace Alpha.Code
{
    public class SecurityContextEx
    {
        public static string getDomainName()
        {
            return IPGlobalProperties.GetIPGlobalProperties().DomainName;
        }
 
        public static string getLDAPDomainName(string domainName)
        {
            StringBuilder sb = new StringBuilder();
            string[] dcItems = domainName.Split(".".ToCharArray());
            sb.Append("LDAP://");
            foreach (string item in dcItems)
            {
                sb.AppendFormat("DC={0},", item);
            }
            return sb.ToString().Substring(0, sb.ToString().Length - 1);
        }
 
        public static List<ADUser> GetUsersInGroup(string group)
        {
            List<ADUser> users = new List<ADUser>();
            string ldapDomainName = SecurityContext.getLDAPDomainName(SecurityContext.getDomainName());
            string domainName = ldapDomainName.Replace("LDAP://", string.Empty);
            List<string> groupMemebers = new List<string>();
 
            DirectoryEntry de = new DirectoryEntry(ldapDomainName);
            DirectorySearcher ds = new DirectorySearcher(de, "(objectClass=person)");
 
            ds.Filter = "(&(objectClass=group)(cn=" + group + "))";
            foreach (SearchResult result in ds.FindAll())
            {
                var dir = result.GetDirectoryEntry();
                var list = dir.Invoke("Members");
                IEnumerable entries = (IEnumerable)list;
                foreach (var entry in entries)
                {
                    DirectoryEntry member = new DirectoryEntry(entry);
                    if (member.SchemaClassName == "group")
                    {
                        List<ADUser> usersInGroup =
                            GetUsersInGroup(member.Properties["name"][0].ToString());
                        foreach (ADUser aduser in usersInGroup)
                        {
                            if (!users.ToDictionary(u => u.Name).ContainsKey(aduser.Name))
                            {
                                users.Add(aduser);
                            }
                        }
                    }
                    else
                    {
                        ADUser aduser = new ADUser(
                            (byte[])member.Properties["objectSid"][0],
                            member.Properties["name"][0].ToString(),
                            member.Properties["distinguishedName"][0].ToString(),
                            member.Properties["sAMAccountName"][0].ToString());
                        users.Add(aduser);
                    }
                }
            }
            return users;
        }
    }
}


Aparte de la función GetUsersInGroup, también existen un par de métodos de apoyo para averiguar el nombre de nuestro dominio, que creo recordar que he publicado con anterioridad, pero por si acaso os los he publicado también.



Si deseamos obtener los usuarios de un grupo en particular, basta con usarlo de este modo:



List<ADUser> users = SecurityContextEx.GetUsersInGroup("My users");


Nota: En ocasiones, puede resultar una buena práctica para la administración de la seguridad de nuestras aplicaciones, crear un grupo en el directorio activo con el mismo nombre de la aplicación. Y de este modo conceder acceso a todos los miembros de dicho grupo a nuestra aplicación. En este caso todavía resultaría más sencillo mostrar todos los usuarios a los que hemos concedido acceso:



List<ADUser> users = SecurityContextEx.GetUsersInGroup(Application.ProductName);


Espero que os haya gustado,



Un saludo desde las frías tierras de Andorra :-)



Y después de la nevada de ayer, esta vez más frías que nunca…



Noviembre 2009


** crossposting desde el blog de Lluís Franco en geeks.ms **

HowTo: Agrupando grupos :-)

Una entrada rápida, no como la de ayer. Aunque el tema está bastante relacionado ya que ambos tratan de Active Directory e identidades.

GroupPolicy

Hoy vamos a ver una forma sencilla de obtener todos los grupos a los que pertenece un usuario, y agruparlos por su nombre de dominio. Y todo esto mediante una sola sentencia LINQ to objects. A ver quién es el guapo o guapa que me dice que LINQ to objects no es una maravilla!

El resultado que vamos a obtener es el siguiente (algunos nombres se han omitido por razones obvias :-P):

Groups under:
  – Group name: Todos
  – Group name: LOCAL
Groups under: BUILTIN
  – Group name: Usuarios
  – Group name: Administradores
Groups under: PRIMARY_DOMAIN_NAME
  – Group name: xxx1
  – Group name: xxx2
  – Group name: xxx3
  – Group name: xxx4
  – Group name: xxxN
Groups under: NT AUTHORITY
  – Group name: INTERACTIVE
  – Group name: Usuarios autentificados

Y el código resultante es un método extensor para la clase WindowsIdentity, con dos funciones auxiliares en forma de métodos extensores de la clase NTAccount:

using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.DirectoryServices.ActiveDirectory;
using System.Linq;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Text;
 
namespace Alpha.Code
{
    public static class SecurityExtensions
    {
        public static IOrderedEnumerable<IGrouping <string, NTAccount>> 
            GetGroupsUnderDomains(this WindowsIdentity identity)
        {
            var groups =
                        from grIdentity in identity.Groups
                        where grIdentity.IsValidTargetType(typeof(NTAccount))
                        select grIdentity.Translate(typeof(NTAccount)) as NTAccount into ntAccounts
                        let domainName = ntAccounts.GetDomainName()
                        let groupName = ntAccounts.GetAccountName()
                        orderby domainName
                        group ntAccounts by domainName into domainGroups
                        orderby domainGroups.Key
                        select domainGroups;
            return groups;
        }
 
        public static string GetDomainName(this NTAccount account)
        {
            string[] split = account.Value.Split('\\');
            return split.Length == 1 ? string.Empty : split[0];
        }
 
        public static string GetAccountName(this NTAccount account)
        {
            string[] split = account.Value.Split('\\');
            return split[split.Length - 1];
        }
    }
}


Para probarlo y ver el resultado:



var groups = WindowsIdentity.GetCurrent().GetGroupsUnderDomains();
foreach (var dg in groups)
{
    Console.WriteLine(string.Format("Groups under: {0}", dg.Key));
    foreach (var g in dg)
    {
        Console.WriteLine(string.Format("  - Group name: {0}", g.GetAccountName()));
    }
}


Un saludo desde las frías tierras de Andorra :-)



Noviembre 2009


** crossposting desde el blog de Lluís Franco en geeks.ms **

How To: ¿Como saber si el usuario actual es administrador del dominio?

Nota: Es una pregunta que me encuentro de forma recurrente en los foros de desarrollo, así que lo apunto aquí para tener una referencia.


El escenario


Cuando desarrollamos una aplicación de escritorio, puede ser interesante saber a qué grupos pertenece el usuario que está ejecutando nuestra aplicación, para mostrar / ocultar / permitir / revocar ciertas acciones, u opciones. Por ejemplo, yo acostumbro a tener un botón en la barra de estado de mis aplicaciones que permite cambiar la cadena de conexión, y evidentemente, solo está visible cuando el usuario pertenece al grupo “Administradores del dominio”.


privileges


IsInRole


Para ello, el objeto WindowsPrincipal dispone de un método IsInRole, que nos dirà si un usuario pertenece a un grupo determinado. Genial, además este método tiene varias sobrecargas, de modo que podemos usarlo pasando el RID, SID, el nombre e incluso una constante basada en la enumeración WindowsBuiltIOnRole:


Nombre de miembro Descripción
AccountOperator Los operadores de cuentas administran las cuentas de los usuarios de un equipo o dominio.
Administrator Los administradores tienen acceso completo y sin restricciones al equipo o dominio.
BackupOperator Los operadores de copia de seguridad pueden reemplazar las restricciones de seguridad con el único propósito de hacer copias de seguridad de los archivos o de restaurarlas.
Guest Los invitados tienen más restricciones que los usuarios.
PowerUser Los usuarios avanzados poseen la mayoría de los permisos administrativos, con algunas restricciones. De este modo, los usuarios avanzados pueden ejecutar aplicaciones heredadas, además de aplicaciones certificadas.
PrintOperator Los operadores de impresión pueden tomar el control de una impresora.
Replicator Los replicadores permiten la duplicación de archivos en un dominio.
SystemOperator Los operadores del sistema administran un equipo en particular.
User Los usuarios no pueden realizar cambios accidentales o intencionados en todo el sistema. En consecuencia, pueden ejecutar aplicaciones certificadas, pero no la mayoría de las aplicaciones heredadas. 

De modo que para saber si nuestro usuario es administrador local, basta con hacer esto:


WindowsPrincipal wp = new WindowsPrincipal(WindowsIdentity.GetCurrent());
return wp.IsInRole(WindowsBuiltInRole.Administrator);

Sencillo, verdad? A partir del usuario que ejecuta nuestra aplicación (válido también en caso de impersonación), creamos un objeto Principal e invocamos al método pasándole el grupo contra el que deseamos validar.


El problema de esta enumeración es que como su nombre indica, sólo contempla los grupos locales. Así que si queremos saber si pertenece a un grupo del dominio parece que tendremos que buscar el SID del grupo, o el nombre, y hardcodearlo ‘a mano’ en nuestra aplicación.


Feo verdad? Pues la verdad es que si, muy feo… vamos a investigar un poco más, a ver si encontramos otra forma.


Nota: Por motivos de rendimiento, para determinar la función del usuario se recomienda utilizar la sobrecarga de IsInRole(SecurityIdentifier) como sobrecarga preferible.


WellKnownSidType


Existe una enumeración llamada WellKnownSidType, que devuelve los identificadores de seguridad más utilizados, vamos a darle un vistazo:


Member name

Description
NullSid Indicates a null SID.
WorldSid Indicates a SID that matches everyone.
LocalSid Indicates a local SID.
CreatorOwnerSid Indicates a SID that matches the owner or creator of an object.
CreatorGroupSid Indicates a SID that matches the creator group of an object.
CreatorOwnerServerSid Indicates a creator owner server SID.
CreatorGroupServerSid Indicates a creator group server SID.
NTAuthoritySid Indicates a SID for the Windows NT authority.
DialupSid Indicates a SID for a dial-up account.
NetworkSid Indicates a SID for a network account. This SID is added to the process of a token when it logs on across a network.
BatchSid Indicates a SID for a batch process. This SID is added to the process of a token when it logs on as a batch job.
InteractiveSid Indicates a SID for an interactive account. This SID is added to the process of a token when it logs on interactively.
ServiceSid Indicates a SID for a service. This SID is added to the process of a token when it logs on as a service.
AnonymousSid Indicates a SID for the anonymous account.
ProxySid Indicates a proxy SID.
EnterpriseControllersSid Indicates a SID for an enterprise controller.
SelfSid Indicates a SID for self.
AuthenticatedUserSid Indicates a SID for an authenticated user.
RestrictedCodeSid Indicates a SID for restricted code.
TerminalServerSid Indicates a SID that matches a terminal server account.
RemoteLogonIdSid Indicates a SID that matches remote logons.
LogonIdsSid Indicates a SID that matches logon IDs.
LocalSystemSid Indicates a SID that matches the local system.
LocalServiceSid Indicates a SID that matches a local service.
NetworkServiceSid Indicates a SID that matches a network service.
BuiltinDomainSid Indicates a SID that matches the domain account.
BuiltinAdministratorsSid Indicates a SID that matches the administrator account.
BuiltinUsersSid Indicates a SID that matches built-in user accounts.
BuiltinGuestsSid Indicates a SID that matches the guest account.
BuiltinPowerUsersSid Indicates a SID that matches the power users group.
BuiltinAccountOperatorsSid Indicates a SID that matches the account operators account.
BuiltinSystemOperatorsSid Indicates a SID that matches the system operators group.
BuiltinPrintOperatorsSid Indicates a SID that matches the print operators group.
BuiltinBackupOperatorsSid Indicates a SID that matches the backup operators group.
BuiltinReplicatorSid Indicates a SID that matches the replicator account.
BuiltinPreWindows2000CompatibleAccessSid Indicates a SID that matches pre-Windows 2000 compatible accounts.
BuiltinRemoteDesktopUsersSid Indicates a SID that matches remote desktop users.
BuiltinNetworkConfigurationOperatorsSid Indicates a SID that matches the network operators group.
AccountAdministratorSid Indicates a SID that matches the account administrators group.
AccountGuestSid Indicates a SID that matches the account guest group.
AccountKrbtgtSid Indicates a SID that matches the account Kerberos target group.
AccountDomainAdminsSid Indicates a SID that matches the account domain administrator group.
AccountDomainUsersSid Indicates a SID that matches the account domain users group.
AccountDomainGuestsSid Indicates a SID that matches the account domain guests group.
AccountComputersSid Indicates a SID that matches the account computer group.
AccountControllersSid Indicates a SID that matches the account controller group.
AccountCertAdminsSid Indicates a SID that matches the certificate administrators group.
AccountSchemaAdminsSid Indicates a SID that matches the schema administrators group.
AccountEnterpriseAdminsSid Indicates a SID that matches the enterprise administrators group.
AccountPolicyAdminsSid Indicates a SID that matches the policy administrators group.
AccountRasAndIasServersSid Indicates a SID that matches the RAS and IAS server account.
NtlmAuthenticationSid Indicates a SID present when the Microsoft NTLM authentication package authenticated the client.
DigestAuthenticationSid Indicates a SID present when the Microsoft Digest authentication package authenticated the client.
SChannelAuthenticationSid Indicates a SID present when the Secure Channel (SSL/TLS) authentication package authenticated the client.
ThisOrganizationSid Indicates a SID present when the user authenticated from within the forest or across a trust that does not have the selective authentication option enabled. If this SID is present, then OtherOrganizationSid cannot be present.
OtherOrganizationSid Indicates a SID present when the user authenticated across a forest with the selective authentication option enabled. If this SID is present, then ThisOrganizationSid cannot be present.
BuiltinIncomingForestTrustBuildersSid Indicates a SID that allows a user to create incoming forest trusts. It is added to the token of users who are a member of the Incoming Forest Trust Builders built-in group in the root domain of the forest.
BuiltinPerformanceMonitoringUsersSid Indicates a SID that matches the group of users that have remote access to schedule logging of performance counters on this computer.
BuiltinPerformanceLoggingUsersSid Indicates a SID that matches the group of users that have remote access to monitor the computer.
BuiltinAuthorizationAccessSid Indicates a SID that matches the Windows Authorization Access group.
WinBuiltinTerminalServerLicenseServersSid Indicates a SID is present in a server that can issue Terminal Server licenses.
MaxDefined Indicates the maximum defined SID in the WellKnownSidType enumeration.

BINGO!!! Parece que tenemos el SID del grupo de admnistradores del dominio (lo he marcado en rojo en la tabla anterior).


Ahora vamos a generar el SID del grupo de adminstradores del dominio y ya podemos volver a probar el método IsInRole:


WindowsPrincipal wp = new WindowsPrincipal(WindowsIdentity.GetCurrent());
SecurityIdentifier sid = new SecurityIdentifier(WellKnownSidType.AccountDomainAdminsSid, null);
return wp.IsInRole(sid);

Ops! Nuestro gozo en un pozo… se necesita informar el segundo argumento del constructor para el SID del grupo:


DomainSidError


DomainSid


¿Y que kkgrnn$# representa que es este identificador? Pues según pone en la ayuda del constructor, debe proporcionarse el SID del dominio para que el constructor pueda devolver algunos identificadores de WellKnownSidType, entre los cuales está el de los administradores del dominio.


Dicho de otro modo, o sabemos el SID de nuestro dominio o todo lo anterior no vale para nada… :-(


¿Y cómo podemos saber el SID de dominio? Después de buscar un ratito, lo único que he encontrado es una utilidad de consola llamada PsGetSid, que forma parte de las PSTools del inefable Mark Russinovich. Basta descargar esta utilidad y ejecutarla desde la consola de este modo para saber el SID de nuestro dominio (el nombre de dominio en formato “microsoft.com” o “net.volvo.com”):


DomainSidConsole


Sin embargo, me niego a tener que hacer esto para saber el identificador del dominio. Así que vamos a probar si podemos recuperar esta propiedad del esquema de AD mediante un DirectoryEntry. Para ello utilizaremos la clase Domain:


Domain d = Domain.GetDomain(new
    DirectoryContext(DirectoryContextType.Domain, getDomainName()));
using (DirectoryEntry de = d.GetDirectoryEntry())
{
    byte[] domSid = (byte[])de.Properties["objectSid"].Value;
    string sdomainSid = sIDtoString(domSid);
    Console.WriteLine(sdomainSid);
}   

Nota: Aquí necesitaremos dos funciones de apoyo, la primera nos devuelve el nombre del domino, y la segunda transforma el array de bits del SID en su representación textual:


public static string getDomainName()
{
    return IPGlobalProperties.GetIPGlobalProperties().DomainName;
}

public static string sIDtoString(byte[] sidBinary)
{
    SecurityIdentifier sid = new SecurityIdentifier(sidBinary, 0);
    return sid.ToString();
}

A todo esto el valor de la variable sdomainSid es el esperado!!! :-D


 


 


 


 


 


 


 


Poniéndolo todo junto


Al igual que el alegre bandolero, también soy un fanático de los métodos extensores, así que vamos a encapsular todo esto en un método que extienda la clase WindowsIdentity. Aquí va todo el código junto:


 
using System.DirectoryServices;
using System.DirectoryServices.ActiveDirectory;
using System.Net.NetworkInformation;
using System.Security.Principal;
 
namespace Alpha.Code
{
    public static class SecurityExtensions
    {
        public static bool IsDomainAdmin (this WindowsIdentity identity)
        {
            Domain d = Domain.GetDomain(new
                DirectoryContext(DirectoryContextType.Domain, getDomainName()));
            using (DirectoryEntry de = d.GetDirectoryEntry())
            {
                byte[] bdomSid = (byte[])de.Properties["objectSid"].Value;
                string sdomainSid = sIDtoString(bdomSid);
                WindowsPrincipal wp = new WindowsPrincipal(identity);
                SecurityIdentifier dsid = new SecurityIdentifier(sdomainSid);
                SecurityIdentifier dasid = new SecurityIdentifier(
                    WellKnownSidType.AccountDomainAdminsSid, dsid);
                return wp.IsInRole(dasid);
            }
        }
 
        public static string getDomainName()
        {
            return IPGlobalProperties.GetIPGlobalProperties().DomainName;
        }
 
        public static string sIDtoString(byte[] sidBinary)
        {
            SecurityIdentifier sid = new SecurityIdentifier(sidBinary, 0);
            return sid.ToString();
        }
    }
}

Y la forma de usarlo es tan sencilla como esto:


if (WindowsIdentity.GetCurrent().IsDomainAdmin())
{
    //Acciones a realizar si el usuario es administrador de dominio... 
}

Un saludo desde las frías tierras de Andorra :-)


Noviembre 2009


** crossposting desde el blog de Lluís Franco en geeks.ms **

VSTO: Tabla de valores para .FaceId

Nota rápida para todos los que alguna vez os toque desarrollar algun proyecto VSTO.


Al asignar una inagen a un botón de las barras de herramientas debes asignarle un identificador numérico de imágen. Durante mucho tiempo he buscado alguna tabla que contenga todos los identificadores sin éxito. Ayer, dí con este enlace:


http://www.kebabshopblues.co.uk/2007/01/04/visual-studio-2005-tools-for-office-commandbarbutton-faceid-property/


En el tenéis las tablas de imágenes e identificadores. Yo, por si acaso he hecho una copia por si algun dia se pierde el enlace :-)


Aquí están:


faceId_0000-0099


faceId_0100-0199


faceId_0200-0299


faceId_0300-0399


faceId_0400-0499


faceId_0500-0599


faceId_0600-0699


faceId_0700-0799


faceId_0800-0899


faceId_0900-0999


faceId_1000-1099


faceId_1100-1199


faceId_1200-1299


faceId_1300-1399


faceId_1400-1499


faceId_1500-1599


faceId_1600-1699


faceId_1700-1799


faceId_1800-1899


faceId_1900-1999


faceId_2000-2099


faceId_2100-2199


faceId_2200-2299


faceId_2300-2399


faceId_2400-2499


faceId_2500-2599


faceId_2600-2699


faceId_2700-2799


faceId_2800-2899


faceId_2900-2999


Que aproveche! :-)


** crossposting desde el blog de Lluís Franco en geeks.ms **