Wednesday, May 31, 2006

A Recursive Pattern For DN-Syntax Attributes

A common task that any Active Directory developer will face is to expand group membership for a given group.  This is a trivially simple task if we are only interested in finding direct group membership.  Things can get much more complicated when nested or indirect group membership is also involved.  Consider the following:

Group A: members – “Timmy”, “Alice”, “Bob”, “Group B”

Group B: members – “Jake”, “Jimmy”, “Nelson”

If we wanted to fully expand the group membership of Group A, we would expect to see 6 users in this case.  The issue of course is that the membership for a group is held on the ‘member’ attribute.  If we were to inspect this attribute directly, we would see only 4 members, with one of them being ‘Group B’.  The problem of course is that we cannot tell just by looking at the ‘member’ attribute which one is a group and which ones are the object types we are interested in (in this case user objects).  Short of a naming convention to indicate which is a group, we would have to bind or search for each object and determine if it was a group in order to continue the search.  This is how it is done today using .NET 1.x.  In fact – you can find an example of this by reading Chapter 11 and specifically viewing Listing 11.7.  I am also glossing over a couple details for 1.x regarding large DN-syntax attributes – but you can read about that of course in Chapter 11 .  I also won’t post the code for expanding group membership using recursion in 2.0 because that code is already available for download from the book’s companion website (Listing 11.6).  However, I will talk about this as a basis to discover a more general pattern.

Introduced with Windows 2003 and available to .NET 2.0 users is a new type of search called Attribute Scoped Query (ASQ).  An ASQ allows us to scope our searches to any DN-syntax attribute.  This powerful feature is ideal for things like membership expansion, or indeed, any DN-syntax attribute that should be expanded.  By scoping our search on the DN, we can easily overcome some of the limitations in other methods (such as range retrieval), which leads to a simpler conceptual model.

Another example where we would want to expand DN-syntax attributes occurs when we are checking for employees managed directly and indirectly by a particular individual.  For instance, we might have an employee that manages 1 employee that in turn manages 15 other employees.  It is fair to say in most organizations that the first employee really manages 16 employees rather than just one.  We have to use recursion however to fully expand this relationship.  The employee carries their manager’s DN on the attribute ‘manager’, while the manager has a backlink to the employee on the ‘directReports’ attribute.  This is a similar class of problem as the group membership expansion.

If we look at these two examples, we should notice some similarities and some key differences.  In both cases, we have the DN of another object to describe the relationship.  In both cases, we need to chase that DN to see if it in turn references other DNs.  However, there are two key differences.  First, in the case of group membership, we don’t care to see the intermediate results (e.g. Group B), but only the fully expanded members.  Next, for group membership, we are chasing a multi-valued forward link (the ‘member’ attribute), while in the employee example, we are chasing the backlink (the ‘directReports’ attribute).  The question we should consider is whether these differences will change our pattern.  They would be almost identical problems if the employee was trying to figure out all their bosses, or the user was trying to determine all their group memberships.

<Jeopardy music playing> So, do the two differences matter?  What if the forward link is single-valued (‘manager’ as opposed to ‘member’ for example)? </Jeopardy music playing>

The answer is yes and no.  The problem with excluding intermediate results certainly must be accounted for, but whether or not it is the backlink or forward link actually has no bearing on the problem.  It turns out that ASQ searches work just fine with DN-syntax attributes of either type – even when the forward link is single-valued.

Ignoring the intermediate value issue for now, we can produce a generalized solution for any DN-syntax recursion:

public class RecursiveLinkPair
{   
    DirectoryEntry entry;
    ArrayList members;
    Hashtable processed;
    string attrib;
 
    public RecursiveLinkPair(DirectoryEntry entry, string attrib)
    {
        if (entry == null)
            throw new ArgumentNullException("entry");
 
        if (String.IsNullOrEmpty(attrib))
            throw new ArgumentException("attrib");
 
        this.attrib = attrib;
        this.entry = entry;
        this.processed = new Hashtable();
        this.processed.Add(
            this.entry.Properties[
                "distinguishedName"][0].ToString(),
            null
            );
 
        this.members = Expand(this.entry);
    }
 
