Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Storer.ActiveDirectory - Active Directory User/Group Encapsulation Classes

4.79/5 (16 votes)
12 Apr 2007CDDL4 min read 1   528  
A couple of classes to handle Users and Groups in Active Directory

Introduction

For almost three years now I've been interfacing with Active Directory through C#. Utilizing DirectoryEntry-style access is slow and cumbersome when you're dealing with multiple users, especially when your goal is simply read-only access. Plus, remembering all the different parameters to use can be difficult when you're trying to write a quick program to solve something, and not a several-month project.

So, for all of you who use Active Directory (or are thinking about it), this class library is for you!

Here's the Idea

System.DirectoryServices has a nice search feature for Active Directory called DirectorySearcher. This class is much faster than a DirectoryEntry for accessing data in a user or group object.

The idea is this: set up a way to make it both fast and simple for Active Directory User/Group Access. With my class, it can be a simple as:

C#
static void Main(string[] args)
{
    try
    {
        // Find the User "Administrator" and View/Modify
        User _User = Search.ForUser(User.Properties.SAMACCOUNTNAME, 
            "Administrator");
        Console.WriteLine("Username:            {0}", 
            _User.SAMAccountName);
        Console.WriteLine("Full Name:           {0}", 
            _User.FullName);
        Console.WriteLine("DistinguishedName:   {0}", 
            _User.DistinguishedName);
        Console.WriteLine("Logon Count:         {0}",  
            _User.LogonCount);
        Console.WriteLine("Object SID (string)  {0}", 
            _User.ObjectSIDString);

        foreach (string GroupName in _User.TokenGroups)
            Console.WriteLine("Token Group:         {0}", 
                GroupName);

        _User.Enabled = false;
        _User.FirstName = "Bob";
        _User.MiddleInitial = "T";
        _User.LastName = "Admin";
        _User.SaveChanges();

        // Find the Group "Administrators" and View/Modify

        Group _Group = Search.ForGroup(Group.Properties.COMMONNAME, 
            "Administrators");
        Console.WriteLine("Group:               {0}", 
            _Group.CommonName);
        
        foreach (string _Member in _Group.Members)
            Console.WriteLine("Member:              {0}", _Member);

        _Group.AddMember(_User.DistinguishedName);
        _Group.SaveChanges();

    }
    catch (Exception Error)
    {
        Console.WriteLine("Error: {0}", Error);
    }
}

An extension to this is finding a list of Users or Groups, which can be done with the Search.ForUsers(...) or Search.ForGroups(...) method.

Using the code

Simply Add the reference to Storer.ActiveDirectory to your code, then use the following Classes for your projects:

  • Storer.ActiveDirectory.User: The User Class.
  • Storer.ActiveDirectory.Group: The Group Class.
  • Storer.ActiveDirectory.Search: Static Methods for Finding Users, Group and DirectoryEntries.
  • Storer.ActiveDirectory.Methods: Static Methods for doing useful things like getting the Domain Name, or Authenticating a User (with a password), or even converting a Byte[] ObjectSID to an ObjectSIDString (Which is already done for you in the User/Group Classes).

You may also need to add a reference to System.DirectoryServices if you're going to be using custom search parameters and/or moving DirectoryEntry objects.

Also: Don't worry about disposing of any unused COM Objects in the User/Group code. Any necessary disposal is handled for you, except for if you pass a DirectoryEntry object into the Search.ForUsers(...) Method, in which case you'll have to dispose of the Search Root used; I would recommend a using clause for that.

Points of Interest

The secret, as said above, is using the DirectorySearcher class instead of the DirectoryEntry class for accessing the properties of a User or Group Object.

Accessing the Values from a DirectorySearcher are the same as accessing them from a DirectoryEntry. I encapsulated the process in the following method:

