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.
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 |
| 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.
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.
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.
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.
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.
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;
}
}
}