Click here to Skip to main content
15,881,757 members
Articles / Web Development / ASP.NET
Article

Storer.ActiveDirectory - Active Directory User/Group Encapsulation Classes

Rate me:
Please Sign up or sign in to vote.
4.79/5 (16 votes)
12 Apr 2007CDDL4 min read 226.3K   525   63   102
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)


Written By
Software Developer Employer's Security
United States United States
I'm a Computer Support Technician for a Indiana School System. Really, I'm a C#/ASP.NET Developer in a Software Support coat. Jack-of-all-trades, I do Hardware Support, Software Support, Programming in C#.NET, ASP.NET / Javascript, Web Design, Graphic Arts and lots of other stuff.

I've been programming almost 21 years now, and have used more than 10 different languages in the past.

Comments and Discussions

 
AnswerRe: Connect to another Directory Service Pin
Diedzz24-Jul-07 3:10
Diedzz24-Jul-07 3:10 
GeneralRe: Connect to another Directory Service Pin
John Storer II24-Jul-07 6:47
John Storer II24-Jul-07 6:47 
GeneralRe: Connect to another Directory Service Pin
Diedzz24-Jul-07 19:25
Diedzz24-Jul-07 19:25 
GeneralRe: Connect to another Directory Service [modified] Pin
thund3rstruck29-Jul-07 2:32
thund3rstruck29-Jul-07 2:32 
GeneralRe: Connect to another Directory Service Pin
Donkey Master18-Mar-08 0:10
Donkey Master18-Mar-08 0:10 
Generalalternate credentials Pin
atw1234523-Jul-07 5:47
atw1234523-Jul-07 5:47 
GeneralRe: alternate credentials Pin
John Storer II23-Jul-07 6:26
John Storer II23-Jul-07 6:26 
QuestionReuse permission request Pin
Marc Cramer19-Jul-07 13:19
Marc Cramer19-Jul-07 13:19 
AnswerRe: Reuse permission request Pin
John Storer II19-Jul-07 20:02
John Storer II19-Jul-07 20:02 
GeneralRe: Reuse permission request Pin
Marc Cramer20-Jul-07 3:33
Marc Cramer20-Jul-07 3:33 
QuestionHow i Can Get manager Attribute Pin
alzikan14-Jul-07 7:31
alzikan14-Jul-07 7:31 
AnswerRe: How i Can Get manager Attribute Pin
John Storer II14-Jul-07 13:09
John Storer II14-Jul-07 13:09 
GeneralFinding Access Rights for a Group by a user Pin
seh670328-Jun-07 10:24
seh670328-Jun-07 10:24 
GeneralRe: Finding Access Rights for a Group by a user Pin
John Storer II28-Jun-07 10:31
John Storer II28-Jun-07 10:31 
GeneralRe: Finding Access Rights for a Group by a user Pin
seh670328-Jun-07 11:02
seh670328-Jun-07 11:02 
Generalpublic static List ForUsers(string Key, string Value) Pin
KenL_MCSE16-Jun-07 14:58
KenL_MCSE16-Jun-07 14:58 
GeneralRe: public static List ForUsers(string Key, string Value) Pin
John Storer II16-Jun-07 15:26
John Storer II16-Jun-07 15:26 
GeneralRe: public static List ForUsers(string Key, string Value) Pin
KenL_MCSE16-Jun-07 15:51
KenL_MCSE16-Jun-07 15:51 
GeneralGreat Code, but issue with SID binding Pin
Matt Imholt3-Jun-07 16:47
Matt Imholt3-Jun-07 16:47 
GeneralRe: Great Code, but issue with SID binding Pin
John Storer II3-Jun-07 19:42
John Storer II3-Jun-07 19:42 
NewsRequests for permission to use need to go here in comments. Pin
John Storer II24-May-07 8:23
John Storer II24-May-07 8:23 
QuestionHow to get Computer Details Pin
Rizwan Tahir19-May-07 1:54
Rizwan Tahir19-May-07 1:54 
QuestionSetFlag_CannotChangePassword() Pin
lihio5-May-07 22:42
lihio5-May-07 22:42 
AnswerRe: SetFlag_CannotChangePassword() Pin
John Storer II6-May-07 1:23
John Storer II6-May-07 1:23 
GeneralGreat work Pin
JohnDavies3-May-07 23:36
JohnDavies3-May-07 23:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.