C#
private void PopulateFields(ResultPropertyCollection Collection)
{
    if (Collection.Contains(Properties.ACCOUNTCONTROL))
        AccountControl = (int?)Collection[Properties.ACCOUNTCONTROL][0] ?? 0;

    if (Collection.Contains(Properties.ASSISTANT))
        Assistant = Collection[Properties.ASSISTANT][0] as string;

    if (Collection.Contains(Properties.CELLPHONE))
        CellPhone = Collection[Properties.CELLPHONE][0] as string;
    ...
    if (Collection.Contains(Properties.STREETADDRESS))
        StreetAddress = Collection[Properties.STREETADDRESS][0] as string;

    if (Collection.Contains(Properties.USERPRINCIPALNAME))
        UserPrincipalName = Collection[Properties.USERPRINCIPALNAME][0] as 
            string;

    if (Collection.Contains(Properties.ZIPCODE))
        ZipCode = Collection[Properties.ZIPCODE][0] as string;
}

Saving the Changes made to a User is handled by retrieving the DirectoryEntry by ObjectSID and saving only the Changed Values.

C#
public void SaveChanges()
{
    try
    {
        using (DirectoryEntry deUser = 
            Search.ForDirectoryEntry(Properties.OBJECTSID, ObjectSIDString))
        {
            if (_PropertiesLoaded.Contains(Properties.ACCOUNTCONTROL))
                if (!object.Equals(deUser.Properties[Properties.
                    ACCOUNTCONTROL].Value, AccountControl))
                    SetPropertyValue(deUser, Properties.ACCOUNTCONTROL, 
                    AccountControl);

            if (_PropertiesLoaded.Contains(Properties.ASSISTANT))
                if (!object.Equals(deUser.Properties[Properties.
                    ASSISTANT].Value, Assistant))
                    SetPropertyValue(deUser, Properties.ASSISTANT, 
                    Assistant);

            if (_PropertiesLoaded.Contains(Properties.CELLPHONE))
                if (!object.Equals(deUser.Properties[Properties.
                    CELLPHONE].Value, CellPhone))
                    SetPropertyValue(deUser, Properties.CELLPHONE, 
                    CellPhone); 
            ...
            if (_PropertiesLoaded.Contains(Properties.STREETADDRESS))
                if (!object.Equals(deUser.Properties[Properties.
                    STREETADDRESS].Value, StreetAddress))
                    SetPropertyValue(deUser, Properties.STREETADDRESS,  
                    StreetAddress);

            if (_PropertiesLoaded.Contains(Properties.USERPRINCIPALNAME))
                if (!object.Equals(deUser.Properties[Properties.
                    USERPRINCIPALNAME].Value, UserPrincipalName))
                    SetPropertyValue(deUser, Properties.USERPRINCIPALNAME, 
                    UserPrincipalName);

            if (_PropertiesLoaded.Contains(Properties.ZIPCODE))
                if (!object.Equals(deUser.Properties[Properties.
                    ZIPCODE].Value, ZipCode))
                    SetPropertyValue(deUser, Properties.ZIPCODE, ZipCode);

            deUser.CommitChanges();

            if (_PropertiesLoaded.Contains(Properties.COMMONNAME))
                if (!object.Equals(deUser.Properties[Properties.
                    COMMONNAME].Value, CommonName))
                {
                    deUser.Rename("CN=" + CommonName);
                    deUser.CommitChanges();
                }
        }
    }
    catch (Exception Error)
    { throw new Exception("Save Error.", Error); }
}

Handling the Multi-Value Keys are usually very simple, and are made read-only. Almost all of the User properties and All of the Group Properties can be retrieved using a DirectorySearcher object, with the exception of User.TokenGroups. This requires a different approach:

C#
...
public List<string> TokenGroups
{
    get
    {
        if (this[Properties.TOKENGROUPS] == null)
            this[Properties.TOKENGROUPS] = GetTokenGroups(ObjectSIDString);
        return (List<string>)this[Properties.TOKENGROUPS];
    }
    private set { this[Properties.TOKENGROUPS] = value; }
}
...
public static List<string> GetTokenGroups(string ObjectSIDString)
{
    List<string> TokenGroups = new List<string>();

    try
    {
        using (DirectoryEntry deUser = 
            Search.ForDirectoryEntry(Properties.OBJECTSID, ObjectSIDString))
        {
            deUser.RefreshCache(new string[] { Properties.TOKENGROUPS });

            if (deUser.Properties.Contains(Properties.TOKENGROUPS))
            {
                if (deUser.Properties[Properties.TOKENGROUPS] != null)
                {
                    foreach (byte[] GroupSID in 
                        deUser.Properties[Properties.TOKENGROUPS])
                    {
                        string sGroupSID = 
                            Methods.ConvertBytesToStringSid(GroupSID);
                        string sGroupName = Search.ForGroupName(sGroupSID);
                        if (!string.IsNullOrEmpty(sGroupName))
                            TokenGroups.Add(sGroupName);
                    }
                }
            }
        }
    }
    catch
    { throw; }

    return TokenGroups;
}
...

