Printing Details

URL: http://www.ianatkinson.net/computing/adcsharp.htm
Date: 06 Sep 2010 23:38

All content © Ian Atkinson 2000–2010, not to be re-used without permission

Active Directory With C#

If you work in the kind of large institution that I do and are using Microsoft Active Directory then the chances are that at certain times you will need to perform actions on the directory that are outside the scope of the MSAD tools. This could be things like specialised queries, bulk account creation or mass updates of user information. The MSAD tools and even some of the command line tools are quite limiting and difficult to use in this regard.

Whatever the reason, you may find that at some point you need to either purchase additional software for managing AD or write your own. Obviously I’d rather write my own software as it’s cheaper, more rewarding and you can customise it however you like!

I found that when I was trying to learn how to make C# work nicely with AD there were a lack of simple tutorials to get me started, although I did find a few useful blog posts. Often any examples that I found did much more in the program than I was after, so it was difficult to pick out the few lines that I was actually interested in.

So, this page contains a few basic but fully working programs which illustrate common scenarios that you may have. If you can read and understand these examples you should be able to apply the principles to much larger and very powerful programs as I have done.

Obviously you need to be careful with this kind of programming and where ever possible you shouldn’t be testing on a live environment. Queries are safe enough but when you get on to account creation and modification the potential to royally muck up a lot of account very quickly is a real danger, so take care!

Adjusting the Filter

In all of the examples where the program asks for a username the program then matches this to the field cn, which is what the AD GUI refers to as ‘Full Name’ and is what is listed as ‘name’ in the tabulated account lising of Active Directory Users and Computers.

You could change the username to something else by adjusting the filter. For example if you wanted to enter a user logon name (called samaccountname in the schema), you could set the filter as follows:

search.Filter = "(samaccountname=" + username + ")";

The createDirectoryEntry Function

All of these examples contain the same function called createDirectoryEntry, located at the bottom of the program. In order to try out the examples you will need to edit this function and enter both a hostname for your own AD server and also an appropriate search path. I have left in as examples the paths that I used when creating the programs.

If you are logged into a system as a domain administrator or a user with appropriate privilages then you should not need to specify a username and password for the connection.

However, if you are running the program as an unprivilaged user then you will need to add (or prompt for and program accordingly) a username and password to the DirectoryEntry object. The function is overloaded several times so you can just append as follows:

DirectoryEntry ldapConnection = new DirectoryEntry("server", "username", "password");

Example one : Retrieving All Information From a User’s Record

This first example will introduce you to the classes needed for querying the AD using C#. I will explain this example fully as this will give a good understanding of the other examples also, once you grasp the major principles involved.

What we are going to do first is retrieve a full LDAP entry for a particular user. This isn’t something that you would want to do very often as it isn’t at all selective and would be overkill when querying a lot of users.

This is useful however if you need to find out what a particular field in the Active Directory is called.

For example, in the AD GUI we can set a ‘PO Box’ as part of the address (in College we use this for pigeon hole numbers). When you wish to query this information in your C# program the field is actually called postofficebox.

AD Post Box

There is no tool that I know of which shows the correlation between the fields in the GUI and what the fields are called in the schema, so it has been necessary for me several times during development to set one of the fields to ‘foo’ and then run a full query looking for ‘foo’ in order to reveal the correct field.

The example is not too hard to understand, however there are several different classes used in order to accomplish the task. First we create a DirectoryEntry object. As you will have guessed from the section above regarding your setting, this class will contain all of the information which describes the server we are trying to connect to such as address, username and so on.

We then create a DirectorySearcher object. This class describes a search and operates against the DirectoryEntry object, so it knows where to search, and has it’s own properties such as its Filter so it knows what to search for.

We then use the class SearchResult against the DirectorySearcher object, which represents an LDAP entry. This object has a number of Properties (such as user name, e-mail address) and a number of generic objects associated with each property:

SearchResult result
Properties Objects
cn Ian Atkinson
mail santa@clause.ac.uk
memberof
users
staff
domain administrators

