Encuentre membresía grupal recursiva (Active Directory) usando C #

Estoy buscando obtener una lista de todos los grupos de los que un usuario es miembro en Active Directory, tanto explícitamente enumerados en la lista de propiedades memberOf como implícitamente a través de membresía de grupo nested. Por ejemplo, si examino UserA y UserA es una parte de GroupA y GroupB, también quiero incluir GroupC si GroupB es miembro de GroupC.

Para darle un poco más de información sobre mi solicitud, lo haré de forma limitada. Básicamente, quiero un control de seguridad ocasionalmente que enumere estas membresías adicionales. Querré diferenciarlos, pero eso no debería ser difícil.

Mi problema es que no he encontrado una forma eficiente de hacer que esta consulta funcione. El texto estándar en Active Directory ( este artículo de CodeProject ) muestra una forma de hacer esto que es básicamente una búsqueda recursiva. Eso parece terriblemente ineficiente. Incluso en mi pequeño dominio, un usuario puede tener más de 30 membresías grupales. Eso significa más de 30 llamadas a Active Directory para un usuario.

He revisado el siguiente código LDAP para obtener todas las entradas memberOf a la vez:

(memberOf:1.2.840.113556.1.4.1941:={0}) 

donde {0} sería mi ruta LDAP (por ejemplo: CN = UsuarioA, OU = Usuarios, DC = foo, DC = org). Sin embargo, no devuelve ningún registro. La desventaja de este método, incluso si funcionara, sería que no sabría qué grupo era explícito y cuál implícito.

Eso es lo que tengo hasta ahora. Me gustaría saber si hay una forma mejor que el artículo de CodeProject y, de ser así, cómo se podría lograr (el código real sería maravilloso). Estoy usando .NET 4.0 y C #. Mi Active Directory está en un nivel funcional de Windows 2008 (aún no es R2).

Sed, gracias por esto, es una pregunta interesante.

Luego, solo una corrección, dices:

He revisado el siguiente código LDAP para obtener todas las entradas memberOf a la vez:

 (memberOf:1.2.840.113556.1.4.1941:={0}) 

No lo haces funcionar. Recuerdo que lo hice funcionar cuando supe de su existencia, pero estaba en un filtro LDIFDE.EXE. Entonces lo aplico a ADSI en C # y todavía funciona. Había demasiados paréntesis en la muestra que tomé de Microsoft, pero estaba funcionando ( fuente en la syntax del filtro de búsqueda AD ).

De acuerdo con su observación sobre el hecho de que no sabemos si un usuario pertenece explícitamente al grupo, agrego una solicitud más. Sé que esto no es muy bueno, pero es lo mejor que puedo hacer.

 static void Main(string[] args) { /* Connection to Active Directory */ DirectoryEntry deBase = new DirectoryEntry("LDAP://WM2008R2ENT:389/dc=dom,dc=fr"); /* To find all the groups that "user1" is a member of : * Set the base to the groups container DN; for example root DN (dc=dom,dc=fr) * Set the scope to subtree * Use the following filter : * (member:1.2.840.113556.1.4.1941:=cn=user1,cn=users,DC=x) */ DirectorySearcher dsLookFor = new DirectorySearcher(deBase); dsLookFor.Filter = "(member:1.2.840.113556.1.4.1941:=CN=user1 Users,OU=MonOu,DC=dom,DC=fr)"; dsLookFor.SearchScope = SearchScope.Subtree; dsLookFor.PropertiesToLoad.Add("cn"); SearchResultCollection srcGroups = dsLookFor.FindAll(); /* Just to know if user is explicitly in group */ foreach (SearchResult srcGroup in srcGroups) { Console.WriteLine("{0}", srcGroup.Path); foreach (string property in srcGroup.Properties.PropertyNames) { Console.WriteLine("\t{0} : {1} ", property, srcGroup.Properties[property][0]); } DirectoryEntry aGroup = new DirectoryEntry(srcGroup.Path); DirectorySearcher dsLookForAMermber = new DirectorySearcher(aGroup); dsLookForAMermber.Filter = "(member=CN=user1 Users,OU=MonOu,DC=dom,DC=fr)"; dsLookForAMermber.SearchScope = SearchScope.Base; dsLookForAMermber.PropertiesToLoad.Add("cn"); SearchResultCollection memberInGroup = dsLookForAMermber.FindAll(); Console.WriteLine("Find the user {0}", memberInGroup.Count); } Console.ReadLine(); } 

En mi árbol de prueba esto da:

 LDAP://WM2008R2ENT:389/CN=MonGrpSec,OU=MonOu,DC=dom,DC=fr adspath : LDAP://WM2008R2ENT:389/CN=MonGrpSec,OU=MonOu,DC=dom,DC=fr cn : MonGrpSec Find the user 1 LDAP://WM2008R2ENT:389/CN=MonGrpDis,OU=ForUser1,DC=dom,DC=fr adspath : LDAP://WM2008R2ENT:389/CN=MonGrpDis,OU=ForUser1,DC=dom,DC=fr cn : MonGrpDis Find the user 1 LDAP://WM2008R2ENT:389/CN=MonGrpPlusSec,OU=ForUser1,DC=dom,DC=fr adspath : LDAP://WM2008R2ENT:389/CN=MonGrpPlusSec,OU=ForUser1,DC=dom,DC=fr cn : MonGrpPlusSec Find the user 0 LDAP://WM2008R2ENT:389/CN=MonGrpPlusSecUniv,OU=ForUser1,DC=dom,DC=fr adspath : LDAP://WM2008R2ENT:389/CN=MonGrpPlusSecUniv,OU=ForUser1,DC=dom,DC=fr cn : MonGrpPlusSecUniv Find the user 0 

