Tuesday, January 18, 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.

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++)
            return sb.ToString();

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