【问题标题】:Return Active Directory for group members with attributes with C# .NET Core 3.1为具有 C# .NET Core 3.1 属性的组成员返回 Active Directory
【发布时间】:2020-09-01 11:21:00
【问题描述】:

我需要从 Active Directory 中给定组的成员中检索某些属性,但我认为我走错了路。

我当前的 LDAP 查询如下所示:

(&(memberOf:1.2.840.113556.1.4.1941:={DN for group})(objectClass=user)(objectCategory={objectCategory}))

这是一个相当繁重的查询,无论结果包含 0、1 或 1000 个用户,平均需要 10 - 20 秒。结果可以在 C# 和 powershell 中复制(Get-ADGroup -LDAPFilter {Your filter})

我的一个同事提议实现类似于这个 powershell 查询的东西

$group = "{samAccountName}"

$attributes = "employeeId","sn","givenName","telephoneNumber","mobile","hPRnr","cn","samAccountName","gender","company","reshId"

Get-ADGroupMember -Identity $group | Get-ADUser -Properties $attributes | select $attributes

是否可以使用 C# 中可用的 api 以某种方式实现 powershell 查询或者是否有更好的解决方案?

清除。 我今天有一个 C# 方法,它强调 LDAP。无论 AD 组中有 0 个成员还是 1000 个成员,平均性能在 10 - 15 秒之间。

代码如何与添加到项目中的以下库一起工作的完整示例:

Microsoft.AspNet.WebApi.Client
Microsoft.Extensions.Logging.Log4Net.AspNetCore
Newtonsoft.Json
System.DirectoryServices
System.DirectoryServices.AccountManagement
System.Runtime.Serialization.Json
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.DirectoryServices;
using Microsoft.Extensions.Logging;

namespace ActiveDirectoryLibrary.Standard.Services
{
    public class LdapService
    {
        private ILogger _logger;
        private string PersonCategory = "ObjectCategoryForUser";

        public LdapService(ILogger logger)
        {
            _logger = logger;
        }

        public List<User> GetUserRecordsInNestedGroupDetailed(string nestedGroup, string ou)
        {
            var groupStopwatch = new Stopwatch();
            groupStopwatch.Start();
            var group = GetGroup(nestedGroup, ou);
            groupStopwatch.Stop();
            _logger.LogDebug(
                $"Method {nameof(GetUserRecordsInNestedGroupDetailed)}: Getting the group {nestedGroup} took {groupStopwatch.ElapsedMilliseconds} ms");

            if (group == null || !string.IsNullOrEmpty(group.DistinguishedName)) return new List<User>();

            //PersonCategory is the object category for a user object in Active Directory

            var ldapFilter =
                $"(&(memberOf:1.2.840.113556.1.4.1941:={group.DistinguishedName})(objectClass=user)(objectCategory={PersonCategory}))";
            var groupMembers = new List<User>();

            using (var adsEntry = new DirectoryEntry())
            {
                using (var ds = new DirectorySearcher(adsEntry))
                {
                    var stopwatch = new Stopwatch();
                    stopwatch.Start();
                    ds.Filter = ldapFilter;
                    ds.SearchScope = SearchScope.Subtree;
                    LoadAdUserProperties(ds);
                    var members = ds.FindAll();
                    stopwatch.Stop();
                    _logger.LogDebug(
                        $"Method {nameof(GetUserRecordsInNestedGroupDetailed)}: Time consumed {stopwatch.ElapsedMilliseconds} ms for {group.DistinguishedName}");
                    foreach (SearchResult sr in members)
                    {
                        groupMembers.Add(MapSearchResultToUser(sr));
                    }
                }
            }

            return groupMembers;
        }

        public Group GetGroup(string samAccountName, string ou)
        {
            using (var entry = new DirectoryEntry($"LDAP://{ou}"))
            {
                var ds = new DirectorySearcher(entry)
                {
                    Filter = "(&(objectcategory=group)(SamAccountName=" + samAccountName + "))"
                };
                var group = ds.FindOne();
                return group == null ? null : MapSearchResultToGroup(group);
            }
        }

        public static Group MapSearchResultToGroup(SearchResult @group)
        {
            var returnGroup = new Group
            {
                Changed = GetProperty<DateTime>(@group, "whenchanged"),
                SamAccountName = GetProperty<string>(group, "SamAccountName"),
                Description = GetProperty<string>(group, "Description"),
                Created = GetProperty<DateTime>(group, "whencreated"),
                DistinguishedName = GetProperty<string>(group, "distinguishedname"),
                Name = GetProperty<string>(group, "name")
            };
            return returnGroup;
        }

        private static void LoadAdUserProperties(DirectorySearcher ds)
        {
            ds.PropertiesToLoad.Add("reshid");
            ds.PropertiesToLoad.Add("employeeid");
            ds.PropertiesToLoad.Add("sn");
            ds.PropertiesToLoad.Add("givenname");
            ds.PropertiesToLoad.Add("gender");
            ds.PropertiesToLoad.Add("telephonenumber");
            ds.PropertiesToLoad.Add("mobile");
            ds.PropertiesToLoad.Add("cn");
            ds.PropertiesToLoad.Add("distinguishedName");
            ds.PropertiesToLoad.Add("samaccountname");
            ds.PropertiesToLoad.Add("companyname");
        }

