Friday, 10 August 2007

Range Retrieval using System.DirectoryServices.Protocols

Link pair attributes in Active Directory and ADAM can be quite big.  I don't know the official limit, but needless to say, for practical purposes you can assume they are quite large indeed.  By default, AD and ADAM will not return the entire attribute if it contains more than a certain number of values (1000 for Windows 2000 and 1500 for Windows 2003+ by default).  As such, if you truly want robust code, you need to always use what is called range retrieval for link value paired attributes.

Range retrieval is a process similar to paging in directory services, whereby you ask the directory for a certain range of particular attribute.  You know that you are using range retrieval when you see the attribute being requested in the following format:

"[attribute];range=[start]-[end]"

As an example, in the case of the 'member' attribute, you might ask for the first 1500 values like so:

"member;range=0-1499"

Notice, it is zero based so you need to take this into account.  The general algorithm as such is:

  1. Ask for as big as you can get.  This means use the "*" for the ending range to ask for it all.
  2. The directory will respond with either the actual max value (some integer), or with a "*" indicating you got everything.  If you got everything, you are done.
  3. If not, using the max value now as your step, repeatedly ask for larger and larger values inside a loop until the directory responds with a "*" as the end range.

We covered how to use range retrieval in SDS in our book, and you can download sample code that shows how from the book's website.  What we didn't cover was how to do it in SDS.P.

SDS.P is a layer closer the the metal than our ADSI based System.DirectoryServices (SDS).  As such, if you are expected to do range retrieval for SDS, you can be assured that you need to do it for SDS.P as well.  Adopting the code SDS, you get something like this (but modified to some extent):

static List<string> RangeRetrieve(

    LdapConnection connect, string dn, string attribute)

{

    int idx = 0;

    int step = 0;

    List<string> list = new List<string>();

 

    string range = String.Format(

       "{0};range={{0}}-{{1}}",

       attribute

       );

 

    string currentRange = String.Format(range, idx, "*");

 

    SearchRequest request = new SearchRequest(

        dn,

        String.Format("({0}=*)", attribute),

        SearchScope.Base,

        new string[] { currentRange }

        );

 

    SearchResultEntry entry = null;

    bool lastSearch = false;

 

    while (true)

    {

        SearchResponse response =

            (SearchResponse)connect.SendRequest(request);

 

        if (response.Entries.Count == 1) //should only be one

        {

            entry = response.Entries[0];

 

            //this might be optimized to find full step or just use 1000 for

            //compromise

            foreach (string attrib in entry.Attributes.AttributeNames)

            {

                currentRange = attrib;

                lastSearch = currentRange.IndexOf("*", 0) > 0;

                step = entry.Attributes[currentRange].Count;

            }

 

            foreach (string member in

                entry.Attributes[currentRange].GetValues(typeof(string)))

            {

                list.Add(member);

                idx++;

            }

 

            if (lastSearch)

                break;

 

            currentRange = String.Format(range, idx, (idx + step));

 

            request.Attributes.Clear();

            request.Attributes.Add(currentRange);

 

        }

        else

            break;

 

    }

    return list;

}

Happy coding... of course, if you are clever you will realize you can avoid all this range retrieval mess by using an attribute scope query (ASQ). :)

*edit: tried to fix the style for code to render in Google Reader correctly

Wednesday, 01 August 2007

Getting Active Directory Group Membership in .NET 3.5

I have previously covered pretty extensively the options for getting a user's group membership in Active Directory or ADAM (soon to be Active Directory LDS (Lightweight Directory Services)) here on the blog, in the forum, and in the book.  However, there is a new option for users of .NET 3.5 that should be of interest.

The Directory Services group at Microsoft has released in beta form a new API for dealing with a lot of the common things we need to do with users, groups, and computers in Active Directory, ADAM, and the local machine.  This API is called System.DirectoryServices.AccountManagement (or SDS.AM).  Here is a simple example of how to get a users groups (including nested, and primary):

