v 1.0
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.
string domain="";
string aDLogin="";
string aDPswd="";
string filePath="";
bool _trace = false;
string trace="";
Declare increment variable to help with cycling through the passing in arguments
int i = 0
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.
public static string GetManager
(string Domain, string ADLogin, string ADPswd, string Login)
{
string dn = "";
string trace = "";
string filter = string.Format("(&(ObjectClass={0})
(ObjectCategory={1})(sAMAccountName={2}))", "user", "person", Login);
try
{
DirectoryEntry adRoot = new DirectoryEntry
("LDAP://" + Domain, ADLogin, ADPswd, AuthenticationTypes.Secure);
DirectorySearcher searcher = new DirectorySearcher(adRoot);
searcher.SearchScope = SearchScope.Subtree;
searcher.ReferralChasing = ReferralChasingOption.All;
searcher.Filter = filter;
SearchResult result = searcher.FindOne();
DirectoryEntry manager = result.GetDirectoryEntry();
dn = (string)manager.Properties["distringuisedName"][0];
}
catch (Exception ex)
{
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.
public static void UpdateUser(string Domain, string ADLogin,
string ADPswd, string Login, String[] Parameters, String[] Values)
{
string trace = "";
try
{
string filter = string.Format("(&(ObjectClass={0})
(ObjectCategory={1})({2}={3}))", "user", "person",
Parameters[0].ToString(), Login);
DirectoryEntry adRoot = new DirectoryEntry("LDAP://" +
Domain, ADLogin, ADPswd, AuthenticationTypes.Secure);
DirectorySearcher searcher = new DirectorySearcher(adRoot);
searcher.SearchScope = SearchScope.Subtree;
searcher.ReferralChasing = ReferralChasingOption.All;
searcher.Filter = filter;
SearchResultCollection results = searcher.FindAll();
foreach (SearchResult result in results)
{
DirectoryEntry user = result.GetDirectoryEntry();
for (int i = 1; i < Values.Length; i++)
{
if (Parameters[i] != null)
{
switch(Parameters[i].ToUpper())
{
case "MANAGER":
user.Properties[Parameters[i]].Value =
GetManager(Domain, ADLogin, ADPswd, Values[i]);
break;
default:
user.Properties[Parameters[i]].Value = Values[i];
break;
}
user.CommitChanges();
}
else
{
break;
}
}
}
}
catch (Exception ex)
{
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.
public static void ProcessFile(string Domain, string ADLogin,
string ADPswd, string FilePath)
{
string trace = "";
bool exists = File.Exists(FilePath);
if (exists)
{
try
{
using (StreamReader sr = new StreamReader(FilePath))
{
char[] parms = {'|',';'};
String[] Parameters = sr.ReadLine().Split(parms);
while (!sr.EndOfStream)
{
String[] Values = sr.ReadLine().Split(parms);
UpdateUser(Domain, ADLogin, ADPswd, Values[0].ToString(),
Parameters, Values);
}
}
}
catch (Exception ex)
{
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