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!
It's worth noting that you can do a lot of these things with Powershell as well these days, this page was first written before powershell existed.
What is a little bit confusing is that there are essentially two sets of classes which can be used for AD operations. One is easier to use but not as versatile, the other is harder to use but lets you do pretty much anything (within my experience anyway!).
Which approach you use will depend on your project requirements. If you literally want to write a password resetting tool or
a simple phone book then the AccountManagement
libraries probably contain everything you need so you should use those.
For anything more complicated you may find that you need to get a bit more down and dirty with the LDAP and use the DirectoryServices
approach.
The ‘older’ and more difficult approach is using just System.DirectoryServices
on its own. This lets you do pretty much
anything that you like however the approach is more technical.
For example the properties of the AD objects (description, telephone etc.) are all held in an array which can present its own problems and involve a lot of iteration and use of casting since they are all generic objects.
Using this approach doing things like setting the password or enabling/disabling the account is much more cryptic in the way in which it is achieved, often requiring UAC codes to be manually set and so on.
The ‘newer’ approach is to use System.DirectoryServices.AccountManagement
which was designed to make
managing AD through .NET much easier. Rather than accessing properties using an array they are exposed directly
within the classes (and typed accordingly), allowing us to use things like user.DisplayName
which is much tidier.
We also have easy to use methods available such as .SetPassword()
and .UnlockAccount()
as well as the .Enabled
property which can be used to easily manage accounts. These are self explanatory in use
once you have retrieved the object from AD so are not included in the examples!
The problem as I said though is that whilst the AccountManagement
library makes things much easier in some regards
it is also quite limited in others.
It exposes only a small number of the LDAP fields that you may want to use (name, description, email, home dir and phone is about it) so if you need access to a more obscure property it won’t suffice.
One thing that’s worth noting is that you can use the newer libraries to get a UserPrincipal
object as they are
called, and then access the underlying LDAP object with the .GetUnderlyingObject()
method. This means that you could
start a program with the newer approach but if you find it too limiting drop into the older approach half way through and
have full access.
This is not a very neat approach but does work well, we will have a look at how this works in example 8. Hopefully if the newer libraries are expanded further in future .NET released there should be less and less reason to ever need to do this!
These first examples all use the older approach and will serve you best if you are writing a large or complex AD management program.
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 + ")";
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");
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
.
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 |
[email protected] | |
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.
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 : [email protected]
sn : Atkinson
postofficebox : J10
...
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.
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 : [email protected]
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.
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 : [email protected]
Rudolph : [email protected]
Elf : [email protected]
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.
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
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:
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.
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;
}
}
}
This second set of examples all use the newer libraries and will serve you best if you are writing smaller or simpler programs.
This first example is similar to example 2 but using the newer libraries. The approach to searching for a user is a little different as you can see. Here we will just retrieve a person’s name and phone number from their logon name.
First a PrincipalContext
object is created, this is the connection to AD and is overloaded many times so
that you can pass it an LDAP path, user name and password if required (omitted from the example to keep it simple).
We then create a UserPrincipal
object and set some criteria on it. This can be confusing as when we have
retrieved a user from AD it will be an object of the same type, however this object is not real and is only used for
searching. This effectively replaces the LDAP search filter. So we make the object and set its samaccountname
which allows us to search in the PrincipalContext
for a user with that logon name.
Finally we create the PrincipalSearcher
object and run it using the search user we just created. The result is
then cast into a new UserPrincipal
object which represents the acutal AD user. The cast is used as the same approach
is also used to find GroupPrincipal
or ComputerPrincipal
objects.
This may sound complicated but hopefully when you read below you will see it is only a few lines of code!
using System;
using System.DirectoryServices.AccountManagement;
namespace new_ad_exmaple
{
class Program
{
static void Main(string[] args)
{
try
{
// enter AD settings
PrincipalContext AD = new PrincipalContext(ContextType.Domain, "leeds-art.ac.uk");
// create search user and add criteria
Console.Write("Enter logon name: ");
UserPrincipal u = new UserPrincipal(AD);
u.SamAccountName = Console.ReadLine();
// search for user
PrincipalSearcher search = new PrincipalSearcher(u);
UserPrincipal result = (UserPrincipal)search.FindOne();
search.Dispose();
// show some details
Console.WriteLine("Display Name : " + result.DisplayName);
Console.WriteLine("Phone Number : " + result.VoiceTelephoneNumber);
}
catch (Exception e)
{
Console.WriteLine("Error: " + e.Message);
}
}
}
}
And the output:
Enter logon name: iana
Display Name : Ian Atkinson
Phone Number : 1234
This example is similar to example 3, here we show all people’s phone numbers. To find all objects we
have still created the search object but have simply not specified any criteria for it (other than the AD
it belongs to) so the search returns all objects. We can then iterate over these with a handy foreach
.
In real life you should limit the scope of a search like this of course by specifying the correct OU rather than searching the whole AD, but I'm trying to keep the example code concise!
using System;
using System.DirectoryServices.AccountManagement;
namespace new_ad_exmaple
{
class Program
{
static void Main(string[] args)
{
try
{
PrincipalContext AD = new PrincipalContext(ContextType.Domain, "leeds-art.ac.uk");
UserPrincipal u = new UserPrincipal(AD);
PrincipalSearcher search = new PrincipalSearcher(u);
foreach(UserPrincipal result in search.FindAll())
if (result.VoiceTelephoneNumber != null)
Console.WriteLine("{0,30} {1} ", result.DisplayName, result.VoiceTelephoneNumber);
search.Dispose();
}
catch (Exception e)
{
Console.WriteLine("Error: " + e.Message);
}
}
}
}
And some abbreviated output:
Joe Bloggs 1245
Jim Bloggs 1456
Jan Bloggs 1765
Bob Bloggs 1298
Here we will see how a UserPrincipal
can be converted into a more general object so that we
can access properties which are not exposed by the AccountManagement
libraries. There should
be no reason to do this in future if the libraries are expanded to expose more properties!
This example will now list the person’s mail box along with the phone number by expanding the
previous example, as I said earlier this is
the field postofficebox
in AD which is not exposed by AccountManagement
.
In addition to the previous example we now get the underlying object from the UserPrincipal
which we
call lowerLdap
.
Since .GetUnderlyingObject()
returns a generic object we have to cast this to a
DirectoryEntry
(the System.DirectoryServices
equivalent of a UserPrincipal
as seen in the
earlier examples above) so that we can access its properties
array.
using System;
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
namespace new_ad_exmaple
{
class Program
{
static void Main(string[] args)
{
try
{
PrincipalContext AD = new PrincipalContext(ContextType.Domain, "leeds-art.ac.uk");
UserPrincipal u = new UserPrincipal(AD);
PrincipalSearcher search = new PrincipalSearcher(u);
foreach (UserPrincipal result in search.FindAll())
{
if (result.VoiceTelephoneNumber != null)
{
DirectoryEntry lowerLdap = (DirectoryEntry)result.GetUnderlyingObject();
Console.WriteLine("{0,30} {1} {2}",
result.DisplayName,
result.VoiceTelephoneNumber,
lowerLdap.Properties["postofficebox"][0].ToString());
}
}
search.Dispose();
}
catch (Exception e)
{
Console.WriteLine("Error: " + e.Message);
}
}
}
}
And some abbreviated output:
Joe Bloggs 1245 J10
Jim Bloggs 1456 J11
Jan Bloggs 1765 J12
Bob Bloggs 1298 J13