Active Directory: Tune performance of function to

2020-05-09 17:10发布

问题:

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);
}

回答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]));
}