static void Main(string[] args)
{
    PrincipalContext ctx = new PrincipalContext(ContextType.Domain);

    using (ctx)
    {
        Principal p = Principal.FindByIdentity(ctx, "ryandunn");

        using (p)
        {
            var groups = p.GetGroups();
            using (groups)
            {
                foreach (Principal group in groups)
                {
                    Console.WriteLine(group.SamAccountName + "-" + group.DisplayName);
                }
            }
        }

    }
    Console.ReadLine();
}

That's not too bad - in fact, it looks worse than it is because I am trying to make sure everything is wrapped in a 'using' statement where necessary.  The equivalent code to do this would be many times more (using DsCrackNames or LDAP searches) and would yield far less information being returned (just the DN in most cases).

Over the next few weeks and months, I intend to dig more deeply into this namespace and put some samples up here for everyone.  This is just a taste for now, but it should show you how powerful this namespace really is.

 *Updated to fix CSS renderings in Google Reader

Thursday, 05 April 2007

Transitive Link Value Filter for SP1

If hot-LDAP-filter-action is your thing, but you were let down in my last post since it required SP2 and Longhorn, then this should get you all hot and bothered again:  Hotfix for SP1.

I know, it's a hotfix - which means you have to contact Microsoft to get it.  But if you want to take advantage of the new LDAP_MATCHING_RULE_IN_CHAIN without upgrading to SP2 or Longhorn, then this is it.

Tuesday, 20 March 2007

Transitive Link Value Filter Evaluation

I was speaking with Eric Fleischman at the MVP summit this year and he told me about a neat feature you will find in Window Server 2003 SP2 and Longhorn server.  It is a new type of matching rule ID filter that allows for transitive link value evaluation.  This is one filter type that is incredibly useful and you will want to know about.

First some background: a matching rule ID filter is a special syntax filter that allows for an arbitrary search behavior as defined by the matching rule.  Active Directory and ADAM only shipped with two matching rules until recently: LDAP_MATCHING_RULE_BIT_AND (1.2.840.113556.1.4.803) and LDAP_MATCHING_RULE_BIT_OR (1.2.840.113556.1.4.804).  We commonly used these matching rules to check our bitwise flag values.  For instance, here is the key portion of the filter that specifies an account is disabled:

(userAccountControl:1.2.840.113556.1.4.803:=2)

Notice that we have the attribute name we are searching on, the rule OID we want to use (the AND rule), and the value to check (in decimal).  Pretty simple, right?

The new matching rule is called LDAP_MATCHING_RULE_IN_CHAIN and it has an OID of 1.2.840.113556.1.4.1941.  This new rule allows us to search across all DN-syntax attributes recursively and evaluate the entire tree of relationships (hence the transitive name).

Evaluating Group Membership

Where we will typically see this used is in group membership evaluation.  Specifically, it answers two questions that are constantly asked by developers:  What groups are my user a member of? and What users are in this group?

It is really simple to use this filter, so here we go.  The first question is, what groups is my user a member of?

(member:1.2.840.113556.1.4.1941:=CN=User1,OU=X,DC=domain,DC=com)

The next question, what users are in this group?

(memberOf:1.2.840.113556.1.4.1941:=CN=A Group,OU=Y,DC=domain,DC=com)

If we place base this search on the main partition and use a subtree search, it will return for us all the matches across the domain.  However, if we scope the second search to a specific user object and use a base search, it is a quick and dirty way of telling us if the user is a member of the group.  Hence, this would also work for a type of IsInRole() function:

public bool IsUserMember(DirectoryEntry user, string groupDN)
{
    string filter = String.Format(
        "(memberOf:1.2.840.113556.1.4.1941:={0})", groupDN);

    DirectorySearcher ds = new DirectorySearcher(
        user,
        filter,
        null,
        SearchScope.Base);

    return (ds.FindOne() != null);
}

Now, I want to also point out that this sort of code also makes me cringe a little bit thinking about the abuse that can occur...  Remember, it is performing a search each and every time you want to check group membership.  It is still a better idea to build the entire group membership of a user and store it in one of the IPrincipal classes and use the .IsInRole() functionality to keep network access to a minimum.

Creating an Org Chart