        public static User MapSearchResultToUser(SearchResult userProperty)
        {

            var reshId = GetProperty<string>(userProperty, "reshid");
            var employeeElement = GetProperty<string>(userProperty, "employeeid");
            var surname = GetProperty<string>(userProperty, "sn");
            var givenname = GetProperty<string>(userProperty, "givenname");
            var gender = GetProperty<string>(userProperty, "gender");
            var phone = GetProperty<string>(userProperty, "telephonenumber");
            var mobile = GetProperty<string>(userProperty, "mobile");
            var hpr = GetProperty<string>(userProperty, "hprnr");
            var cn = GetProperty<string>(userProperty, "cn");
            var samAccountName = GetProperty<string>(userProperty, "samaccountname");
            var company = GetProperty<string>(userProperty, "company");
            var account = new User
            {
                EmployeeId = employeeElement,
                Sn = surname,
                GivenName = givenname,
                Gender = gender,
                Telephone = phone,
                Mobile = mobile,
                Cn = cn,
                SamAccountName = samAccountName,
                Company = company,
                ReshId = reshId
            };
            return account;
        }

        private static T GetProperty<T>(SearchResult userProperty, string key)
        {
            if (userProperty.Properties[key].Count == 1)
            {
                return (T) userProperty.Properties[key][0];
            }

            return default(T);
        }

        public class Group
        {
            public DateTime Changed { get; set; }
            public string SamAccountName { get; set; }
            public string Description { get; set; }
            public DateTime Created { get; set; }
            public string DistinguishedName { get; set; }
            public string Name { get; set; }
        }

        public class User
        {
            public string EmployeeId { get; set; }
            public string Sn { get; set; }
            public string GivenName { get; set; }
            public string Telephone { get; set; }
            public string OfficePhone { get; set; }
            public string Mobile { get; set; }
            public string Mail { get; set; }
            public string Cn { get; set; }
            public string SamAccountName { get; set; }
            public string Gender { get; set; }
            public string Company { get; set; }
            public string ReshId { get; set; }
        }
    }
}

【问题讨论】:

  • 您需要递归查询组层次结构,还是只对组的直接成员感兴趣?
  • 棘手的问题。在最坏的情况下,这是一个嵌套组,所以我相信我必须搜索整个树而不仅仅是第一层。

标签: c# .net powershell active-directory ldap


【解决方案1】:

我在一篇关于finding members of a group 的文章中已经写到了这一点,因为组成员身份有时可能是一件非常复杂的事情。但这里有一个我放在那里的方法,它可能对你的情况足够好。

我已经修改它以返回一个 User 对象,就像您在代码中一样。如果您为recursive 参数传递true,它将遍历嵌套组。您应该能够对其进行修改以满足您的需要。

public static IEnumerable<User> GetGroupMemberList(DirectoryEntry group, bool recursive = false) {
    var members = new List<User>();

    group.RefreshCache(new[] { "member" });

    while (true) {
        var memberDns = group.Properties["member"];
        foreach (string member in memberDns) {
            using (var memberDe = new DirectoryEntry($"LDAP://{member.Replace("/", "\\/")}")) {
                memberDe.RefreshCache(new[] { "objectClass", "samAccountName", "mail", "mobile" });

                if (recursive && memberDe.Properties["objectClass"].Contains("group")) {
                    members.AddRange(GetGroupMemberList(memberDe, true));
                } else {
                    members.Add(new User {
                        SamAccountName = (string) memberDe.Properties["samAccountName"].Value,
                        Mail = (string) memberDe.Properties["mail"].Value,
                        Mobile = (string) memberDe.Properties["mobile"].Value,
                    });
                }
            }
        }

        if (memberDns.Count == 0) break;

        try {
            group.RefreshCache(new[] {$"member;range={members.Count}-*"});
        } catch (COMException e) {
            if (e.ErrorCode == unchecked((int) 0x80072020)) { //no more results
                break;
            }
            throw;
        }
    }
    return members;
}

您必须为该组传递一个DirectoryEntry 对象。如果您已经拥有该组的 DN,则可以这样创建它:

new DirectoryEntry($"LDAP://{dn.Replace("/", "\\/")}")

如果没有,您可以通过sAMAccountName 找到该组,如下所示:

var groupSamAccountName = "MyGroup";
var ds = new DirectorySearcher($"(sAMAccountName={groupSamAccountName})") {
    PropertiesToLoad = { "cn" } //just to stop it from returning every attribute
};
var groupDirectoryEntry = ds.FindOne()?.GetDirectoryEntry();

var members = GetGroupMemberList(groupDirectoryEntry, false); //pass true if you want recursive members

我的文章中还有其他代码用于从外部受信任域(如果您有任何受信任域)查找成员以及用于查找将该组作为主要组的用户,因为主要组关系在组的member 属性。

