Active Directory User Class Update






4.20/5 (2 votes)
How to create a utility to update the Active Directory User Class

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
- How to add a new user using DirectoryServices?
- Update Manager Name in Active Directory Using C#
- Microsoft MSDN Website with AD CLass and Attribute definitions
Revision History
- 02/09/2009: Original article