The properties have generic objects associated with them as the class has no concept of their content. If you wish you will need to cast or convert to more specific classes in order to perform some operations, for example a telephone extension could be cast to an int.

In many cases there will be a single object associated with each property, for example a user can have only one user logon name (or samaccountname).

However some properties, such as memberof which represents a user’s group membership, will have many objects (one for each group in this case).

The SearchResult object operates like an array, so we can retrieve a particular value such as result.Properties["cn"][0] for the first object associated with the cn property. In the example above result.Properties["memberof"][1] is "staff".

We can also iterate through all of the objects associated with a given property by using the ResultPropertyCollection class, which is what we do in the example below.

NB: this first example is more heavily commented than the rest in order to outline the common parts. In subsequent examples I have removed the comments and only commented the new or relevant parts.

retrieve_all_info.cs:

using System;
using System.Text;
using System.DirectoryServices;

namespace activeDirectoryLdapExamples
{
   class Program
   {
      static void Main(string[] args)
      {
         Console.Write("Enter user: ");
         String username = Console.ReadLine();

         try
         {
            // create LDAP connection object

            DirectoryEntry myLdapConnection = createDirectoryEntry();

            // create search object which operates on LDAP connection object
            // and set search object to only find the user specified

            DirectorySearcher search = new DirectorySearcher(myLdapConnection);
            search.Filter = "(cn=" + username + ")";

            // create results objects from search object

            SearchResult result = search.FindOne();
            
            if (result != null)
            {
               // user exists, cycle through LDAP fields (cn, telephonenumber etc.)

               ResultPropertyCollection fields = result.Properties;

               foreach (String ldapField in fields.PropertyNames)
               {
                  // cycle through objects in each field e.g. group membership
                  // (for many fields there will only be one object such as name)

                  foreach (Object myCollection in fields[ldapField]) 
                     Console.WriteLine(String.Format("{0,-20} : {1}", 
                                   ldapField, myCollection.ToString()));
               }
            }

            else
            {
               // user does not exist
               Console.WriteLine("User not found!");
            }
         }

         catch (Exception e)
         {
            Console.WriteLine("Exception caught:\n\n" + e.ToString());
         }
      }

      static DirectoryEntry createDirectoryEntry()
      {
         // create and return new LDAP connection with desired settings

         DirectoryEntry ldapConnection     = new DirectoryEntry("rizzo.leeds-art.ac.uk");
         ldapConnection.Path               = "LDAP://OU=staffusers,DC=leeds-art,DC=ac,DC=uk";
         ldapConnection.AuthenticationType = AuthenticationTypes.Secure;

         return ldapConnection;
      }
   }
}

Here is an (abbreviated) example of the output:

H:\Desktop\adcsharp>retrieve_all_info

Enter user: Ian Atkinson

distinguishedname    : CN=Ian Atkinson,OU=IT,OU=staffusers,DC=leeds-art,DC=ac,DC=uk
cn                   : Ian Atkinson
mailnickname         : iana
displayname          : Ian Atkinson
title                : Senior Infrastructure Support Engineer
samaccountname       : iana
givenname            : Ian
mail                 : santa@clause.ac.uk
sn                   : Atkinson
postofficebox        : J10

<snip>

Example 2 - Retrieving Selected Information From a User’s Record

This example is almost identical to the above example, however we are now selective about which fields from the AD we want to bring in. This is a much more realistic example as it’s obviously bad practise to query more data than is required.

We load certain properties by calling the PropertiesToLoad.Add method on our DirectorySearcher object.

retrieve_some_info.cs:

using System;
using System.Text;
using System.DirectoryServices;

