Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Active Directory User Class Update

0.00/5 (No votes)
11 Feb 2009 3  
How to create a utility to update the Active Directory User Class
v 1.0
ADUserUpdate.gif

Introduction

I recently was brought into an issue where there was a need to do some significant Active Directory cleanup work. For anyone that has worked with Active directory, there are not a lot of easy to use, free tools to help get data in and out of it (at least I have not come across any). At this point, I decided to put on my research hat and take a look on-line to see if there were any community solutions or guides available. I was able to find some information that was helpful too though they did not do exactly what I wanted to do (see references for links). So I took the information that I found and started to see what I could come up with.

Desired Solution

What I wanted to accomplish was to create a utility that could be run from a command line. This utility would need to be able to take in provided arguments to help it connect to Active Directory and receive a file path. The utility would need to be able to process a text file that contains values to be updated in Active Directory. The first row of the file would contain “header” values for each column and would match active directory attributes in the User class schema.

Main Program Code

The first point of business was to create a console application, define the variables needed to receive arguments from the command line and be able to parse the arguments and assign them to the variables. The code below starts by declaring the needed variables and then creates a “switch” statement to cycle through the arguments passed and assign the values to the appropriate variables.

//Declare local variables needed to establish the connection
//to AD and find the file to update it from.
string domain="";
string aDLogin="";
string aDPswd="";
string filePath="";

//Declare the trace variables for advanced logging
bool _trace = false;
string trace="";

Declare increment variable to help with cycling through the passing in arguments
int i = 0

//Cycle through the passed arguments and assigned appropriate values to the variables
foreach(string str in args)
{
   switch (str.ToUpper())
   {
       case "/ADLOGIN":
           aDLogin = args[i + 1];
           break;
       case "/ADPSWD":
           aDPswd = args[i + 1];
           break;
       case "/FILEPATH":
           filePath = args[i + 1];
           break;
       case "/DOMAIN":
           domain = args[i + 1];
           break;
       case "/TRACE":
           _trace = true;
           break;
       default:
           break;
   }
   i++;
} 

New Helper Class

Now that I had a process with the ability to parse the provided arguments and assign them to variables, I needed to create a helper class to handle the interaction with Active Directory. The three methods that I would need to create in order to accomplish my goals would be a file parsing method, an Active Directory user update method and last the ability to get a distringuised name for the manager attribute. In planning out the three new methods, the best place to start would be to create the process to get the manager’s distringuised name (many times it is easier to get data from a system then to update a system which can help to iron out some of the issues that occur when doing the later).

GetManager Method

This method starts by declaring the basic variables needed to make the request and store the results. The first variable assignment begins with creating the filter needed to find the manager user record. For those that are familiar with Active Directory Schemas, there are two main types of objects; Classes and Attributes. The class that I am concerned with is the User class. This is the class that defines all the user entries in Active Directory. The type of user that I am looking for is a person. The last bit of the search string includes the sAMAccountName or the accountid used on the domain (i.e. the sAMAccountName for DOMAIN\USER would be USER).

Next I created the variables needed to access Active Directory and search it. The First DirectoryEntry variable (adRoot) is used to connect to the root of the Active Directory instance using a LDAP call to the domain server.

After this, I created a searcher variable where the filter will be applied and it will be used to attempt to find the manager’s user record. This variable is then used to find the first instance of a user record that matches the filter. The result is then assigned to the manager variable through which we extract the distringuised name attribute and assign it to a local variable. The variable is then returned to the calling process.

//The method to obtain the distringuised Name of the manager
public static string GetManager
	(string Domain, string ADLogin, string ADPswd, string Login)
{
   //Declare variables to use for storing the distringuised name 
   //and setup the filter variables
   string dn = "";
   string trace = "";
   string filter = string.Format("(&(ObjectClass={0})
	(ObjectCategory={1})(sAMAccountName={2}))", "user", "person", Login);

   try
   {
       //Setup the directory root using the LDAP search string and connection information
       DirectoryEntry adRoot = new DirectoryEntry
	("LDAP://" + Domain, ADLogin, ADPswd, AuthenticationTypes.Secure);
       //Create the searcher variable and set it to the root variable
       DirectorySearcher searcher = new DirectorySearcher(adRoot);
       searcher.SearchScope = SearchScope.Subtree;
       searcher.ReferralChasing = ReferralChasingOption.All;
       //Set the search variable filter to the filter variable
       searcher.Filter = filter;

       //Find only the first user based upon the search string
       SearchResult result = searcher.FindOne();

       //Create the manager AD record and set it equal to the search result
       DirectoryEntry manager = result.GetDirectoryEntry();
               
       //Get the manager's distringuised name and set it to the dn variable
       dn = (string)manager.Properties["distringuisedName"][0];

   }
   catch (Exception ex)
   {
       //Create the error trace variable and write it out to the log file
       trace = ex.Message + " - " + ex.StackTrace.ToString() + "\n";
       trace += DateTime.Now.Date.ToString("yyyyMMdd") + " " + 
		DateTime.Now.TimeOfDay.ToString() + "\n";

       File.AppendAllText("ADUserUpdate.log", trace);
   }

   return dn;
}

