0

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);
}
J Weezy
  • 3,507
  • 3
  • 32
  • 88

1 Answers1

1

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:

  1. Calling .Guid on the DirectoryEntry, like you have in your code.
  2. 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]));
}
Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • I have implemented the code you have provided. In the foreach(SearchResult), I am getting the error 'System.Runtime.InteropServices.COMException: 'Unknown error (0x80005000)' That is a boilerplate error. I have provided the code for getting the DN in the update to this question. The only difference is that the error is on an account that is disabled. Would that matter? – J Weezy May 03 '18 at 16:54
  • Which line is throwing the exception? – Gabriel Luci May 03 '18 at 17:03
  • Nevermind. For the ones you get in the search, there's another way. See my update. – Gabriel Luci May 03 '18 at 17:03
  • I did some testing with method 2. In the filter, I included one group and one user object. Then, when I looped over each object in the SearchResultCollection I notice that the de.PropertiesCollection is refreshed and populated with information for both objects. Based on your explanation, my understanding was that the Properties.ObjectGuid value should have been the only thing update, thus network traffic is not reduced. But, it appears the entire PropertyCollection is refreshed. Is my understanding correct? – J Weezy May 04 '18 at 17:48
  • The `new DirectoryEntry` part is only relevant to the members you are getting from the `member` attribute. For the rest that you are getting by the `DirectorySearcher`, use my updated method at the end of my answer. Don't create a `new DirectoryEntry` for those. – Gabriel Luci May 04 '18 at 18:07
  • @GabirelLuci Can I use the refresh cache method to get the objects SchemaClassName? I tried replacing "objectGUID" with "SchemaClassName" but it is null. I suspect that is bc RefreshCache only gets members of the Properties list contained in each object (i.e. a sub-list) whereas the SchemaClassName is a peer to the Properties list. Currently, I am using the first method in your answer (.SchemaClassName). – J Weezy May 05 '18 at 22:32