namespace activeDirectoryLdapExamples
{
   class Program
   {
      static void Main(string[] args)
      {
         Console.Write("Enter user: ");
         String username = Console.ReadLine();

         try
         {
            DirectoryEntry myLdapConnection = createDirectoryEntry();
            DirectorySearcher search = new DirectorySearcher(myLdapConnection);
            search.Filter = "(cn=" + username + ")";

            // create an array of properties that we would like and
            // add them to the search object

            string[] requiredProperties = new string[]{"cn", "postofficebox", "mail"};

            foreach (String property in requiredProperties) 
               search.PropertiesToLoad.Add(property);

            SearchResult result = search.FindOne();
            
            if (result != null)
            {
               foreach (String property in requiredProperties)
                  foreach (Object myCollection in result.Properties[property]) 
                     Console.WriteLine(String.Format("{0,-20} : {1}", 
                                   property, myCollection.ToString()));
            }

            else Console.WriteLine("User not found!");
         }

         catch (Exception e)
         {
            Console.WriteLine("Exception caught:\n\n" + e.ToString());
         }
      }

      static DirectoryEntry createDirectoryEntry()
      {
         // create and return new LDAP connection with desired settings

         DirectoryEntry ldapConnection     = new DirectoryEntry("rizzo.leeds-art.ac.uk");
         ldapConnection.Path               = "LDAP://OU=staffusers,DC=leeds-art,DC=ac,DC=uk";
         ldapConnection.AuthenticationType = AuthenticationTypes.Secure;
         return ldapConnection;
      }
   }
}

Here is an example of the output:

H:\Desktop\adcsharp>retrieve_some_info

Enter user           : Ian Atkinson
cn                   : Ian Atkinson
postofficebox        : J10
mail                 : santa@clause.ac.uk

Example 3 - Retrieving Information for All Users

So far we have only retrieved information for a single user. In this example we will retrieve some information for all of the users in our search base.

We can accomplish this simply by using the FindAll rather than the FindOne method on our DirectorySearcher object and then iterating through the results.

all_users.cs:

using System;
using System.Text;
using System.DirectoryServices;

namespace activeDirectoryLdapExamples
{
   class Program
   {
      static void Main(string[] args)
      {
         Console.Write("Enter property: ");
         String property = Console.ReadLine();

         try
         {
            DirectoryEntry myLdapConnection = createDirectoryEntry();

            DirectorySearcher search = new DirectorySearcher(myLdapConnection);
            search.PropertiesToLoad.Add("cn");
            search.PropertiesToLoad.Add(property);

            SearchResultCollection allUsers = search.FindAll();

            foreach(SearchResult result in allUsers)
            {
               if (result.Properties["cn"].Count > 0 && result.Properties[property].Count > 0)
               {
                  Console.WriteLine(String.Format("{0,-20} : {1}",
                                result.Properties["cn"][0].ToString(),
                                result.Properties[property][0].ToString()));
               }
            }  
         }

         catch (Exception e)
         {
            Console.WriteLine("Exception caught:\n\n" + e.ToString());
         }
      }

      static DirectoryEntry createDirectoryEntry()
      {
         // create and return new LDAP connection with desired settings

         DirectoryEntry ldapConnection = new DirectoryEntry("rizzo.leeds-art.ac.uk");
         ldapConnection.Path = "LDAP://OU=staffusers,DC=leeds-art,DC=ac,DC=uk";
         ldapConnection.AuthenticationType = AuthenticationTypes.Secure;
         return ldapConnection;
      }
   }
}

Here is an example of the output:

H:\Desktop\adcsharp>all_users

Enter property       : mail

Ian Atkinson         : santa@clause.ac.uk
Rudolph              : rudolph@clause.ac.uk
Elf                  : elf@clause.ac.uk

Example 4 - Updating a User

Having covered querying the AD we will now move on to updating the AD! This is much simpler than you might imagine as the search results that we have already found really represent actual objects on the server, so we can easily edit the properties of the result and then write this information back to the AD.

We do this by creating a DirectoryEntry object from the search result (using the GetDirectoryEntry method) and then setting the Value for any property that we would like to change. When we are finished we use the CommitChanges method to actually write the changes.

