Return 具有 C# .NET Core 3.1 属性的组成员的 Active Directory
Return Active Directory for group members with attributes with C# .NET Core 3.1
我需要从 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; }
}
}
}
我在一篇关于 finding members of a group 的文章中写到了这一点,因为有时组成员资格可能是一件非常复杂的事情。但这是我放在那里的一种方法,可能足以满足您的情况。
我已将其修改为 return 一个 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 package to be able to use the System.DirectoryServices
namespace. This will limit you to only being able to run your application on Windows. If you need to run your app on non-Windows operating systems, you can look into the Novell.Directory.Ldap.NETStandard,但我无能为力。
由于我目前的回答与 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 个成员的嵌套组平均有 13000 毫秒。
为什么要查询AD两次的原因:
- 找到广告群获取群成员
- 从 User 获取 UserPrincipal 对象中不可用的自定义属性。
我需要从 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; }
}
}
}
我在一篇关于 finding members of a group 的文章中写到了这一点,因为有时组成员资格可能是一件非常复杂的事情。但这是我放在那里的一种方法,可能足以满足您的情况。
我已将其修改为 return 一个 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 package to be able to use the System.DirectoryServices
namespace. This will limit you to only being able to run your application on Windows. If you need to run your app on non-Windows operating systems, you can look into the Novell.Directory.Ldap.NETStandard,但我无能为力。
由于我目前的回答与 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 个成员的嵌套组平均有 13000 毫秒。
为什么要查询AD两次的原因:
- 找到广告群获取群成员
- 从 User 获取 UserPrincipal 对象中不可用的自定义属性。