The other area where we will find this filter being pretty handy is when we want to find all the users that directly and indirectly report to a single person.  This is the typical situation when building org charts or trying to find the users for a mailing list.  Here is one such example:

public static void GetOrgChart(DirectoryEntry entry, string bossDN)
{
    string filter = String.Format(
        "(&(mail=*)(manager:1.2.840.113556.1.4.1941:={0}))",
        bossDN);

    DirectorySearcher ds = new DirectorySearcher(
        entry,
        filter
        );

    using (SearchResultCollection src = ds.FindAll())
    {
        foreach (SearchResult sr in src)
        {
            Console.WriteLine(sr.Properties["mail"][0]);
        }
    }
}
  

Performance

The next question we will want to answer for this new filter type is the performance relative to the code required to process this recursively.  I will use the code I presented here to test the recursive case and compare it to a simple filter of:

(memberOf:1.2.840.113556.1.4.1941:=CN=BigNestedGroup,OU=X,DC=Y)

So, to cut to the chase, how does the new filter compare to recursively chasing DN link pairs yourself?  Short answer... it doesn't.  The code I wrote for the book and here on the blog blows it away by a factor of 10.  I have to admit that before I ran the tests, I expected the new filter to run circles around my code and I was pretty shocked when the reverse was actually true.  I tested this using 3 nested groups each with 100 members and running both the recursive search and the transitive search 100 times and averaging the results.  To expand the lead group with 300 direct and indirect members took roughly half a second using a transitive filter (409ms) as compared to only (41ms) using the recursive search.  This would have a big impact on server apps serving many of these searches, but would probably not be a huge factor in client side apps or where a smallish number of these searches are performed.

Summary

The transitive filter is a great new addition and can greatly simplify the code that you have to write.  This new filter is not without pitfalls however.  Make sure you are cognizant of the performance tradeoffs that are inherent in choosing this filter as it is considerably slower to use. 

Monday, 24 July 2006

A Recursive Pattern For DN Syntax Attributes Part 2

I am revisiting this particular topic once again to finish it up.  Last time, we established a general pattern for searching any DN syntax attribute in Active Directory or ADAM and chasing down all the nested results in either direction (i.e. forward link to back link or vice versa).  The solution worked well with one caveat:  intermediate values.  We often do not want to capture the intermediate values, but only the end results.  As an example, if we were to expand the group membership for a group object (the ‘member’ attribute) to discover the users, we would not want to include the other nested groups as values in our results, but we would only want to include the users in those nested groups.  In other words, we would want to exclude the intermediate values.  This is different than another example, say of discovering an org chart by expanding the ‘directReports’ attribute where we would clearly want to know all the intermediate reports.

For the specific example of expanding a group object to get membership, I posted an example in the book that used recursion and specifically excluded the result if the ‘objectClass’ was ‘group’.  I posed the question, “was this necessary?”.  Or more specifically, “can we create a general solution that will deal with both cases for intermediate values?”

The answer, of course, is yes.  We can pretty easily create a solution that will allow us to keep the intermediate values or discard them if we want.  In the general case, we do not need to know the object types.  In our example in the book, I think it was more clear what was happening by putting knowledge of the object type.  However, it also could have been solved with a simple boolean and no knowledge of the objects themselves.  Here is the revised solution.  I am omitting the IADsPathname interface this time, but you can easily pull it from the last post.

public class RecursiveLinkPair

{

    DirectoryEntry entry;

    ArrayList members;

    Hashtable processed;

    string attrib;

    bool includeAll;

 

    public RecursiveLinkPair(DirectoryEntry entry, string attrib, bool includeIntermediate)

    {

        if (entry == null)

            throw new ArgumentNullException("entry");

 

        if (String.IsNullOrEmpty(attrib))

            throw new ArgumentException("attrib");

 

        this.includeAll = includeIntermediate;

        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",

                "objectClass" },

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

 

                    if (sr.Properties.Contains(this.attrib))

                    {

                        if (this.includeAll)

                            al.Add(dn);

 

                        SetNewPath(this.entry, dn);

                        al.AddRange(Expand(this.entry));

                    }

