Click here to Skip to main content
15,880,956 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.2K   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

 
Questionwhat account/credentials are used to perform operations Pin
Member 724777628-Jul-10 8:15
Member 724777628-Jul-10 8:15 
AnswerRe: what account/credentials are used to perform operations Pin
John Storer II28-Jul-10 10:07
John Storer II28-Jul-10 10:07 
GeneralRe: what account/credentials are used to perform operations [modified] Pin
Member 724777629-Jul-10 5:26
Member 724777629-Jul-10 5:26 
And thank you for the quick response.

While helpful, I'm still a little confused as to what credentials are being used in my situation...

in our web application, on user login, we authenticate the user's username/password through forms authentication using a membership provider that hits Active Directory; we pass a specific AD user acct. info to use for performing the authentication of the logging in user's username/password.

as of right now, i've only tested the code that adds/removes AD users on my local machine. So what "user" is it using to perform the AD operations? it's not actually using the user credentials of the user I log into the web application with, is it?

I would bet it's using whatever IIS/local user account that .NET debugging runs through the localhost? This account seems to be the ASPNET user on my local machine. I guess this is the user your storer.activedirectory module uses then?

And so when I push this web application to one of our servers, it will instead use that server's IIS windows user/pass that it uses for serving up Web Applications? Would it be the user account under which the IIS Processes run, i.e. the NetworkService account?

Would you happen to know the exact user account that it may be using?
And/or how would I go about specifically using impersonation for only the storer.activedirectory usage of our web application?

I apologize for my lack of understanding of how IIS, windows user accounts, etc. interact with each other.

Greatly appreciate your help on this.



-- Modified Thursday, July 29, 2010 11:40 AM
GeneralRe: what account/credentials are used to perform operations Pin
John Storer II29-Jul-10 10:48
John Storer II29-Jul-10 10:48 
GeneralRe: what account/credentials are used to perform operations [modified] Pin
Member 72477762-Aug-10 8:26
Member 72477762-Aug-10 8:26 
GeneralRe: what account/credentials are used to perform operations Pin
Member 72477763-Aug-10 9:01
Member 72477763-Aug-10 9:01 
GeneralIsUserInGroup method Pin
Cabbi17-Mar-10 5:44
Cabbi17-Mar-10 5:44 
GeneralRe: IsUserInGroup method Pin
John Storer II17-Mar-10 6:04
John Storer II17-Mar-10 6:04 
GeneralRe: IsUserInGroup method Pin
Cabbi17-Mar-10 20:57
Cabbi17-Mar-10 20:57 
GeneralRe: IsUserInGroup method Pin
Cabbi18-Mar-10 4:57
Cabbi18-Mar-10 4:57 
GeneralRe: IsUserInGroup method Pin
John Storer II18-Mar-10 5:57
John Storer II18-Mar-10 5:57 
GeneralRe: IsUserInGroup method Pin
Cabbi18-Mar-10 6:08
Cabbi18-Mar-10 6:08 
GeneralRe: IsUserInGroup method Pin
John Storer II18-Mar-10 6:18
John Storer II18-Mar-10 6:18 
GeneralRe: IsUserInGroup method Pin
Cabbi17-Mar-10 22:28
Cabbi17-Mar-10 22:28 
GeneralRe: IsUserInGroup method Pin
Cabbi18-Mar-10 6:22
Cabbi18-Mar-10 6:22 
GeneralRe: IsUserInGroup method Pin
John Storer II18-Mar-10 6:31
John Storer II18-Mar-10 6:31 
AnswerRe: IsUserInGroup method *** RESOLVED *** Pin
Cabbi18-Mar-10 6:52
Cabbi18-Mar-10 6:52 
QuestionHow to find all the user in AD GROUP in sqlserver 2005. Pin
krazzz21-Sep-09 20:02
krazzz21-Sep-09 20:02 
Generaluse of the properties Pin
royalstar24-Nov-08 3:18
royalstar24-Nov-08 3:18 
GeneralQuering multiple domains Pin
Joao Matos12-Sep-08 0:32
Joao Matos12-Sep-08 0:32 
GeneralTelephone Number Pin
ajonas11-Jun-08 12:25
ajonas11-Jun-08 12:25 
GeneralRe: Telephone Number Pin
ajonas12-Jun-08 8:03
ajonas12-Jun-08 8:03 
GeneralGet rights of User on a group [modified] Pin
Offlinesurfer31-May-08 0:48
Offlinesurfer31-May-08 0:48 
GeneralRe: Get rights of User on a group Pin
John Storer II31-May-08 5:29
John Storer II31-May-08 5:29 
GeneralRe: Get rights of User on a group Pin
Offlinesurfer1-Jun-08 1:50
Offlinesurfer1-Jun-08 1:50 

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.