要在 .NET Core 中使用此代码,您需要安装 Microsoft.Windows.Compatibility NuGet 包才能使用 System.DirectoryServices 命名空间。这将限制您只能在 Windows 上运行您的应用程序。如果您需要在非 Windows 操作系统上运行您的应用程序,您可以查看Novell.Directory.Ldap.NETStandard,但我无法提供帮助。

【讨论】:

  • 尽可能多地获取您需要的信息。所需的时间将取决于小组的规模。如果您需要更改它,我写了另一篇关于在使用 AD 编码时获取better performance 的文章。一不小心就很容易表现不佳。
  • 我的建议没有得到足够好的性能,所以我创建了一个基于 AccountManagment 的 PrincipalContext 和 DirectoryServices 的 LDAP 的解决方案来查询 AD。似乎与 AccountManagment 和 DirectoryServices 一样,它是或者当涉及到哪些属性可用以及它们的性能如何时。我什至检查了 Microsoft.ActiveDirectory.Management.dll(Powershell 使用),看看是否有任何快速且易于使用的东西,但我很快就打消了这个想法,因为 API 是针对 Powershell 而不是通过 C# 访问它.
  • 我很想知道您的解决方案。 AccountManagement 命名空间只是DirectoryServices 的包装,所以你可以用AccountManagement 做任何你不能用DirectoryServices 做的事情。出于同样的原因,您始终可以通过直接使用DirectoryServices 来获得更快的性能——这取决于您如何使用它。
  • 嗨加布里埃尔,我的解决方案是下面的答案。出于某种原因,我通过使用包装器和 DirectoryServices 来更快。
【解决方案2】:

由于我目前的答案与 Gabriel Lucis 大相径庭,我认为最好提出我的想法:

    public IEnumerable<User> GetContactDetailsForGroupMembersWithPrincipalContext(string samAccountName, string ou, bool recursive)
    {
        var ctx = new PrincipalContext(ContextType.Domain);
        var grp = GroupPrincipal.FindByIdentity(ctx, IdentityType.SamAccountName, samAccountName);
        var users = new List<User>();

        if (grp != null)
        {
            foreach (var principal in grp.GetMembers(true))
            {
                var member = (UserPrincipal) principal;
                var user = GetUser(member);
                if (user != null)
                {
                    users.Add(user);
                }
            }
        }

        return users;
    }

    private User GetUser(UserPrincipal member)
    {
        var entry = (DirectoryEntry) member.GetUnderlyingObject();
        var search = new DirectorySearcher(entry);
        search.PropertiesToLoad.Add("samAccountName");
        search.PropertiesToLoad.Add("mail");
        search.PropertiesToLoad.Add("mobile");
        var result = search.FindOne();
        var user = MapSearchResultToUser(result);
        return user;
    }

    public static User MapSearchResultToUser(SearchResult userProperty)
    {
        var mobile = GetProperty<string>(userProperty, "mobile");
        var mail = GetProperty<string>(userProperty, "mail");
        var samAccountName = GetProperty<string>(userProperty, "samaccountname");

        var account = new User
        {
            Mobile = mobile,
            Mail = mail,
            SamAccountName = samAccountName,
        };
        return account;
    }

    private static T GetProperty<T>(SearchResult userProperty, string key)
    {
        if (userProperty.Properties[key].Count == 1)
        {
            return (T)userProperty.Properties[key][0];
        }

        return default(T);
    }

最多 50 个成员的平均性能约为 500 到 1000 毫秒。该代码不能很好地扩展,一个有 1079 个成员的嵌套组平均有 13 000 毫秒。

我要查询AD两次的原因:

  1. 找到AD组获取组成员
  2. 从 User 获取自定义属性,这些属性在 UserPrincipal 对象中不可用。

【讨论】:

  • 我很难相信这执行得更快 :) 我更新了答案中的代码以返回一个 User 对象,就像你一样,并添加了代码来向你展示如何按名称查找组使用DirectorySearcher。测试一下。
  • 但是,您发布的这个新代码并不等同于您问题中的代码。您的原始代码正在递归搜索成员。这个新代码不是。如果您不需要递归成员,只需从原始代码中删除 :1.2.840.113556.1.4.1941:,这可能比其他任何东西都快。但它不会从其他域中获取成员(如果有的话)。
  • 并且在这个新代码中,您无需再使用DirectorySearcher 来查找帐户。只需使用entry.Properties["sAMAccountName"].Value。这就是为每个成员添加一个新的网络请求。
  • 现在测试 Gabriel 的答案,因为我的初衷是通过 LDAP 完成大部分工作,所以花了一段时间才知道你在做什么。但是,我低估了递归搜索的繁重程度。在查看 Powershells Get-AdGroupMember 如何开箱即用时,我也有点盲目。我已经测试了两个嵌套组,其中有超过 7000 个成员。我的原始代码限制为 1000 个条目(默认情况下,但可以限制),而 Gabriels 代码循环直到完成。是否可以找到嵌套组的大小?
  • 验证组是否有意义,以确保它不是嵌套组?
猜你喜欢
  • 1970-01-01
  • 2020-05-30
  • 1970-01-01
  • 2017-10-31
  • 1970-01-01
  • 1970-01-01
  • 2017-10-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多