                    else

                        al.Add(dn);

                }

            }

        }

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

    }

}

This simple class uses the AttributeScopeQuery option to enumerate the attribute and then uses the utility interface IADsPathname to reset the entry as we recurse the results.  So, we now finally have a generic solution that will work on all DN syntax attributes in both directions and with or without intermediate values.

Wednesday, 31 May 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…

Monday, 11 July 2005

DsCrackNames in .NET

As I alluded to some time ago in my previous post, entitled “Enumerating Token Groups (tokenGroups) in .NET” there is another method to converting the collection of SIDs obtained from the ‘tokenGroups’ attribute.

An API is available to us that can conveniently convert all the SIDs in one call to a number of different formats for us.  There is a bit of pre-work involved to define the signature, setup some structures and whatnot, but it is very slick once you have it working.

I decided that a sample would be in order to demonstrate this one.  So here it is.

The usual caveats apply – this is not production code and I am an embarrassingly bad WinUI designer so give me a break.  The point of this exercise is to show you how to use this particular API in a somewhat practical sample.

Enjoy.  Feedback is welcomed.

 

Friday, 01 April 2005

Enumerating a user's groups in .NET

I have seen a number of techniques for enumerating a user's group in Active Directory and .NET.  Here is one that seems to crop up every now and then (note, I cleaned this up so at least we were calling Dispose() on our DirectoryEntrys):

        ArrayList al = new ArrayList();
        using (DirectoryEntry user = new DirectoryEntry(_adsPath, _username, _password, AuthenticationTypes.Secure))
        {
            object adsGroups = user.Invoke("Groups");
 
            foreach (object adsGroup in (IEnumerable)adsGroups)
            {
                using (DirectoryEntry group = new DirectoryEntry(adsGroup))
                {
                    al.Add(group.Name);
                }
            }
        }

As we can see, this one uses Reflection to grab the native IADsMembers interface.  We then enumerate the groups by placing each native object into a DirectoryEntry and consuming it as we wish.

What's wrong with this method?  At first glance, nothing really.  Sure, it seems a little more complex than just retrieving the 'memberOf' property from the user to begin with.  We also know that this will not expose the nested group relationships like enumerating the 'tokenGroup' attribute will.

No, the real issue with this is that we are at risk for a memory leak.  What would happen if an error occurred right after we .Invoke() and before the foreach loop finishes?  We would have left a bunch of native IADs objects in limbo.  Only the DirectoryEntry has the .Dispose() pattern that will calls the appropriate Marshal.ReleaseComObject() on the native IADs object.

Developers make mistakes and this one is fraught with peril.  Don't use it.  Instead, enumerate the 'memberOf' attribute or 'tokenGroups' and avoid the issue.

Wednesday, 09 March 2005

Enumerating Token Groups (tokenGroups) in .NET

The 'tokenGroups' attribute is a calculated attribute (we must use .RefreshCache() to get it) that exists for all users in Active Directory.  It contains a collection of SIDs for each security group that the user is a member of.  The advantage of this collection is that it only contains security groups, and it contains all security groups including nested and primary groups.  The disadvantage is that it is a little bit more complicated to do anything with this attribute.

There are two methods of enumerating the tokenGroups and returning the security groups for a user.  The first method is to use DsCrackNames on collection of SIDs and have the Win32 api return the groups in your choice of name formats.  This is a powerful and fast method, but you will need to rely on p/invoke and setting up some structures.  The other method is to build an LDAP query filter and then use the DirectorySearcher to find all the groups.  This method returns a SearchResult for each group which means you could additionally retrieve more information about the group as well as does not require any p/invoke code, so it is usually more palatable for users.

Here are the steps we would take to enumerate the groups:

1.Create a DirectoryEntry to serve as the SearchRoot for our DirectorySearcher
2.Bind to our user object with another DirectoryEntry and pull the 'tokenGroups' attribute
3.Iterate over each SID in the tokenGroup and build the LDAP filter (formatting the SID bytes correctly)
4.Search the Directory with the constructed filter
5.Iterate each returned SearchResult for your information.

