Reputation: 494
I need to retrieve certain attributes from members of a given group in Active Directory and I think I'm on the wrong track.
My current LDAP query looks like this:
(&(memberOf:1.2.840.113556.1.4.1941:={DN for group})(objectClass=user)(objectCategory={objectCategory}))
This is a rather heavy query, which in average takes 10 - 20 seconds regardless of the result contains 0, 1 or a 1000 users. The result can be replicated in C# and powershell (Get-ADGroup -LDAPFilter {Your filter})
A colleague of mine proposed implementing something similar to this powershell query
$group = "{samAccountName}"
$attributes = "employeeId","sn","givenName","telephoneNumber","mobile","hPRnr","cn","samAccountName","gender","company","reshId"
Get-ADGroupMember -Identity $group | Get-ADUser -Properties $attributes | select $attributes
Is it possible to use the api available in C# to implement the powershell query somehow or is there a better solution?
To clearify. I have a C# approach today which has an emphasis on LDAP. The average perfomance is between 10 - 15 seconds whether there are 0 or 1000 members in the AD group.
A complete example of how the code works with the following libraries added to the project:
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; }
}
}
}
Upvotes: 2
Views: 5508
Reputation: 40858
I've written about this in an article I wrote about finding members of a group, since group membership can be an oddly complicated thing sometimes. But here is a method that I put there that will likely be good enough for your case.
I've modified it to return a User
object like you are in your code. If you pass true
for the recursive
parameter, it will traverse nested groups. You should be able to modify it to suit your needs.
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;
}
You do have to pass it a DirectoryEntry
object for the group. If you already have the DN of the group, you can create it like this:
new DirectoryEntry($"LDAP://{dn.Replace("/", "\\/")}")
If you don't, you can find the group by its sAMAccountName
like this:
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
There is other code in my article for finding members from external trusted domains (if you have any trusted domains) and for finding users who have the group as their primary group, since the primary group relationship isn't visible in the group's member
attribute.
To use this code in .NET Core, you need to install the 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, but I can't help there.
Upvotes: 4
Reputation: 494
Since my current answer diverges heavily from Gabriel Lucis, I figured it be better to propose what I came up with:
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);
}
The average performance up to 50 members is about 500 to 1000 ms. The Code does not scale very well, a nested group with 1079 members had in average 13 000 ms.
The reason why I have to query AD two times:
Upvotes: 0