In this small example we retrieve a user’s job title (title in the schema) and then change it for a new one.

update_user.cs:

using System;
using System.Text;
using System.DirectoryServices;

namespace activeDirectoryLdapExamples
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.Write("Enter user      : ");
            String username = Console.ReadLine();

            try
            {
                DirectoryEntry myLdapConnection = createDirectoryEntry();

                DirectorySearcher search = new DirectorySearcher(myLdapConnection);
                search.Filter = "(cn=" + username + ")";
                search.PropertiesToLoad.Add("title");

                SearchResult result = search.FindOne();

                if (result != null)
                {
                    // create new object from search result

                    DirectoryEntry entryToUpdate = result.GetDirectoryEntry();

                    // show existing title

                    Console.WriteLine("Current title   : " + 
                                      entryToUpdate.Properties["title"][0].ToString());

                    Console.Write("\n\nEnter new title : ");

                    // get new title and write to AD

                    String newTitle = Console.ReadLine();

                    entryToUpdate.Properties["title"].Value = newTitle;
                    entryToUpdate.CommitChanges();

                    Console.WriteLine("\n\n...new title saved");
                }

                else Console.WriteLine("User not found!");
            }

            catch (Exception e)
            {
                Console.WriteLine("Exception caught:\n\n" + e.ToString());
            }
        }

        static DirectoryEntry createDirectoryEntry()
        {
            // create and return new LDAP connection with desired settings

            DirectoryEntry ldapConnection     = new DirectoryEntry("rizzo.leeds-art.ac.uk");
            ldapConnection.Path               = "LDAP://OU=staffusers,DC=leeds-art,DC=ac,DC=uk";
            ldapConnection.AuthenticationType = AuthenticationTypes.Secure;
            return ldapConnection;
        }
    }
}

Here is an (abbreviated) example of the output. Note how when the program is run for the second time the title that is retrieved is the one entered the first time around:

H:\Desktop\adcsharp>update_user

Enter user      : Ian Atkinson
Current title   : Senior Infrastructure Support Engineer


Enter new title : Dogsbody


...new title saved

H:\Desktop\adcsharp>update_user

Enter user      : Ian Atkinson
Current title   : Dogsbody


Enter new title : Senior Infrastructure Support Engineer


...new title saved

Example 5 - Adding a New User

One of the most complex things that you may decide you need to do is add a new user from your C# program, rather than using the AD tools.

Again, there are various commercial programs to do this and also tools in the Resource Kit than can be scripted with, but you may find that you just can’t find something to do absolutely everything that you need, just how you need it done.

The program below should be a good starting point for anyone wanting to add in their own users. It shows you how to:

  • Create a user with custom options
  • Set a password
  • Enable the account
  • Add the user to some groups
  • Create a home folder
  • Set the ownserhip of the home folder
  • Set the permissions/ACL of the home folder

Obviously if you wanted to use this as a basis for your own program you would need to set the options to your own requirements and tweak as necessary.

Specifically if you want to write a flexible program to write users in and out of different OUs, rather than a single OU, then it will be necessary to create multiple LDAP connections with different paths, and also a more complex function to add users to groups which searches the whole subtree.

create_user.cs:

using System;
using System.Text;
using System.DirectoryServices;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;

namespace activeDirectoryLdapExamples
{
   class Program
   {
      static void Main(string[] args)
      {
         // connect to LDAP

         DirectoryEntry myLdapConnection = createDirectoryEntry();

         // define vars for user

         String domain      = "leeds-art.ac.uk";
         String first       = "Test";
         String last        = "User";
         String description = ".NET Test";
         object[] password  = { "12345678" };
         String[] groups    = { "Staff" };
         String username    = first.ToLower() + last.Substring(0, 1).ToLower();
         String homeDrive   = "H:";
         String homeDir     = @"\\gonzo.leeds-art.ac.uk\data3\USERS\" + username;

         // create user

         try
         {
            if (createUser(myLdapConnection, domain, first, last, description,
                     password, groups, username, homeDrive, homeDir, true) == 0)
            {

               Console.WriteLine("Account created!");
               Console.ReadLine();
            }

            else
            {
               Console.WriteLine("Problem creating account :(");
               Console.ReadLine();
            }
         }

         catch (Exception e)
         {
            Console.WriteLine("Exception caught:\n\n" + e.ToString());
            Console.ReadLine();
         }
      }

