views:

3256

answers:

1

I'm trying to get all the direct reports of a User through Active Directory, recursively. So given a user, i will end up with a list of all users who have this person as manager or who have a person as manager who has a person as manager ... who eventually has the input user as manager.

My current attempt is rather slow:

private static Collection<string> GetDirectReportsInternal(string userDN, out long elapsedTime)
{
    Collection<string> result = new Collection<string>();
    Collection<string> reports = new Collection<string>();

    Stopwatch sw = new Stopwatch();
    sw.Start();

    long allSubElapsed = 0;
    string principalname = string.Empty;

    using (DirectoryEntry directoryEntry = new DirectoryEntry(string.Format("LDAP://{0}",userDN)))
    {
        using (DirectorySearcher ds = new DirectorySearcher(directoryEntry))
        {
            ds.SearchScope = SearchScope.Subtree;
            ds.PropertiesToLoad.Clear();
            ds.PropertiesToLoad.Add("directReports");
            ds.PropertiesToLoad.Add("userPrincipalName");
            ds.PageSize = 10;
            ds.ServerPageTimeLimit = TimeSpan.FromSeconds(2);
            SearchResult sr = ds.FindOne();
            if (sr != null)
            {
                principalname = (string)sr.Properties["userPrincipalName"][0];
                foreach (string s in sr.Properties["directReports"])
                {
                    reports.Add(s);
                }
            }
        }
    }

    if (!string.IsNullOrEmpty(principalname))
    {
        result.Add(principalname);
    }

    foreach (string s in reports)
    {
        long subElapsed = 0;
        Collection<string> subResult = GetDirectReportsInternal(s, out subElapsed);
        allSubElapsed += subElapsed;

        foreach (string s2 in subResult)
        {
        result.Add(s2);
        }
    }



    sw.Stop();
    elapsedTime = sw.ElapsedMilliseconds + allSubElapsed;
    return result;
}

Essentially, this function takes a distinguished Name as input (CN=Michael Stum, OU=test, DC=sub, DC=domain, DC=com), and with that, the call to ds.FindOne() is slow.

I found that it is a lot faster to search for the userPrincipalName. My Problem: sr.Properties["directReports"] is just a list of strings, and that is the distinguishedName, which seems slow to search for.

I wonder, is there a fast way to convert between distinguishedName and userPrincipalName? Or is there a faster way to search for a user if I only have the distinguishedName to work with?

Edit: Thanks to the answer! Searching the Manager-Field improved the function from 90 Seconds to 4 Seconds. Here is the new and improved code, which is faster and more readable (note that there is most likely a bug in the elapsedTime functionality, but the actual core of the function works):

private static Collection<string> GetDirectReportsInternal(string ldapBase, string userDN, out long elapsedTime)
{
    Collection<string> result = new Collection<string>();

    Stopwatch sw = new Stopwatch();
    sw.Start();
    string principalname = string.Empty;

    using (DirectoryEntry directoryEntry = new DirectoryEntry(ldapBase))
    {
        using (DirectorySearcher ds = new DirectorySearcher(directoryEntry))
        {
            ds.SearchScope = SearchScope.Subtree;
            ds.PropertiesToLoad.Clear();
            ds.PropertiesToLoad.Add("userPrincipalName");
            ds.PropertiesToLoad.Add("distinguishedName");
            ds.PageSize = 10;
            ds.ServerPageTimeLimit = TimeSpan.FromSeconds(2);
            ds.Filter = string.Format("(&(objectCategory=user)(manager={0}))",userDN);

            using (SearchResultCollection src = ds.FindAll())
            {
                Collection<string> tmp = null;
                long subElapsed = 0;
                foreach (SearchResult sr in src)
                {
                    result.Add((string)sr.Properties["userPrincipalName"][0]);
                    tmp = GetDirectReportsInternal(ldapBase, (string)sr.Properties["distinguishedName"][0], out subElapsed);
                    foreach (string s in tmp)
                    {
                    result.Add(s);
                    }
                }
            }
          }
        }
    sw.Stop();
    elapsedTime = sw.ElapsedMilliseconds;
    return result;
}
+5  A: 

First off, setting Scope to "subtree" is unnecessary when you already have the DN you are looking for.

Also, how about finding all objects whose "manager" property is the person you look for, then iterating them. This should generally be faster than the other way around.

(&(objectCategory=user)(manager=<user-dn-here>))

EDIT: The following is important but has only been mentioned in the comments to this answer so far:

When the filter string is built as indicated above, there is the risk of breaking it with characters that are valid for a DN, but have special meaning in a filter. These must be escaped:

*   as  \2a
(   as  \28
)   as  \29
\   as  \5c
NUL as  \00
/   as  \2f

// Arbitrary binary data can be represented using the same scheme.

EDIT: Setting the SearchRoot to the DN of an object, and the SearchScope to Base also is a fast way to pull a single object out of AD.

Tomalak
Thanks. I'll see how it performs without Subtree. For your second suggestion, that sounds interesting. I need to wrap my brain around a bit as the function still needs to be recursive, but i'll test that immediately.
Michael Stum
If I could vote you up 10 times, I would do so. Chaging the function to search for the manager improved it from running 90 Seconds to just 4 Seconds now.
Michael Stum
Beware that with that approach, you need to migrate the risk of breaking your filter string with characters that are valid in a DN but reserved in a filter string. On the top of my head, at least '#' needs to be escaped.
Tomalak
Thanks for that information. I found this in a book now: "The characters <,;+\"> cannot appear within a DN unless they are escaped with a \ character. Additionally, # must be escaped if it is the first character in the DN. "
Michael Stum
Actually, i think that i looked at the wrong source. The returned DN will always be properly escaped for a DN, but you were referring to the Filter: http://msdn.microsoft.com/en-us/library/aa746475.aspx => "Special Characters". I'll add a "Escape DN for Filter" function to my code then.
Michael Stum
You probably also need to check for circular references in the manager hierarchy. I doubt there is a mechanism in AD that prevents them.
Tomalak