简体   繁体   English

使用 C# 查找递归组成员资格 (Active Directory)

[英]Find Recursive Group Membership (Active Directory) using C#

I am looking to get a list of all of the groups that a user is a member of in Active Directory, both explicitly listed in the memberOf property list as well as implicitly through nested group membership.我希望获取用户在 Active Directory 中所属的所有组的列表,这些组都明确列出在 memberOf 属性列表中,也包括通过嵌套组成员身份隐式列出。 For example, if I examine UserA and UserA is a part of GroupA and GroupB, I also want to list GroupC if GroupB is a member of GroupC.例如,如果我检查 UserA 并且 UserA 是 GroupA 和 GroupB 的一部分,如果 GroupB 是 GroupC 的成员,我还想列出 GroupC。

To give you a bit more insight into my application, I will be doing this on a limited basis.为了让您更深入地了解我的应用程序,我将在有限的基础上进行此操作。 Basically, I want a security check occasionally that will list these additional memberships.基本上,我偶尔需要一个安全检查来列出这些额外的成员资格。 I will want to differentiate the two but that shouldn't be hard.我想区分这两者,但这应该不难。

My problem is that I have not found an efficient way to make this query work.我的问题是我还没有找到一种有效的方法来使这个查询工作。 The standard text on Active Directory ( This CodeProject Article ) shows a way to do this that is basically a recursive lookup. Active Directory 上的标准文本(此 CodeProject 文章)显示了一种执行此操作的方法,该方法基本上是递归查找。 That seems terribly inefficient.这似乎非常低效。 Even in my small domain, a user might have 30+ group memberships.即使在我的小域中,一个用户也可能拥有 30 多个组成员资格。 That means 30+ calls to Active Directory for one user.这意味着一个用户对 Active Directory 的调用超过 30 次。

I've looked into the following LDAP code to get all of the memberOf entries at once:我查看了以下 LDAP 代码以一次获取所有 memberOf 条目:

(memberOf:1.2.840.113556.1.4.1941:={0})

where {0} would be my LDAP path (ex: CN=UserA,OU=Users,DC=foo,DC=org).其中 {0} 将是我的 LDAP 路径(例如:CN=UserA,OU=Users,DC=foo,DC=org)。 However, it does not return any records.但是,它不返回任何记录。 The downside of this method, even if it worked, would be that I wouldn't know which group was explicit and which was implicit.这种方法的缺点是,即使它有效,我也不知道哪个组是显式的,哪个是隐式的。

That is what I have so far.这就是我到目前为止所拥有的。 I would like to know if there is a better way than the CodeProject article and, if so, how that could be accomplished (actual code would be wonderful).我想知道是否有比 CodeProject 文章更好的方法,如果有,如何实现(实际代码会很棒)。 I am using .NET 4.0 and C#.我正在使用 .NET 4.0 和 C#。 My Active Directory is at a Windows 2008 functional level (it isn't R2 yet).我的 Active Directory 处于 Windows 2008 功能级别(还不是 R2)。

Thirst thanks for this an interesting question.非常感谢这个有趣的问题。

Next, just a correction, you say:接下来,只是一个更正,你说:

I've looked into the following LDAP code to get all of the memberOf entries at once:我查看了以下 LDAP 代码以一次获取所有 memberOf 条目:

(memberOf:1.2.840.113556.1.4.1941:={0})

You don't make it work.你不让它工作。 I remember I make it work when I learnt about its existence, but it was in an LDIFDE.EXE filter.我记得当我知道它的存在时我让它工作了,但它在一个 LDIFDE.EXE 过滤器中。 So I apply it to ADSI in C# and it's still working.所以我将它应用到 C# 中的 ADSI 并且它仍然有效。 There were too much parenthesis in the sample I took from Microsoft, but it was working ( source in AD Search Filter Syntax ).我从 Microsoft 获取的示例中有太多括号,但它可以正常工作( AD Search Filter Syntax 中的源代码)。

According to your remark concerning the fact that we don't know if a user explicitly belongs to the group I add one more request.根据您关于我们不知道用户是否明确属于该组这一事实的评论,我再添加一个请求。 I know this is not very good, but it's the best I'am abable to do.我知道这不是很好,但这是我能做的最好的。

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();
}

In my test tree this give:在我的测试树中,这给出:

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

(edited) '1.2.840.113556.1.4.1941' is not working in W2K3 SP1, it begins to work with SP2. (已编辑)“1.2.840.113556.1.4.1941”在 W2K3 SP1 中不起作用,它开始在 SP2 中起作用。 I presume it's the same with W2K3 R2.我认为它与 W2K3 R2 相同。 It's supposed to work on W2K8.它应该适用于W2K8。 I test here with W2K8R2.我在这里用 W2K8R2 测试。 I'll soon be able to test this on W2K8.我很快就能在 W2K8 上对此进行测试。