UpdateUser Method

Now that I have a working method to get information out of Active Directory, I copied the code and modified it so that I can write the values to Active Directory. The most notable change is that in the manager method, I only wanted to work with one record found by the searcher. In this process, I want to find all matches and apply the change to all the records (e.g. I want to find all records with Bob Jones as the manager and update them to Sally Smith, or find all users in Phoenix and change their status to inactive due to a site closure).

This process starts like the GetManager method where it builds the filter and sets up the search object to find the targeted search entries. The main difference is that the search filter is a little different. I added a search criteria to help the process be a little more dynamic. The Added search criterion is the column name for the first column in the source file and the value for that criterion is the first value in the row that is being processed.

Once the searcher has found all the applicable user records, it then cycles through the results. The process also sets up another loop to cycle through each of the columns in the file. The loop first checks to see if the parameter value equals “MANAGER” and if it does, then it calls the GetManager method to get the manager's distringuised name and assigns it to the Active Directory manager attribute for the target user record. You can continue to add other attribute names in this switch statement to handle any other attributes that need special handling. For all other parameters, the value is then directly assigned to the Active Directory attribute for the specific user. After this loop is a very important piece of code. This is where I call the method to commit the changes to the specific user record. Without this, all the changes we made will not go through.

//This method is used to update user info in AD based upon the provided file path
//The "parameter" variable is a collection of the values in the header of the file
//provided.  The "values" variable is a collection of the values for 
//the row currently being processed
public static void UpdateUser(string Domain, string ADLogin, 
	string ADPswd, string Login, String[] Parameters, String[] Values)
{
   //declare the trace string.  I did not add any trace file 
   //exports to this part of the process
   //with the exception of error logging for simplicity though this might be something
   //that you would want to add.
   string trace = "";
           
   try
   {
       //Build the LDAP search string.  The search string will look at 
       //all values in the user class, with the category of person.
       //The last part of the search string pulls in the AD attribute name 
       //from the file column 1 header and the value from the
       //row that is currently being processed
       string filter = string.Format("(&(ObjectClass={0})
		(ObjectCategory={1})({2}={3}))", "user", "person", 

Parameters[0].ToString(), Login);

       //Declare the directory entry using the LDAP call process 
       //providing appropriate login and password.
       DirectoryEntry adRoot = new DirectoryEntry("LDAP://" + 
		Domain, ADLogin, ADPswd, AuthenticationTypes.Secure);

       //Declare the LDAP searcher object and instantiate it using the root LDAP call
       DirectorySearcher searcher = new DirectorySearcher(adRoot);
       //Set the search scope
       searcher.SearchScope = SearchScope.Subtree;
       searcher.ReferralChasing = ReferralChasingOption.All;
       //Apply the search filter
       searcher.Filter = filter;

       //Collection variable to store all results returned
       SearchResultCollection results = searcher.FindAll();

       //Cycle through all results returned
       foreach (SearchResult result in results)
       {
           //Define a "user" result object
           DirectoryEntry user = result.GetDirectoryEntry();

           //Cycle through the list of values to be processed
           for (int i = 1; i < Values.Length; i++)
           {
               //Check to make sure the parameter or column name is not null
               if (Parameters[i] != null)
               {
                   //Not all AD attributes can be handled the same.
                   //Some like the manager value
                   //do not take a normal name string but takes a distringuised name.
                   //some attributes are multivalued and have to be handled a little
                   //differently from single valued attributes and so forth.
                   //By looking at the AD schema master attributes they will help you to
                   //determine how to handle each of the desired attributes
                   //I only included a couple for an example
                   switch(Parameters[i].ToUpper())
                   {
                       case "MANAGER":
                           //If the provided column header is "manager" 
                           //then call the procedure
                           //to get distringuised name for the manager
                           user.Properties[Parameters[i]].Value = 
				GetManager(Domain, ADLogin, ADPswd, Values[i]);
                           break;
                       default:
                           //All else set the value to the attribute
                           user.Properties[Parameters[i]].Value = Values[i];
                           break;
                   }
                   //Apply the changes to the attribute
                   user.CommitChanges();
               }
               else
               {
                   //If the parameter value is null break out of the for loop
                   break;
               }
           }
       }
   }
   catch (Exception ex)
   {
       //If there is an error setup the trace string and write it out to the log file
       trace = ex.Message + " - " + ex.StackTrace.ToString() + "\n";
       trace += DateTime.Now.Date.ToString("yyyyMMdd") + " " + 
			DateTime.Now.TimeOfDay.ToString() + "\n";

       File.AppendAllText("ADUserUpdate.log", trace);
   }
} 