(editado) ‘1.2.840.113556.1.4.1941’ no funciona en W2K3 SP1, comienza a funcionar con SP2. Supongo que es lo mismo con W2K3 R2. Se supone que funciona en W2K8. Pruebo aquí con W2K8R2. Pronto podré probar esto en W2K8.

Si no hay otro camino que las llamadas recursivas (y no creo que exista), al menos puede dejar que el marco haga el trabajo por usted: vea el método UserPrincipal.GetAuthorizationGroups (en el espacio de nombres System.DirectoryServices.AccountManagement e introduzca en .Net 3.5)

Este método busca a todos los grupos recursivamente y devuelve los grupos de los que el usuario es miembro. El conjunto devuelto también puede incluir grupos adicionales que el sistema consideraría como miembros del usuario para fines de autorización.

Compare con los resultados de GetGroups (“Devuelve una colección de objetos grupales que especifican los grupos de los cuales el actual director es miembro”) para ver si la membresía es explícita o implícita.

Utilice el filtro ldap de manera recursiva, pero consulte todos los grupos devueltos después de cada consulta para reducir el número de viajes redondos.

Ex:

  1. Obtener todos los grupos donde el usuario es miembro
  2. Obtenga todos los grupos donde los grupos del Paso 1 son miembros
  3. Obtenga todos los grupos donde los grupos del Paso 2 son miembros

En mi experiencia, rara vez hay más de 5, pero definitivamente debe ser mucho menos de 30.

También:

  • Asegúrese de retirar solo las propiedades que va a necesitar.
  • Los resultados del almacenamiento en caché pueden ayudar significativamente al rendimiento pero hacen que mi código sea mucho más complicado.
  • Asegúrese de utilizar la agrupación de conexiones.
  • El grupo primario debe manejarse por separado

puede utilizar las propiedades tokenGroups y tokenGroupsGlobalAndUniversal si está en el servidor de Exchange. tokenGroups le proporcionará todos los grupos de seguridad a los que pertenece este usuario, incluidos los grupos nesteds y usuarios de dominio, usuarios, etc. tokenGroupsGlobalAndUniversal incluirá todo desde tokenGroups Y grupos de distribución

 private void DoWorkWithUserGroups(string domain, string user) { var groupType = "tokenGroupsGlobalAndUniversal"; // use tokenGroups for only security groups using (var userContext = new PrincipalContext(ContextType.Domain, domain)) { using (var identity = UserPrincipal.FindByIdentity(userContext, IdentityType.SamAccountName, user)) { if (identity == null) return; var userEntry = identity.GetUnderlyingObject() as DirectoryEntry; userEntry.RefreshCache(new[] { groupType }); var sids = from byte[] sid in userEntry.Properties[groupType] select new SecurityIdentifier(sid, 0); foreach (var sid in sids) { using(var groupIdentity = GroupPrincipal.FindByIdentity(userContext, IdentityType.Sid, sid.ToString())) { if(groupIdentity == null) continue; // this group is not in the domain, probably from sidhistory // extract the info you want from the group } } } } } 

Si usa .NET 3.5 o superior, puede usar el espacio de nombres System.DirectoryServices.AccountManagement que realmente lo hace fácil.

Consulte la respuesta relacionada aquí: grupos nesteds de Active Directory

  static List ad_find_all_members(string a_sSearchRoot, string a_sGroupDN, string[] a_asPropsToLoad) { using (DirectoryEntry de = new DirectoryEntry(a_sSearchRoot)) return ad_find_all_members(de, a_sGroupDN, a_asPropsToLoad); } static List ad_find_all_members(DirectoryEntry a_SearchRoot, string a_sGroupDN, string[] a_asPropsToLoad) { string sDN = "distinguishedName"; string sOC = "objectClass"; string sOC_GROUP = "group"; string[] asPropsToLoad = a_asPropsToLoad; Array.Sort(asPropsToLoad); if (Array.BinarySearch(asPropsToLoad, sDN) < 0) { Array.Resize(ref asPropsToLoad, asPropsToLoad.Length+1); asPropsToLoad[asPropsToLoad.Length-1] = sDN; } if (Array.BinarySearch(asPropsToLoad, sOC) < 0) { Array.Resize(ref asPropsToLoad, asPropsToLoad.Length+1); asPropsToLoad[asPropsToLoad.Length-1] = sOC; } List lsr = new List(); using (DirectorySearcher ds = new DirectorySearcher(a_SearchRoot)) { ds.Filter = "(&(|(objectClass=group)(objectClass=user))(memberOf=" + a_sGroupDN + "))"; ds.PropertiesToLoad.Clear(); ds.PropertiesToLoad.AddRange(asPropsToLoad); ds.PageSize = 1000; ds.SizeLimit = 0; foreach (SearchResult sr in ds.FindAll()) lsr.Add(sr); } for(int i=0;i