You have to use a DirectoryEntry object to retrieve the Token Groups, because it is a calculated property. Notice that the methods that require direct DirectoryEntry access are static methods, as to keep them separate from the rest of the class.

Another point of interest is setting user flags. Here in the User class, I set four: Enabled, MustChangePasswordOnNextLogin, CannotChangePassword, and PasswordNeverExpires. All but the CannotChangePassword are handled through the AccountControl & PasswordLastSet properties. The CannotChangePassword switch is shown below; it's a bit more complicated:

C#
public static void SetFlag_CannotChangePassword(string ObjectSIDString, 
    bool Value)
{

    Guid ChangePasswordGUID = new 
        Guid("{AB721A53-1E2F-11D0-9819-00AA0040529B}");
    bool wasModified = false;

    try
    {
        using (DirectoryEntry deUser = 
            Search.ForDirectoryEntry(Properties.OBJECTSID, ObjectSIDString))
        {
            ActiveDirectorySecurity ads = deUser.ObjectSecurity;
            AuthorizationRuleCollection arc = ads.GetAccessRules(true, true, 
                typeof(NTAccount));

            foreach (ActiveDirectoryAccessRule adar in arc)
            {
                if (adar.ObjectType == ChangePasswordGUID && 
                    (adar.IdentityReference.Value == @"EVERYONE" || 
                    adar.IdentityReference.Value == @"NT 
                    AUTHORITY\SELF"))
                {
                    ActiveDirectoryAccessRule AccessRule = new 
                        ActiveDirectoryAccessRule(adar.IdentityReference, 
                        adar.ActiveDirectoryRights, AccessControlType.Deny, 
                        adar.ObjectType, adar.InheritanceType);
                    if (!ads.ModifyAccessRule((Value ? 
                        AccessControlModification.Add : 
                        AccessControlModification.Remove), 
                        AccessRule, out wasModified))
                        throw new Exception("ACE Not Modified: (" + 
                            adar.IdentityReference.Value + ")");
                }

            }
            deUser.ObjectSecurity = ads;
            deUser.CommitChanges();
        }
    }
    catch
    { throw; }
}

Note the GUID: It took me forever to figure that out, and without it very odd behavior occurs with users if you set every ADAR to Disallow/Allow. It's usually the simplest things!

The User.Path property is calculated from the DistinguishedName, and is simply a list of the parts of the path in reverse order, much like a Directory is listed (such as C:\Windows\Somewhere\Somefile.txt, it's listed like: "com\company\ouname\ouname\commonname").

The best part of the Search method's deal with the PropertiesToLoad method variables. When you search for a user, you can choose to only return a few of the properties of a user (such as FirstName or SAMAccountName) instead of the whole thing. Make sure to check it out: it will make retrieving your Users and Groups that much faster!

I encourage you to experiment and develop the code. I've gone through many iterations of this class and this has been the best/fastest. I only included the Properties that I use most often, but there are other User and Group Properties. Add or remove whatever you need to: always tailor it to suit your needs. Please let me know if/when you do, so that I can make my own adjustments.

History

  • Sunday, April 1, 2007 [2.0]: Uploaded to CodeProject.
  • Sunday, April 9, 2007 [2.1]: General Update of Code. Many changes occurred; please see source code.
    • Properties changed to allow nulls from string (Convert.ToString(object) will return null when the object is null, but Convert.ToString(string) return string.Empty on null.
    • List<*> Properties now use ?? to prevent nulls, which cuts down on Exceptions thrown.
    • SaveChanges() and PopulateValues() bug fixes/updates.
    • User.Path changed from List<string> to string with "\" separators.
    • Various other fixes/updates.

License

This article, along with any associated source code and files, is licensed under The Common Development and Distribution License (CDDL)