Active Directory: Tune performance of function to retrieve group members - c#

This post is a follow-up to the following:
Active Directory: DirectoryEntry member list <> GroupPrincipal.GetMembers()
I have a function that retrieves the distinguishedName attribute for all members of a group in Active Directory. This function is used in a much large script that retrieves all user and group objects (total run time is 7-10 minutes). My problem here is that the downstream SSIS Lookup on the distinguishedName is extremely slow. This is not surprising due to the fact that it is looking up a varchar(255) versus UniqueIdentifier (16 bytes). I could do a SQL Select on the source and then Merge Join, which would speed things up. But, I am noticing a potential race condition (see run time above) in the extract where group members exist without a matching distinguishedName. If this is the case, then I need to address that; however, a Merge Join won't fail the load whereas a Lookup can be set to fail the load.
So, I need to get the guid on-the-fly via the distinguishedName. However, when I try to use the below method, the performance of the GetGroupMemberList function drops substantially. Is there a better/faster way to get the group member guid via the distinguishedName?
Method (for both loops):
listGroupMemberGuid.Add(new DirectoryEntry("LDAP://" + member, null, null, AuthenticationTypes.Secure).Guid);
listGroupMemberGuid.Add(new DirectoryEntry("LDAP://" + user, null, null, AuthenticationTypes.Secure).Guid);
Function:
private List<string> GetGroupMemberList(string strPropertyValue, string strActiveDirectoryHost, int intActiveDirectoryPageSize)
{
// Variable declaration(s).
List<string> listGroupMemberDn = new List<string>();
string strPath = strActiveDirectoryHost + "/<GUID=" + strPropertyValue + ">";
const int intIncrement = 1500; // https://msdn.microsoft.com/en-us/library/windows/desktop/ms676302(v=vs.85).aspx
var members = new List<string>();
// The count result returns 350.
var group = new DirectoryEntry(strPath, null, null, AuthenticationTypes.Secure);
//var group = new DirectoryEntry($"LDAP://{"EnterYourDomainHere"}/<GUID={strPropertyValue}>", null, null, AuthenticationTypes.Secure);
while (true)
{
var memberDns = group.Properties["member"];
foreach (var member in memberDns)
{
members.Add(member.ToString());
}
if (memberDns.Count < intIncrement) break;
group.RefreshCache(new[] { $"member;range={members.Count}-*" });
}
//Find users that have this group as a primary group
var secId = new SecurityIdentifier(group.Properties["objectSid"][0] as byte[], 0);
/* Find The RID (sure exists a best method)
*/
var reg = new Regex(#"^S.*-(\d+)$");
var match = reg.Match(secId.Value);
var rid = match.Groups[1].Value;
/* Directory Search for users that has a particular primary group
*/
var dsLookForUsers =
new DirectorySearcher {
Filter = string.Format("(primaryGroupID={0})", rid),
SearchScope = SearchScope.Subtree,
PageSize = 1000,
SearchRoot = new DirectoryEntry(strActiveDirectoryHost)
};
dsLookForUsers.PropertiesToLoad.Add("distinguishedName");
var srcUsers = dsLookForUsers.FindAll();
foreach (SearchResult user in srcUsers)
{
members.Add(user.Properties["distinguishedName"][0].ToString());
}
return members;
}
Update 1:
Code for retrieving the DN in the foreach(searchResult):
foreach (SearchResult searchResult in searchResultCollection)
{
string strDn = searchResult.Properties["distinguishedName"][0].ToString();
var de = new DirectoryEntry("LDAP://" + strDn, null, null, AuthenticationTypes.Secure);
de.RefreshCache(new[] { "objectGuid" });
var guid = new Guid((byte[])de.Properties["objectGuid"].Value);
}

It will always be slower since you have to talk to Active Directory again for each member. But, you can minimize the amount of traffic that it does.
I did a couple quick tests, while monitoring network traffic. I compared two methods:
Calling .Guid on the DirectoryEntry, like you have in your code.
Using this method:
var de = new DirectoryEntry("LDAP://" + member, null, null, AuthenticationTypes.Secure);
de.RefreshCache(new [] {"objectGuid"});
var guid = new Guid((byte[]) de.Properties["objectGuid"].Value);
The second method had significantly less network traffic: less than 1/3rd on the first account, and even less for each account after (it seems to reuse the connections).
I know that if you use .Properties without calling .RefreshCache first, it will pull every attribute for the account. It seems like using .Guid does the same thing.
Calling .RefreshCache(new [] {"objectGuid"}); only gets the objectGuid attribute and nothing else and saves it in the cache. Then when you use .Properties["objectGuid"] it already has the attribute in the cache, so it doesn't need to make any more network connections.
Update:
For the ones you get in the search, just ask for the objectGuid attribute instead of the distinguishedName:
dsLookForUsers.PropertiesToLoad.Add("objectGuid");
var srcUsers = dsLookForUsers.FindAll();
foreach (SearchResult user in srcUsers)
{
members.Add(new Guid((byte[])user.Properties["objectGuid"][0]));
}

Related

Two Groups with the same sAMAccountName, using FindOne() to getting the second occurance of group

I have a user that i'm currently listing "memberOf". I wanted to get some details about each group the user is a member of, such as distinguishedName, last modified, and description... The problem is, I'm using FindOne() in my code and i have a couple groups with sAMAccountName that are duplicated in various domains. Is there a way to use FindOne() and get the second occurance of the group as I have it coded below, or do I need to rewrite and use FindAll() and handling it that way.
Relevant Code Below:
foreach (object item in groups)
{
string groupProp = string.Empty;
using (DirectoryEntry dirEntry = CreateDirectoryEntry())
{
using (DirectorySearcher dirSearcher2 = new DirectorySearcher(dirEntry))
{
dirSearcher2.Filter = string.Format("(sAMAccountName=" + item + ")");
dirSearcher2.PropertiesToLoad.Add("description");
dirSearcher2.PropertiesToLoad.Add("whenChanged");
dirSearcher2.PropertiesToLoad.Add("distinguishedName");
SearchResult searchResult2 = dirSearcher2.FindOne();
if (searchResult2 != null)
{
DirectoryEntry employee = searchResult2.GetDirectoryEntry();
string desc = string.Empty;
string date = string.Empty;
string dname = string.Empty;
if (employee.Properties["description"].Value != null)
{
desc = employee.Properties["description"].Value.ToString();
}
if (employee.Properties["whenChanged"].Value != null)
{
date = employee.Properties["whenChanged"].Value.ToString();
}
if (employee.Properties["distinguishedName"].Value != null)
{
dname = employee.Properties["distinguishedName"].Value.ToString();
if (dname.Contains("DC=academic"))
{
dname = "academic";
}
}
}
}
}
Relevant New Code:
using (var results = dirSearcher2.FindAll())
{
foreach (SearchResult searchResult2 in results)
{
html.Append("<tr><td>" + item.ToString() + "</td>");
if (searchResult2.Properties.Contains("description"))
{
desc = searchResult2.Properties["description"][0].ToString();
}
if (searchResult2.Properties.Contains("whenChanged"))
{
date = searchResult2.Properties["whenChanged"][0].ToString();
}
if (searchResult2.Properties.Contains("distinguishedName"))
{
dom = searchResult2.Properties["distinguishedName"][0].ToString();
if (dom.Contains("DC=academic"))
{
dname = "academic";
}
else if (dom.Contains("DC=office"))
{
dname = "office";
}
else
{
dname = "not listed";
}
}
html.Append("<td>" + desc + "</td><td>" + dname + "</td><td>" + date + "</td></tr>");
}
Essentially, I'm getting the same results as it was getting with my first code, IE not getting the correct information on the second Group. IE: i have two groups named AppDev, both are on different domains; however, both show academic in the display. When I look in AD, i see that the distiguished name shows DC=office on one group, though the code above isn't pulling that.
FindOne() only finds one. If you need to see more, you will need to use FindAll(). Just make sure you wrap the result in a using statement, since the documentation says that you can have memory leaks if you don't:
using (var results = dirSearcher2.FindAll()) {
foreach (SearchResult searchResult2 in results) {
//do stuff
}
}
If you only want to find 2 (for example, if you only need to know if more than one exists), then you can set the SizeLimit property of your DirectorySearcher to 2:
dirSearcher2.SizeLimit = 2;
A note about efficiency: When you use .GetDirectoryEntry() and then get the properties from the DirectoryEntry object, DirectoryEntry actually goes back out to AD to get those attributes, even though you already got them during your search. You already used PropertiesToLoad to ask for those attributes, so they are already available in your SearchResult object. Just be aware that all attributes in the Properties list of SearchResult are presented as arrays, so you always need to use [0], even if they are single-valued attributes in AD.
if (searchResult2.Properties.Contains("description")) {
desc = searchResult2.Properties["description"][0];
}
If also need to make sure you are searching the Global Catalog, which will return results from all domains in your forest. You do this by creating the DirectoryEntry that you use for your SearchRoot with GC:// instead of LDAP://. This tell it to use port 3268 (the GC port) rather than the default LDAP port (389). You are creating this object in your CreateDirectoryEntry() method.

Retrieve records from Active Directory using data structure algorithm in c#

I need to fetch active directory records and insert in SQL database. There are approximately 10,000 records. I have used this code:
List<ADUser> users = new List<ADUser>();
DirectoryEntry entry = new DirectoryEntry("LDAP://xyz.com");
ADUser userToAdd = null;
IList<string> dict = new List<string>();
DirectorySearcher search = new DirectorySearcher(entry);
search.Filter = "(&(objectClass=user))";
search.PropertiesToLoad.Add("samaccountname");
search.PageSize = 1000;
foreach (SearchResult result in search.FindAll())
{
DirectoryEntry user = result.GetDirectoryEntry();
if (user != null && user.Properties["displayName"].Value!=null)
{
userToAdd = new ADUser
{
FullName = Convert.ToString(user.Properties["displayName"].Value),
LanId = Convert.ToString(user.Properties["sAMAccountName"].Value)
};
users.Add(userToAdd);
}
}
How can I optimize the above code in terms of speed and space complexity? Can I use traversal in binary tree as Active Directory structure looks similar to binary tree.
The list returned by DirectorySearcher.FindAll() is just a list. So you can't traverse it any better than you already are.
To optimize this, don't use GetDirectoryEntry(). That is doing two things:
Making another network request to AD that is unnecessary since the search can return any attributes you want, and
Taking up memory since all those DirectoryEntry objects will stay in memory until you call Dispose() or the GC has time to run (which it won't till your loop finally ends).
First, add displayName to your PropertiesToLoad to make sure that gets returned too. Then you can access each property by using result.Properties[propertyName][0]. Used this way, every property is returned as an array, even if it is a single-value attribute in AD, hence you need the [0].
Also, if your app is staying open after this search completes, make sure you call Dispose() on the SearchResultCollection that comes out of FindAll(). In the documentation for FindAll(), the "Remarks" section indicates you could have a memory leak if you don't. Or you can just put it in a using block:
DirectorySearcher search = new DirectorySearcher(entry);
search.Filter = "(&(objectClass=user))";
search.PropertiesToLoad.Add("sAMAccountName");
search.PropertiesToLoad.Add("displayName");
search.PageSize = 1000;
using (SearchResultCollection results = search.FindAll()) {
foreach (SearchResult result in results) {
if (result.Properties["displayName"][0] != null) {
userToAdd = new ADUser {
FullName = (string) result.Properties["displayName"][0],
LanId = (string) result.Properties["sAMAccountName"][0]
};
users.Add(userToAdd);
}
}
}

DirectorySearcher SizeLimit and List.Sort producing unwanted results C#

I have some code which takes an input string of users to search for in Active Directory. I limit the number of results (to 20) returned for searches. I then sort the results by the Active Directory created date descending. However, if I have more than 20 results (e.g. 100) on a particular user (e.g. 'Smith'), I get 20 users (due to my limitiing of the results) sorted by created date, but it's the last 20 of the 100 sorted by created date, where I want the first 20. If I remove the SizeLimit, I get all 100 results in the correctly sorted order. Below is my code, not sure what needs to be adjusted.
public void getADSearchResults(string searchString)
{
//Create list to hold ADUser objects
List<ADUser> users = new List<ADUser>();
string[] allUsers = searchString.Split(new Char[] { ',' }, userSearchLimit);
var json = "";
foreach (string name in allUsers)
{
//Connect to the root of the active directory domain that this computer is bound to with the default credentials; seems to cover employee and provider OUs at minimum
var rootDirectory = new DirectoryEntry();
DirectorySearcher adSearch = new DirectorySearcher(rootDirectory);
adSearch.Filter = "(&(objectClass=user)(anr=" + name + "))";
//LIMIT NUMBER OF RESULTS PER USER SEARCHED
adSearch.SizeLimit = 20;
// Go through all entries from the active directory.
foreach (SearchResult adUser in adSearch.FindAll())
{
DirectoryEntry de = adUser.GetDirectoryEntry();
string userName = "";
userName = adUser.Properties["sAMAccountName"][0].ToString();
string createdDate = "";
createdDate = adUser.Properties["whenCreated"][0].ToString();
ADUser aduser = new ADUser(userName, createdDate);
users.Add(aduser);
}
}
users.Sort((x, y) => DateTime.Parse(y.createdDate).CompareTo(DateTime.Parse(x.createdDate)));
json = new JavaScriptSerializer().Serialize(new { users = users });
//return json;
HttpContext.Current.Response.Write(json);
}
public class ADUser
{
public ADUser(string UserName, string CreatedDate)
{
userName = UserName;
createdDate = CreatedDate;
...
}
I suppose you have a logical mistake there.
I suppose you need to cut this operation in two pieces:
1) Get the whole files and users.
2) Make a loop to critic if an user has appeared before and, if so, jump all his records until the next user.
The problem may be related to the fact you want to perform it in a single operation AND cutting only the 20 first/last records.
Good luck
PS: can you put a USER FILTER there? I mean, after has the whole files in a LIST filter it by user?

C# access msExchRecipientTypeDetails property by System.DirectoryServices.AccountManagement

I use the code snippet below from System.DirectoryServices.AccountManagement to search for user in ActiveDirectory.
The user.Name returns successfully, but how can I retrieve other properties from AD for the user, like msExchRecipientTypeDetails since it is not displayed in VisualStudio 2015 intelligence?
using (PrincipalContext adPrincipalContext = new PrincipalContext(ContextType.Domain, DOMAIN, USERNAME, PASSWORD))
{
UserPrincipal userPrincipal = new UserPrincipal(adPrincipalContext);
userPrincipal.SamAccountName = "user-ID";
PrincipalSearcher search = new PrincipalSearcher(userPrincipal);
foreach (var user in search.FindAll())
{
Console.WriteLine("hei " + user.Name);
// how to retrive other properties from AD like msExchRecipientTypeDetails??
}
}
You need to use DirectoryEntry for any custom attributes like that. Add a reference in your project to "System.DirectoryServices" if you haven't already. Since you already have a Principal object, you could do this to get the DirectoryEntry:
var de = user.GetUnderlyingObject();
And because msExchRecipientTypeDetails is a strange AD large integer, you have to jump through hoops to get the real value. Here's a solution from another question to get the value:
var adsLargeInteger = de.Properties["msExchRecipientTypeDetails"].Value;
var highPart = (Int32)adsLargeInteger.GetType().InvokeMember("HighPart", System.Reflection.BindingFlags.GetProperty, null, adsLargeInteger, null);
var lowPart = (Int32)adsLargeInteger.GetType().InvokeMember("LowPart", System.Reflection.BindingFlags.GetProperty, null, adsLargeInteger, null);
if (lowPart < 0) highPart++;
var recipientType = highPart * ((Int64)UInt32.MaxValue + 1) + lowPart;
Update: Three years later I had to look this up again and stumbled across my own answer. But it turns out that sometimes I was getting a negative value when I shouldn't. I found the answer here:
The work-around is to increase the value returned by the HighPart method by one whenever the value returned by the LowPart method is negative.
Therefore, I've added that bit: if (lowPart < 0) highPart++;

Get "Home Directory" attribute from active directory

I'm trying to get Home Directory attribute value from active directory..
I used the following code:
public static void GetExchangeServerByWwidLdap(string wwid)
{
var exchange = string.Empty;
using (var ds = new DirectorySearcher())
{
ds.SearchRoot = new DirectoryEntry("GC:something");
ds.SearchScope = SearchScope.Subtree;
//construct search filter
string filter = "(&(objectclass=user)(objectcategory=person)";
filter += "(employeeid=" + wwid + "))";
ds.Filter = filter;
string[] requiredProperties = new string[] { "homeDirectory", "homemta" };
foreach (String property in requiredProperties)
ds.PropertiesToLoad.Add(property);
SearchResult result = ds.FindOne();
}
}
When I check result object data, I'm seeing only 2 values: "homemta" and "adspath".
Where is the "homeDirectory" value?
I entered AD website and searched the same values for same users - through the website I can see the all the data I searched for so I assuming that I have code issue somewhere.
What am I doing wrong?
You're trying to retrieve homeDirectory from global catalog.
It’s not there.
You can e.g. bind to the user by ADsPath property (i.e. “LDAP://…” string), then query the homeDirectory attribute of that user.
Or, if you only have a single domain, you can search within that domain instead of searching the GC. In this case you'll be able to retrieve all the properties you want.

Categories