Here is a sample VS.NET solution in C# that demonstrates this:  Enumerate Token Groups

The code for how to do this using DsCrackNames I will leave for another post. (UPDATESee here for DsCrackNames)


Updated 3/16/2005 - I realize that not everyone feels like digging around to find their GUID to use this sample (I initially created it for someone that only had their GUID), so I revisited this so it also accepts the user's login name as well. Download the updated example

Updated 7/13/2005 – Reader “Daniel” was kind enough to point out some errors that I had in my code.  I seem to have deleted my original code at some point and was recreating it from scratch… so I apparently forgot some very key things in the code.  My bad!  I should have tested it better.  These errors have been corrected.

Tuesday, 18 January 2005

Determining your Primary Group in Active Directory using .NET

The Primary Group in Active Directory is an interesting concept. You are assigned the 'Domain Users' group by default when your account is initially created. The Primary Group is somewhat treated differently than any other type of group in the domain.

Due to Windows 2000 Active Directory limitations, a group can hold up to 5000 members. This stems from the fact that membership is accounted for with the 'member' attribute on an AD group. Given that multi-valued attributes (MVAs) have a maximum limit of 5000 ('member' is a MVA), we can see why you typically cannot put more than 5000 users into a group. Since large domains can easily have over 5000 members, we can see that treating the Primary Group like any other type of group would not work. We would quickly run to the limit of the 'member' attribute for the Primary Group.

It was decided at some point that they would have to change how AD managed members for the Primary Group. Instead of keeping the membership on the group object itself, they would store an identifier on the user object that made it calculatable to figure out the Primary Group.

Because of the inconsistency of how the Primary Group was treated, it made it somewhat challenging to figure out what a user's Primary Group was. The WinNT: provider would enumerate all groups, but not tell you which one was the primary group. Just to be contrary, the LDAP: provider would enumerate all groups except the Primary Group. Instead, you could enumerate the 'tokenGroups' attribute, but this would only enumerate the security groups and still not tell you which one was the Primary Group either.

To help ADSI developers, Microsoft released a couple support articles that outlined 3 ways of figuring out the Primary Group.
http://support.microsoft.com/default.aspx?scid=kb;en-us;321360
and
http://support.microsoft.com/default.aspx?scid=kb;en-us;297951

These methods are still valid, but need to be adapted somewhat for .NET. The solution I will present here will be an all LDAP solution. The steps are quite simple:

1. Retrieve the user's SID in byte array format
2. Retrieve the user's Primary Group ID RID (Relative Identifier)
3. Overwrite the user's RID on their SID with the Primary Group RID
4. Construct a binding ADsPath and bind to the Directory.

In order:
1. Retrieve the user's SID in byte array format:
byte[] objectSid = user.Properties["objectSid"].Value as byte[];

2. Retrieve the user's Primary Group ID RID
//calculate the primaryGroupId
user.RefreshCache(new string[]{"primaryGroupId"});

3. Overwrite the user's RID on their SID with the Primary Group RID:
        private byte[] CreatePrimaryGroupSID(byte[] userSid, int primaryGroupID)
        {
            //convert the int into a byte array
            byte[] rid = BitConverter.GetBytes(primaryGroupID);

            //place the bytes into the user's SID byte array
            //overwriting them as necessary
            for (int i=0; i < rid.Length; i++)
            {
                userSid.SetValue(rid[i], new long[]{userSid.Length - (rid.Length - i)});
            }

            return userSid;
        }

4. Finally, construct a binding string from the returned byte[] array and bind
adPath = String.Format("LDAP://<SID={0}>", BuildOctetString(sidBytes));
DirectoryEntry de = new DirectoryEntry(adPath, null, null, AuthenticationTypes.Secure);

        private string BuildOctetString(byte[] bytes)
        {
            StringBuilder sb = new StringBuilder();

            for(int i=0; i < bytes.Length; i++)
            {
                sb.Append(bytes[i].ToString("X2"));
            }
            return sb.ToString();
        }

I have created a small VS.NET 2003 project to demonstrate this. You can download the sample here