ProcessFile Method

Now that I have the ability to read from and write to Active Directory, I need the process to parse the file for the provided file path.

I began by checking to see if the provided file path exists. After I have validated that the file exists, I create a StreamReader object to read the file and keep it open until the process has reached the end of the file.

There are four main variables in this method (outside the StreamReader object); the trace string is used for exception logging, the parms character string used to parse the values in the file into a string array, the parameters string array which will hold the first row, header, values from the file and the values string array that holds the row currently being processed.

After the header row has been read into the parameters string array, I want to have a loop to cycle through the rest of the file. For each row, the values are inserted into the values string array which is used, along with other variables already captured, to call the UpdateUser method.

//The method that takes the connection and file path information and 
//attempts to process the rows in the file
public static void ProcessFile(string Domain, string ADLogin, 
	string ADPswd, string FilePath)
{
   //declare the variable for trace string used for writing the log file
   string trace = "";
   //Check to see if the file path exists
   bool exists = File.Exists(FilePath);
   if (exists)
   {
       try
       {
           //Create a stream reader object, open it and loop until the file 
           //is completely read
           using (StreamReader sr = new StreamReader(FilePath))
           {
               //Declare the parsing variable character string object
               char[] parms = {'|',';'};
               //Read the first line in the file to get the header values
               //The header values will be used to associate the values to the
               //AD attributes.
               //Split the values of the header row by the parms and put them into the
               //parameters variable string array
               String[] Parameters = sr.ReadLine().Split(parms);
               //read the rest of the file
               while (!sr.EndOfStream)
               {
                   //Declare the values string array variable and read the 
                   //current line and split it
                   //into the variable using the parms values
                   String[] Values = sr.ReadLine().Split(parms);
                   //call the Update user method and pass the values to connect to AD and
                   //process the current record, pass the first value as 
                   //the header to pass
                   //the value of the attribute to use as the search string
                   UpdateUser(Domain, ADLogin, ADPswd, Values[0].ToString(), 
			Parameters, Values);
               }
           }
       }
       catch (Exception ex)
       {
           //If there is an error build the error string and write it to the log file
           trace = ex.Message + " - " + ex.StackTrace.ToString() + "\n";
           trace += DateTime.Now.Date.ToString("yyyyMMdd") + " " + 
			DateTime.Now.TimeOfDay.ToString() + "\n";

           File.AppendAllText("ADUserUpdate.log", trace);
       }
   }
}

Final Solution

As you can see, this is not a very complex process and has a lot of room to expand. Some potential enhancements are to create a new Active Directory entry when the process finds a record in the file (i.e. a new employee) that does not already exist (though some would consider this type of automation a security risk, so be very careful), disabling accounts when a record with a term date is found to reduce potential risk with terminating employees, keeping employee information up-to-date from a single source of truth (typically an HRIS system), most importantly reducing the amount of overhead needed to manage the corporate security environment. The list goes on and on.

Points of Interest

Just in the Active Directory User class alone there are over 300 attributes, though most are not visible via the Administrative GUI. There are some definitions on Microsoft’s website along with other places on the web for each of the different attributes though for many of them you will need to do a lot of testing to make sure that you are passing the right values and in the right manner. This is just a beginner’s course into the streamlining of Active Directory maintenance.

References

Revision History

  • 02/09/2009: Original article

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here