      static int createUser(DirectoryEntry myLdapConnection, String domain, String first, 
                            String last, String description, object[] password, 
                            String[] groups, String username, String homeDrive, 
                            String homeDir, bool enabled)
      {
         // create new user object and write into AD

         DirectoryEntry user = myLdapConnection.Children.Add(
                               "CN=" + first + " " + last, "user");
   
         // User name (domain based) 
         user.Properties["userprincipalname"].Add(username + "@" + domain);

         // User name (older systems)
         user.Properties["samaccountname"].Add(username);                             

         // Surname
         user.Properties["sn"].Add(last);                                             

         // Forename
         user.Properties["givenname"].Add(first);                                     

         // Display name
         user.Properties["displayname"].Add(first + " " + last);            

         // Description
         user.Properties["description"].Add(description);                             

         // E-mail
         user.Properties["mail"].Add(first + "." + last + "@" + domain);   

         // Home dir (drive letter)
         user.Properties["homedirectory"].Add(homeDir);                               

         // Home dir (path)
         user.Properties["homedrive"].Add(homeDrive);                                 
       
         user.CommitChanges();

         // set user's password

         user.Invoke("SetPassword", password);

         // enable account if requested (see http://support.microsoft.com/kb/305144 for other codes) 

         if (enabled) 
            user.Invoke("Put", new object[] { "userAccountControl", "512" });

         // add user to specified groups

         foreach (String thisGroup in groups)
         {
            DirectoryEntry newGroup = myLdapConnection.Parent.Children.Find(
                                      "CN=" + thisGroup, "group");

            if (newGroup != null) 
               newGroup.Invoke("Add", new object[] { user.Path.ToString() });
         }

         user.CommitChanges();

         // make home folder on server
         
         Directory.CreateDirectory(homeDir);

         // set permissions on folder, we loop this because if the program
         // tries to set the permissions straight away an exception will be
         // thrown as the brand new user does not seem to be available, it takes
         // a second or so for it to appear and it can then be used in ACLs
         // and set as the owner

         bool folderCreated = false;

         while (!folderCreated)
         {
            try
            {
               // get current ACL

               DirectoryInfo dInfo = new DirectoryInfo(homeDir);
               DirectorySecurity dSecurity = dInfo.GetAccessControl();

               // Add full control for the user and set owner to them

               IdentityReference newUser = new NTAccount(domain + @"\" + username);

               dSecurity.SetOwner(newUser);

               FileSystemAccessRule permissions =
                  new FileSystemAccessRule(newUser, FileSystemRights.FullControl, 
                                           AccessControlType.Allow);

               dSecurity.AddAccessRule(permissions);

               // Set the new access settings.

               dInfo.SetAccessControl(dSecurity);
               folderCreated = true;
            }

            catch (System.Security.Principal.IdentityNotMappedException)
            {
               Console.Write(".");
            }

            catch (Exception ex)
            {
               // other exception caught so not problem with user delay as 
               // commented above

               Console.WriteLine("Exception caught:" + ex.ToString());
               return 1;
            }
         }

         return 0;
      }

      static DirectoryEntry createDirectoryEntry()
      {
         // create and return new LDAP connection with desired settings

         DirectoryEntry ldapConnection     = new DirectoryEntry("rizzo.leeds-art.ac.uk");
         ldapConnection.Path               = "LDAP://OU=staffusers,DC=leeds-art,DC=ac,DC=uk";
         ldapConnection.AuthenticationType = AuthenticationTypes.Secure;
         return ldapConnection;
      }
   }
}