If there is no way other than recursive calls (and I don't believe there is) then at least you can let the framework do the work for you: see the UserPrincipal.GetAuthorizationGroups method (in the System.DirectoryServices.AccountManagement namespace and introduced in.Net 3.5)如果除了递归调用之外别无他法(我不相信有),那么至少您可以让框架为您完成工作:请参阅UserPrincipal.GetAuthorizationGroups 方法(在System.DirectoryServices.AccountManagement命名空间中并引入in.Net 3.5)

This method searches all groups recursively and returns the groups in which the user is a member.此方法递归搜索所有组并返回用户所属的组。 The returned set may also include additional groups that system would consider the user a member of for authorization purposes.返回的集合还可能包括系统出于授权目的而将用户视为成员的其他组。

Compare with the results of GetGroups ("Returns a collection of group objects that specify the groups of which the current principal is a member") to see whether the membership is explicit or implicit.GetGroups的结果(“返回指定当前主体所属的组的组对象的集合”)的结果进行比较,以查看成员资格是显式的还是隐式的。

you can utilize the tokenGroups and tokenGroupsGlobalAndUniversal properties if you are on Exchange server.如果您在 Exchange 服务器上,则可以使用 tokenGroups 和 tokenGroupsGlobalAndUniversal 属性。 tokenGroups will give you all the security groups this user belongs to, including nested groups and domain users, users, etc tokenGroupsGlobalAndUniversal will include everything from tokenGroups AND distribution groups tokenGroups 将为您提供此用户所属的所有安全组,包括嵌套组和域用户、用户等 tokenGroupsGlobalAndUniversal 将包括来自 tokenGroups 和通讯组的所有内容

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
                    }
                }
            }
        }
    }

Use the ldap filter recursively but query for all groups returned after each query to reduce the number of round trips.递归使用 ldap 过滤器,但查询每次查询后返回的所有组以减少往返次数。

Ex:前任:

  1. Get all groups where user is a member获取用户是成员的所有组
  2. Get all groups where Step 1 Groups are members获取步骤 1 组是成员的所有组
  3. Get all groups where Step 2 Groups are members获取步骤 2 组是成员的所有组
  4. ... ...

In my experience there are rarely more then 5 but should definitiely be much less then 30.根据我的经验,很少有超过 5 个,但绝对应该少于 30 个。

Also:还:

  • Make sure to only pull the properties you are going to need back.确保只拉回您需要的属性。
  • Caching results can significantly aid performance but made my code much more complicated.缓存结果可以显着提高性能,但使我的代码更加复杂。
  • Make sure to utilize connection pooling.确保使用连接池。
  • Primary group has to be handled seperately主要组必须单独处理

If you are using .NET 3.5 or higher you can use the System.DirectoryServices.AccountManagement namespace which really makes this easy.如果您使用的是 .NET 3.5 或更高版本,则可以使用System.DirectoryServices.AccountManagement命名空间,这真的很容易。

See the related answer here:Active Directory nested groups在此处查看相关答案:Active Directory 嵌套组

    static List<SearchResult> 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<SearchResult> 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<string>(asPropsToLoad);
        if (Array.BinarySearch<string>(asPropsToLoad, sDN) < 0)
        {
            Array.Resize<string>(ref asPropsToLoad, asPropsToLoad.Length+1);
            asPropsToLoad[asPropsToLoad.Length-1] = sDN;
        }
        if (Array.BinarySearch<string>(asPropsToLoad, sOC) < 0)
        {
            Array.Resize<string>(ref asPropsToLoad, asPropsToLoad.Length+1);
            asPropsToLoad[asPropsToLoad.Length-1] = sOC;
        }

        List<SearchResult> lsr = new List<SearchResult>();

        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<lsr.Count;i++)
            if (lsr[i].Properties.Contains(sOC) && lsr[i].Properties[sOC].Contains(sOC_GROUP))
                lsr.AddRange(ad_find_all_members(a_SearchRoot, (string)lsr[i].Properties[sDN][0], asPropsToLoad));

        return lsr;
    }

    static void Main(string[] args)
    {
    foreach (var sr in ad_find_all_members("LDAP://DC=your-domain,DC=com", "CN=your-group-name,OU=your-group-ou,DC=your-domain,DC=com", new string[] { "sAMAccountName" }))
        Console.WriteLine((string)sr.Properties["distinguishedName"][0] + " : " + (string)sr.Properties["sAMAccountName"][0]);
    }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM