如何获取特定用户的所有AD组?

50

我已经查看了这篇文章,但它并没有回答我的问题。我想要获取一个特定用户所在的所有活动目录组。

我编写了以下代码。但是我无法继续进行,因为我不知道如何给出过滤器以及如何访问属性。

class Program
{
    static void Main(string[] args)
    {
        DirectoryEntry de = new DirectoryEntry("LDAP://mydomain.com");
        DirectorySearcher searcher = new DirectorySearcher(de);
        searcher.Filter = "(&(ObjectClass=group))";
        searcher.PropertiesToLoad.Add("distinguishedName");
        searcher.PropertiesToLoad.Add("sAMAccountName");
        searcher.PropertiesToLoad.Add("name");
        searcher.PropertiesToLoad.Add("objectSid");
        SearchResultCollection results = searcher.FindAll();
        int i = 1;
        foreach (SearchResult res in results)
        {
            Console.WriteLine("Result" + Convert.ToString(i++));
            DisplayProperties("distinguishedName", res);
            DisplayProperties("sAMAccouontName", res);
            DisplayProperties("name", res);
            DisplayProperties("objectSid", res);
            Console.WriteLine();
        }

        Console.ReadKey();
    }

    private static void DisplayProperties(string property, SearchResult res)
    {
        Console.WriteLine("\t" + property);
        ResultPropertyValueCollection col = res.Properties[property];
        foreach (object o in col)
        {
            Console.WriteLine("\t\t" + o.ToString());
        }
    }
}

有什么想法吗?

好的。我从这里学到了 - http://www.eggheadcafe.com/software/aspnet/30375857/directory-searcher-not-returning-all-groups.aspx,memberOf不会返回用户的主要组。但是我可以获取primaryGroupID,它提供了组的RID(?),使用它我需要获取AD组。有什么想法吗? - NLV
请检查我最新发布的答案。你目前的方法并不是很有效。如果用户被分配到另一个域中的通用组,通用组将不会显示在memberOf属性中。 - Harvey Kwok
11个回答

45

你应该使用System.DirectoryServices.AccountManagement,它更加容易。这里有一篇很好的文章,向您介绍了此DLL中所有类的概述。

正如你所指出的那样,你当前的方法无法找到主要组。其实,这比你想象的要糟糕得多。还有一些情况它不起作用,例如来自另一个域的域本地组。你可以在这里查看详细信息。以下是如果使用System.DirectoryServices.AccountManagement切换到的代码,以下代码可以找到此用户分配给的直接组,包括主要组。

UserPrincipal user = UserPrincipal.FindByIdentity(new PrincipalContext (ContextType.Domain, "mydomain.com"), IdentityType.SamAccountName, "username");
foreach (GroupPrincipal group in user.GetGroups())
{
    Console.Out.WriteLine(group);
}

8
user.GetGroups() 非常缓慢! - Kevin .NET
@Kevin.NET很慢吗?基准测试?高性能解决方案? - Kiquenet
确实,这是一个简单的解决方案,但非常慢。foreach中的每个项目都需要一些时间,因此查询不是在1个单独的调用中完成的,而是每次循环。 - Mark
如果每次调用getgroups(),请在其上调用tolist()。groups = userPrincipal.GetAuthorizationGroups().ToList();速度相当快。 - Chazt3n
System.DirectoryServices.AccountManagement 可以用于查询一个对象,但是当查询500个对象时速度非常慢。原因是即使你只需要选择几个字段,这个类也会将所有内容都传输到网络上。 - Russ Ebbing

37

使用tokenGroups

DirectorySearcher ds = new DirectorySearcher();
ds.Filter = String.Format("(&(objectClass=user)(sAMAccountName={0}))", username);
SearchResult sr = ds.FindOne();

DirectoryEntry user = sr.GetDirectoryEntry();
user.RefreshCache(new string[] { "tokenGroups" });

for (int i = 0; i < user.Properties["tokenGroups"].Count; i++) {
    SecurityIdentifier sid = new SecurityIdentifier((byte[]) user.Properties["tokenGroups"][i], 0);
    NTAccount nt = (NTAccount)sid.Translate(typeof(NTAccount));
    //do something with the SID or name (nt.Value)
}

注意:这只获取安全组。


7
这段代码比我以前试过的其他代码(使用PrincipalContext)快了大约10倍。从获取用户所有组的约4秒降到了400毫秒。谢谢。 - Wolf5
@Wolf5 我不明白这个,我是否应该在每次迭代中将每个“NTAccount”添加到列表中? - Flexabust Bergson
请问是否有办法使用这种方法确定每个组嵌套的深度? - PKCS12

29

只需要查询“memberOf”属性并迭代返回结果即可,例如:

            search.PropertiesToLoad.Add("memberOf");
            StringBuilder groupNames = new StringBuilder(); //stuff them in | delimited

                SearchResult result = search.FindOne();
                int propertyCount = result.Properties["memberOf"].Count;
                String dn;
                int equalsIndex, commaIndex;

                for (int propertyCounter = 0; propertyCounter < propertyCount;
                    propertyCounter++)
                {
                    dn = (String)result.Properties["memberOf"][propertyCounter];

                    equalsIndex = dn.IndexOf("=", 1);
                    commaIndex = dn.IndexOf(",", 1);
                    if (-1 == equalsIndex)
                    {
                        return null;
                    }
                    groupNames.Append(dn.Substring((equalsIndex + 1),
                                (commaIndex - equalsIndex) - 1));
                    groupNames.Append("|");
                }

            return groupNames.ToString();

这只是将组名填充到groupNames字符串中,使用管道符分隔,但当您遍历时,可以对它们进行任何操作。


我知道已经有一段时间了,但是你知道如何将安全组与分发列表分开吗? - DDuffy
1
没关系。我解决了。添加“未来世代”的代码。提取组类型并将其作为第二个字符串添加。StringBuilder groupTypes = new StringBuilder(); secondequalsIndex = dn.IndexOf("=", equalsIndex+1); secondcommaIndex = dn.IndexOf(",", commaIndex+1); groupTypes.Append(dn.Substring((secondequalsIndex + 1),(secondcommaIndex - secondequalsIndex) - 1)); groupTypes.Append("|"); - DDuffy

7
这是我列出特定可分辨名称的所有组(直接和间接)的方法:
字符串1.2.840.113556.1.4.1941指定LDAP_MATCHING_RULE_IN_CHAIN。
此规则仅适用于应用于DN的过滤器。这是一个特殊的“扩展”匹配运算符,它遍历对象的祖先链直到根部找到匹配项。
在我的测试中,这种方法比UserPrincipal.GetGroups()方法快25倍。
注意:此方法和GetGroups()方法均不返回主要组(通常为域用户)。为了获取主要组名称,我已确认this method可行。
此外,我发现这个LDAP过滤器列表非常有用。
private IEnumerable<string> GetGroupsForDistinguishedName(DirectoryEntry domainDirectoryEntry, string distinguishedName)
{
    var groups = new List<string>();
    if (!string.IsNullOrEmpty(distinguishedName))
    {
        var getGroupsFilterForDn = $"(&(objectCategory=group)(member:1.2.840.113556.1.4.1941:={distinguishedName}))";
        using (DirectorySearcher dirSearch = new DirectorySearcher(domainDirectoryEntry))
        {
            dirSearch.Filter = getGroupsFilterForDn;
            dirSearch.PropertiesToLoad.Add("name");

            using (var results = dirSearch.FindAll())
            {
                foreach (SearchResult result in results)
                {
                    if (result.Properties.Contains("name"))
                        groups.Add((string)result.Properties["name"][0]);
                }
            }
        }
    }

    return groups;
}

1
使用 $"(&(objectCategory=group)(member:1.2.840.113556.1.4.1941:={distinguishedName}))" 的过滤器可以使查询更加高效。根据这里的第19条注释(https://social.technet.microsoft.com/wiki/contents/articles/5392.active-directory-ldap-syntax-filters.aspx),AD似乎使用组过滤器来限制它检查的对象数量。除此之外,这个方法非常好用 :) - neumann1990

6

这段代码可以更快地运行(比我之前版本的两倍快):

    public List<String> GetUserGroups(WindowsIdentity identity)
    {
        List<String> groups = new List<String>();

        String userName = identity.Name;
        int pos = userName.IndexOf(@"\");
        if (pos > 0) userName = userName.Substring(pos + 1);

        PrincipalContext domain = new PrincipalContext(ContextType.Domain, "riomc.com");
        UserPrincipal user = UserPrincipal.FindByIdentity(domain, IdentityType.SamAccountName, userName); // NGeodakov

        DirectoryEntry de = new DirectoryEntry("LDAP://RIOMC.com");
        DirectorySearcher search = new DirectorySearcher(de);
        search.Filter = "(&(objectClass=group)(member=" + user.DistinguishedName + "))";
        search.PropertiesToLoad.Add("cn");
        search.PropertiesToLoad.Add("samaccountname");
        search.PropertiesToLoad.Add("memberOf");

        SearchResultCollection results = search.FindAll();
        foreach (SearchResult sr in results)
        {
            GetUserGroupsRecursive(groups, sr, de);
        }

        return groups;
    }

    public void GetUserGroupsRecursive(List<String> groups, SearchResult sr, DirectoryEntry de)
    {
        if (sr == null) return;

        String group = (String)sr.Properties["cn"][0];
        if (String.IsNullOrEmpty(group))
        {
            group = (String)sr.Properties["samaccountname"][0];
        }
        if (!groups.Contains(group))
        {
            groups.Add(group);
        }

        DirectorySearcher search;
        SearchResult sr1;
        String name;
        int equalsIndex, commaIndex;
        foreach (String dn in sr.Properties["memberof"])
        {
            equalsIndex = dn.IndexOf("=", 1);
            if (equalsIndex > 0)
            {
                commaIndex = dn.IndexOf(",", equalsIndex + 1);
                name = dn.Substring(equalsIndex + 1, commaIndex - equalsIndex - 1);

                search = new DirectorySearcher(de);
                search.Filter = "(&(objectClass=group)(|(cn=" + name + ")(samaccountname=" + name + ")))";
                search.PropertiesToLoad.Add("cn");
                search.PropertiesToLoad.Add("samaccountname");
                search.PropertiesToLoad.Add("memberOf");
                sr1 = search.FindOne();
                GetUserGroupsRecursive(groups, sr1, de);
            }
        }
    }

2

这是对我有效的代码:

public ArrayList GetBBGroups(WindowsIdentity identity)
{
    ArrayList groups = new ArrayList();

    try
    {
        String userName = identity.Name;
        int pos = userName.IndexOf(@"\");
        if (pos > 0) userName = userName.Substring(pos + 1);

        PrincipalContext domain = new PrincipalContext(ContextType.Domain, "riomc.com");
        UserPrincipal user = UserPrincipal.FindByIdentity(domain, IdentityType.SamAccountName, userName);

        DirectoryEntry de = new DirectoryEntry("LDAP://RIOMC.com");
        DirectorySearcher search = new DirectorySearcher(de);
        search.Filter = "(&(objectClass=group)(member=" + user.DistinguishedName + "))";
        search.PropertiesToLoad.Add("samaccountname");
        search.PropertiesToLoad.Add("cn");

        String name;
        SearchResultCollection results = search.FindAll();
        foreach (SearchResult result in results)
        {
            name = (String)result.Properties["samaccountname"][0];
            if (String.IsNullOrEmpty(name))
            {
                name = (String)result.Properties["cn"][0];
            }
            GetGroupsRecursive(groups, de, name);
        }
    }
    catch
    {
        // return an empty list...
    }

    return groups;
}

public void GetGroupsRecursive(ArrayList groups, DirectoryEntry de, String dn)
{
    DirectorySearcher search = new DirectorySearcher(de);
    search.Filter = "(&(objectClass=group)(|(samaccountname=" + dn + ")(cn=" + dn + ")))";
    search.PropertiesToLoad.Add("memberof");

    String group, name;
    SearchResult result = search.FindOne();
    if (result == null) return;

    group = @"RIOMC\" + dn;
    if (!groups.Contains(group))
    {
        groups.Add(group);
    }
    if (result.Properties["memberof"].Count == 0) return;
    int equalsIndex, commaIndex;
    foreach (String dn1 in result.Properties["memberof"])
    {
        equalsIndex = dn1.IndexOf("=", 1);
        if (equalsIndex > 0)
        {
            commaIndex = dn1.IndexOf(",", equalsIndex + 1);
            name = dn1.Substring(equalsIndex + 1, commaIndex - equalsIndex - 1);
            GetGroupsRecursive(groups, de, name);
        }
    }
}

我在一个循环中进行了200次测试,将其性能与使用AttributeValuesMultiString递归方法的代码进行比较;结果它运行得更快1.3倍。 这可能是由于我们的AD设置造成的。尽管两个片段给出了相同的结果。


2
以下示例来自Code Project文章“(几乎)通过C#访问Active Directory中的所有内容”:
// userDn is a Distinguished Name such as:
// "LDAP://CN=Joe Smith,OU=Sales,OU=domain,OU=com"
public ArrayList Groups(string userDn, bool recursive)
{
    ArrayList groupMemberships = new ArrayList();
    return AttributeValuesMultiString("memberOf", userDn,
        groupMemberships, recursive);
}

public ArrayList AttributeValuesMultiString(string attributeName,
     string objectDn, ArrayList valuesCollection, bool recursive)
{
    DirectoryEntry ent = new DirectoryEntry(objectDn);
    PropertyValueCollection ValueCollection = ent.Properties[attributeName];
    IEnumerator en = ValueCollection.GetEnumerator();

    while (en.MoveNext())
    {
        if (en.Current != null)
        {
            if (!valuesCollection.Contains(en.Current.ToString()))
            {
                valuesCollection.Add(en.Current.ToString());
                if (recursive)
                {
                    AttributeValuesMultiString(attributeName, "LDAP://" +
                    en.Current.ToString(), valuesCollection, true);
                }
            }
        }
    }
    ent.Close();
    ent.Dispose();
    return valuesCollection;
}

只需使用用户的区分名称调用Groups方法,并传入bool标志以指示是否要在结果ArrayList中包含嵌套/子组成员身份:
ArrayList groups = Groups("LDAP://CN=Joe Smith,OU=Sales,OU=domain,OU=com", true);
foreach (string groupName in groups)
{
    Console.WriteLine(groupName);
}

如果你需要在.NET中进行任何严格级别的Active Directory编程,我强烈建议你收藏并审阅上面提到的Code Project文章。

1
好的。我已经递归地找到了父组。但是仍然有两个组(或文件夹?)“Domain Users”没有出现在列表中。但是如果我检查“memberOf”选项卡,它就在那里。Domain users是内置组,对吗?我在这里漏掉了什么吗? - NLV

1
我想说的是,Microsoft LDAP 有一些特殊的方式来递归搜索用户的所有成员。
  1. 您可以为 "member" 属性指定匹配规则。特别地,使用 Microsoft 独有的 LDAP_MATCHING_RULE_IN_CHAIN 规则可以对 "member" 属性进行递归/嵌套成员搜索。当您在成员属性后添加此规则时,该规则将被使用。例如:(member:1.2.840.113556.1.4.1941:= XXXXX )

  2. 对于与账户相同的域,过滤器可以使用 <SID=S-1-5-21-XXXXXXXXXXXXXXXXXXXXXXX> 而不是帐户的 DistinguishedName 属性,这非常方便跨域使用。但是,如果需要解析 ForeignSecurityPrincipal 对象类型,则似乎需要使用 ForeignSecurityPrincipal <GUID=YYYY>,因为 <SID=> 标签不考虑 ForeignSecurityPrincipal 对象类型。您也可以使用 ForeignSecurityPrincipal DistinguishedName。

使用这些知识,您可以LDAP查询那些难以获取的成员资格,例如“域本地”组,一个账户是其中的成员,但除非您查看了组的成员,否则您不会知道用户是否是成员。

//Get Direct+Indirect Memberships of User (where SID is XXXXXX)

string str = "(& (objectCategory=group)(member:1.2.840.113556.1.4.1941:=<SID=XXXXXX>) )";

//Get Direct+Indirect **Domain Local** Memberships of User (where SID is XXXXXX)

string str2 = "(& (objectCategory=group)(|(groupType=-2147483644)(groupType=4))(member:1.2.840.113556.1.4.1941:=<SID=XXXXXX>) )";

//TAA DAA



在替换要检索所有组成员身份的用户的SID后,可以随意尝试这些LDAP查询。我认为这与PowerShell命令Get-ADPrincipalGroupMembership使用的查询相似,如果不是同一个查询。该命令说明:“如果您想在另一个域中搜索本地组,请使用ResourceContextServer参数指定其他域中的备用服务器。”

如果您足够熟悉C#和Active Directory,则应该知道如何使用提供的LDAP查询执行LDAP搜索。

附加文档:


如何使用提供的LDAP查询执行LDAP搜索? - Kiquenet

0

在 curtisk 响应中有一个基于 helpers 类的类:

    public static class ActiveDirectoryHelpers
    {
        private static readonly Regex keyValuePair = new Regex($"(?<key>[^=]+)=(?<value>[^,]+),?");

        public enum X500DirectorySpecification
        {
            /// <summary>Common Name</summary>
            CN,
            /// <summary>Organizational Unit</summary>
            OU,
            /// <summary>Domain Component</summary>
            DC
        }

        public static IEnumerable<string> GetUserMemberOfNodeValue(this PrincipalContext principalContext, string userName, X500DirectorySpecification node)
        {
            return principalContext.GetUserMemberOf(userName)
                .SelectMany(memberOf =>
                    GetUserMemberOfKeyValues(memberOf).Where(item => item.Key == node.ToString()).Select(item => item.Value));
        }

        private static IEnumerable<string> GetUserMemberOf(this PrincipalContext principalContext, string userName)
        {
            using var user = UserPrincipal.FindByIdentity(principalContext, userName);
            IEnumerable<string> result = null;
            if (user != null)
            {
                var directoryEntry = (DirectoryEntry)user.GetUnderlyingObject();
                var directorySearcher = new DirectorySearcher(directoryEntry);

                directorySearcher.PropertiesToLoad.Add("memberOf");
                result = directorySearcher.FindOne().Properties["memberOf"].Cast<string>();
            }

            return result ?? Enumerable.Empty<string>();
        }

        private static IEnumerable<KeyValuePair<string, string>> GetUserMemberOfKeyValues(string memberOfValue)
        {
            return keyValuePair.Matches(memberOfValue).OfType<Match>()
                .Select(item => new KeyValuePair<string, string>(item.Groups["key"].Value.Trim(), item.Groups["value"].Value));
        }
    }

0
如果您有一个LDAP连接,需要使用用户名和密码连接到Active Directory,这里是我用来正确连接的代码:
using System.DirectoryServices.AccountManagement;

// ...

// Connection information
var connectionString = "LDAP://domain.com/DC=domain,DC=com";
var connectionUsername = "your_ad_username";
var connectionPassword = "your_ad_password";

// Get groups for this user
var username = "myusername";

// Split the LDAP Uri
var uri = new Uri(connectionString);
var host = uri.Host;
var container = uri.Segments.Count() >=1 ? uri.Segments[1] : "";

// Create context to connect to AD
var princContext = new PrincipalContext(ContextType.Domain, host, container, connectionUsername, connectionPassword);

// Get User
UserPrincipal user = UserPrincipal.FindByIdentity(princContext, IdentityType.SamAccountName, username);

// Browse user's groups
foreach (GroupPrincipal group in user.GetGroups())
{
    Console.Out.WriteLine(group.Name);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接