    public ArrayList Members
    {
        get { return this.members; }
    }
 
    private ArrayList Expand(DirectoryEntry group)
    {
        ArrayList al = new ArrayList(5000);
 
        DirectorySearcher ds = new DirectorySearcher(
            entry,
            "(objectClass=*)",
            new string[] {
                this.attrib,
                "distinguishedName"
                },
            SearchScope.Base
            );
 
        ds.AttributeScopeQuery = this.attrib;
        ds.PageSize = 1000;
 
        using (SearchResultCollection src = ds.FindAll())
        {
            string dn = null;
            foreach (SearchResult sr in src)
            {
                dn = (string)
                    sr.Properties["distinguishedName"][0];
 
                if (!this.processed.ContainsKey(dn))
                {
                    this.processed.Add(dn, null);
                    al.Add(dn);
 
                    if (sr.Properties.Contains(this.attrib))
                    {
                        SetNewPath(this.entry, dn);
                        al.AddRange(Expand(this.entry));
                    }
                }
            }
        }
        return al;
    }
 
    //we will use IADsPathName utility function instead
    //of parsing string values.  This particular function
    //allows us to replace only the DN portion of a path
    //and leave the server and port information intact
    private void SetNewPath(DirectoryEntry entry, string dn)
    {
        IAdsPathname pathCracker = (IAdsPathname)new Pathname();
 
        pathCracker.Set(entry.Path, 1);
        pathCracker.Set(dn, 4);
 
        entry.Path = pathCracker.Retrieve(5);
    }
}
 
[ComImport, Guid("D592AED4-F420-11D0-A36E-00C04FB950DC"), InterfaceType(ComInterfaceType.InterfaceIsDual)]
internal interface IAdsPathname
{
    [SuppressUnmanagedCodeSecurity]
    int Set([In, MarshalAs(UnmanagedType.BStr)] string bstrADsPath, [In, MarshalAs(UnmanagedType.U4)] int lnSetType);
    int SetDisplayType([In, MarshalAs(UnmanagedType.U4)] int lnDisplayType);
    [return: MarshalAs(UnmanagedType.BStr)]
    [SuppressUnmanagedCodeSecurity]
    string Retrieve([In, MarshalAs(UnmanagedType.U4)] int lnFormatType);
    [return: MarshalAs(UnmanagedType.U4)]
    int GetNumElements();
    [return: MarshalAs(UnmanagedType.BStr)]
    string GetElement([In, MarshalAs(UnmanagedType.U4)] int lnElementIndex);
    void AddLeafElement([In, MarshalAs(UnmanagedType.BStr)] string bstrLeafElement);
    void RemoveLeafElement();
    [return: MarshalAs(UnmanagedType.Interface)]
    object CopyPath();
    [return: MarshalAs(UnmanagedType.BStr)]
    [SuppressUnmanagedCodeSecurity]
    string GetEscapedElement([In, MarshalAs(UnmanagedType.U4)] int lnReserved, [In, MarshalAs(UnmanagedType.BStr)] string bstrInStr);
    int EscapedMode { get; [SuppressUnmanagedCodeSecurity] set; }
}
 
[ComImport, Guid("080d0d78-f421-11d0-a36e-00c04fb950dc")]
internal class Pathname
{
}

Notice I am using the IADsPathName utility interface here.  Since recursion requires us to re-base our search each time, we need to robustly update our DirectoryEntry instance’s .Path each time.  This interface does a much better job than trying to write your own string parsing routines - especially when you need to preserve server and port information for things like ADAM.  Whenever you see interfaces like this, check to make sure someone hasn’t already done the setup work for you.  You can check the .NET framework or ActiveDs interop assembly using Reflector and find the declaration oftentimes.

The final issue we must deal with now is the problem of intermediate values – specifically how to exclude them.  I am going to leave that exercise to the reader with just a hint:

  • I only included 2 attributes in each search.  If you have already seen my solution for Group Expansion in the book (Listing 11.6), you will notice that I make use of a 3rd attribute (objectClass).  Did I need to?

Well, this is a long enough post for now.  Signing off…