LDAP, IIS and WinNT Directory Services


Introduction

Directory Services have gained a lot of traction over the last few years. Directories are repositories of information and can be utilized in many different ways. For example it can be a repository of users and groups or a repository of network entities like computers, printers, network shares, files, etc. A directory is nothing else then your yellow pages or white pages where you can find objects and information about each object. Whenever you require to store objects and properties about each object and need to be able to search these objects plus bind to an object and retrieve its properties then Directory Services is a very good candidate.

But what is the difference between a Directory Service and a relational database like Microsoft SQL Server? A relational database provides access to data and Directory Services provide access to objects. Take as an example users and groups to which users can belong to. In the case of a relational database you would create a User table, a Group table and a GroupAssign table where you can assign users to a group. Finding all users of a group entails querying the Group table to find the ID of the group, then querying the GroupAssign table to get all the assigned users and then querying for each user the User table to get the information about each user. You could do this as well with one query which joins these tables together. Depending on your choice, a DataReader or DataAdapter with a DataSet, you have the data in flat format available in an array or DataSet. If you want the rest of your application to interact with the data in an object orientated way, then you would write your own object wrapper, for example a User object, which then internally interacts with the underlying data.

That is exactly what directory services provide you with. Each directory comes with its own schema which allows you to define new object types and attribute (property) types. It allows you then to create new objects of this type, set the properties of the object and then store the object in a directory container. It also allows you to bind to an object via the DirectoryEntry type or search your directory via the DirectorySearcher type and then interact with these objects. You can read its properties or change its properties and then persist it back to the directory. You interact with objects and properties. And directories make it very easy to add new object types and attributes through their schema.

There are many directory services available on the market, for example the IBM Tivoli Directory Server, the Novell eDirectory or the Microsoft Active Directory. Active Directory is mostly utilized for managing your windows network infrastructure. The Active Directory schema can be extended so applications can store its objects and properties. But this is rarely used as IT managers want to protect the integrity of the network infrastructure and Active Directory is a key part to that. Microsoft addressed this issue with Windows 2003 by releasing Active Directory Application Mode.

ADAM - Active Directory Application Mode

Active Directory Application Mode is a standalone version of Active Directory and runs only on Windows 2003. It has been released after Windows 2003 has been shipped and can be downloaded from the following link. Active Directory Application Mode does not replace Active Directory. Active Directory is intended for managing your windows network infrastructure while ADAM is intended as directory service for applications, for example to store your application specific security information, etc. Active Directory runs as a system service and requires DNS while ADAM runs as a user service and does not require DNS at all. It is simple to install and to uninstall and you can run multiple instances of ADAM on one Windows 2003 server, for example one for each application.

Follow the link above to download ADAM (you need the file ADAMRedistX86.exe), unzip the file and run the "adamsetup.exe" setup. On the first screen it asks whether you want to install "ADAM and ADAM administration tools" or "ADAM administration tools only". Choose the later option if you want to install the ADAM administration tools to be able to administrate an ADAM instance from a remote machine. In our case we want to install a fresh ADAM instance so we choose the first option. Next it asks whether to install "a unique instance" or "a replica of an existing instance". The second option makes it very easy to replicate an existing ADAM instance, for example you have created an ADAM instance on your development machine then extended its schema and now you want to create a separate QA or production ADAM instance. Setup will replicate all schema changes to this new instance of ADAM. We choose the first option as we are installing a new instance of ADAM. Next you give this ADAM instance a name, e.g. "My application directory". Next you enter the ports on which this ADAM instance will listen, by default 389 and 636 when using SSL. If you install multiple instances of ADAM then each one would get its own unique port to listen. Or if you want to protect access in production you might want to choose a random port. The default port 389 is the standard port used for LDAP (Lightweight Directory Access Protocol). Leave the default ports for your first ADAM instance.

Next it asks you if you want to create an application directory partition. ADAM stores directory data in a hierarchical, file-based directory store. The default location of the directory store is "Program Files\Microsoft ADAM\<Instance name>\Data\adamntds.dit". A directory store is organized in logical directory partitions, also called naming contexts. There are three different types of partitions, the configuration partition, the schema partition and the application partition. Each directory can only have one instance of a configuration partition and one instance of a schema partition. These two partitions are always created as part of the ADAM installation. A directory can contain one or more application partitions. Each partition is given a "distinguished name" also called DN, a unique name how to reference the partition. The three partition types are used for the following purpose:

  • Configuration partition - This partition contains configuration information for ADAM. This includes replication information, security information as well as a list of all partitions (under the container CN=Partitions).
  • Schema partition - This partition contains all the object types and attribute types defined in this directory. Through this partition you can extend the out-of-the-box schema with your own object types and attribute types. Before you are able to create new objects or attributes of this type you need to define them in the schema partition.
  • Application partition - This partition holds the application specific information. You can create multiple application partitions, each for a different application or for different purposes of the same application. You can create containers (like folders) and new objects in each application partition. The schema partition defines which type of containers, objects and attributes you can create.

You can create an application partition while installing ADAM (select "Yes, create an application directory partition") or you can create it later (select "No, do not create an application directory partition"). If you create an application directory, then you need to enter the distinguished name, a unique name, of the partition. The distinguished name consists of one or multiple parts, like nodes of a hierarchical tree. This allows you to build a hierarchy. Each part consists of DN attribute and a value. The following DN attributes are allowed:

  • DC - Domain Component
  • C - Country
  • L - Location
  • O - Organization
  • OU - Organizational Unit
  • CN - Common Name

There is no fixed hierarchy (order) how these DN attributes appear. But they allow you to build a hierarchy reflecting the customer's organization or your application structure. Here are a few examples:

  • CN=MyApplication,DC=MyCompany,DC=COM
  • OU=Engineering,O=MyCompany,C=US
  • CN=MyApplication,C=US,DC=MyCompany,DC=COM

As we are installing the first instance of ADAM, select "Yes, create an application directory partition" and enter as partition name "O=MyCompany,C=US". On the next screen you enter the location where the directory store is placed. We leave the default at "Program Files\Microsoft ADAM\My application directory\data". Next you select the windows account to use for running this ADAM instance. Each ADAM instance creates a windows service with the name of the ADAM instance, in our example "My application directory" and this account is used to run this window service. Next you need to select a windows user or group which has administrative rights to this instance of ADAM. This user or group is allowed to use the ADAM tools to view the directory objects, administrate the schema, etc. The last step allows you to import some pre-defined directory objects. Select the "MS-User.ldf" so we are able to create users in this directory.

When the installation is complete you find a new menu item under "Start | All Programs | ADAM". All ADAM tools are placed under the folder "Windows\ADAM", the directory store is placed under "Program Files\Microsoft ADAM\<Instance name>" and a new windows service has been added with the name of your ADAM instance. Go through the same process to install an additional ADAM instance. Setup will detect that another instance is running and will suggest for each ADAM instance another port, by default 50,001, etc. Each instance has also its own entry under "Add or Remove programs" so you can uninstall each ADM instance individually. Each entry is called "Adam Instance <Instance name>", in our case "Adam Instance My application directory". Uninstalling will only remove the selected ADAM instance and never the ADAM tools itself located under "Windows\ADAM".

A look at how to administrate ADAM

Open the tool "ADAM ADSI Edit" through the menu "Start | All Programs | ADAM". ADAM ADSI Edit allows you to connect to ADAM instances and administrate each partition in the directory store as well as create new partitions. Right click on the item "ADAM ADSI Edit" in the list on the left side and select "Connect to" from the popup menu. This allows you to connect to different partitions of any available ADAM instance (remote or local). Let's first connect to the Configuration and Schema partition. Select the radio button "Well-known naming context" and select from the drop down list "Configuration". Enter the server name and port address if the ADAM instance is running on a different machine or on a different port. Click ok to navigate the containers and objects of the Configuration partition. Navigate the tree with the plus and minus sign in front of each node. The name of the top container in the Configuration naming context is "CN=Configuration,CN={GUID}". Under there you find a container called "CN=Partitions". Selecting it shows on the right side the three different partitions available in this directory store. The Schema partition, the Configuration partition (the one you are just viewing) and the application partition we created during the install.

Repeat the same process to connect to the Schema partition. The top container in the Schema partition is named "CN=Schema,CN=Configuration,CN={GUID}". Selecting it shows on the right side all attribute and object types defined in this schema. You see that both the Configuration and the Schema partition have a GUID in the distinguished name to make each unique. So how can you programmatically discover these partitions without knowing the GUID? There is a third well known naming context called RootDSE. You can connect to this naming context using the DirectoryEntry using the LDAP path "
LDAP://<machine name>/RootDSE". The property "configurationNamingContext" gives you the distinguished name to connect to the Configuration partition. The property "schemaNamingContext" provides the DN to connect to the Schema partition. And the property "namingContexts" provides a comma separated list of all naming contexts in the directory store, which includes all application partitions, the Schema partition and the Configuration partition.

To connect to the RootDSE naming context from the tool "ADAM ADSI Edit" repeat the same process as for the Configuration and Schema partition but select as well known naming context RootDSE. Navigating this naming context will show you only one container called RootDSE. Right click it and select Properties from the pop-up menu. It shows you all properties and you will find in the list the three properties mentioned above. You use the same process to connect to the application partition created during the install. But this time you select "Distinguished name (DN) or naming context" and enter in the text box below the DN name of the application partition we created. In our example this is "O=MyCompany,C=US". Navigating this partition shows you as top container "O=MyCompany,C=US". Under it you find three containers called "CN=Roles", "CN=LostAndFound" and "CN=NTDS Quotas".

Let's see how we can create our own object and attribute type and then create an instance of that object in our application partition. First navigate to the Schema partition to create these new types. Right click on "CN=Schema,CN=Configuration,CN={GUID}" and select from the popup menu "New | Object". The dialog shows you which object types you can create and we choose "classSchema". First you enter the common name (without the ‘CN=' prefix), for example "UserProperties". Next you need to enter the "subClassOf" value, meaning this class is a child of which other class. You can use the name of any other object type defined in this schema and therefore build an object hierarchy. The top most object type is called "top" and almost all other object types are a child of this one. Next you need to enter the "governsID", which is a unique ID for this object type. The value for application objects is always "1.2.840.113556.1.6.1.2.x", the last digit being the unique ID you give your object. You can not create the object type if there is already one with the same common name or "governsID" in the schema. When done go to the end of the list of object and attribute types (on the right side) and you will find your new object type you just created.

Next we create a new attribute type which we then assign to the newly created object type, meaning the object UserProperties is allowed to have a property of this type. Right click again on "CN=Schema,CN=Configuration,CN={GUID}" and select from the popup menu "New | Object". This time choose "attributeSchema" from the available object types. Next we enter again the common name without the "CN=' prefix, for example "HomeURL". Each attribute can be of a certain type and the type defines the value of "oMSyntax" and "attributeSyntax". Go to the MSDN article Choosing a Syntax, select the type you want for your attribute and note down these two values. Enter the "oMSyntax" value, for example for a string this is "20". For the "lDAPDisplayName" use the same value as for the common name, for example "HomeURL". Next enter the value for "isSingleValued" which is true for a single value property and false if the property can have multiple values, an array of values. Next enter the "attributeSyntax" you noted down, for a string this is "2.5.5.4". Finally you enter the "attributeID" value which is a unique ID for this attribute type. The value for application attributes is always "1.2.840.113556.1.6.1.1.x", the last digit being the unique ID you give your attribute. You can not create the attribute type if there is already one with the same common name or "attributeID" in the schema or if the value of "oMSyntax" and "attributeSyntax" does not match the list of valid types. When done go to the end of the list of object and attribute types (on the right side) and you will find your new attribute type you just created.

You can also register the object and attribute ID's so no one else can reuse them. For more information go to the MSDN article Obtaining an Object Identifier from Microsoft. Next we assign the attribute type "HomeURL" to the new object type "UserProperties". This unfortunately can not be done through ADAM ADSI Edit. The logical choice would be to open the properties of the "UserProperties" object type and then add the attribute to the "allowedProperties" property. But this will give you the following error message "Modification of a constructed attribute is not allowed". This needs to be done through the "ADAM Schema" MMC snap-in. Go to "Start | Run" and type in "mmc /a". This opens an empty Management Console and allows you to add different snap-ins. Go to the menu "File | Add/Remove Snap-in", click on the add button and select the "ADAM Schema" snap-in. When done this shows you an entry called "ADAM Schema" in the list on the left side. Right click on it and select "Change ADAM Server" from the popup menu. Enter the ADAM server and the port address where the ADAM instance is running on, for example "localhost" and "389". When done you see two entries in the list - "Classes" and "Attributes". You can quite actually also create object and attribute types through this snap-in. You enter the same values but the user interface is different.

Now find your object type under "Classes", right click on it and select properties. Go to the Attributes tab, click the Add button, select the "HomeURL" attribute and click ok. You can add as many attributes to the optional attributes list as required but you can not add mandatory attributes. If you need mandatory attributes then create the object type in the "ADAM Schema" snap-in and select them while creating the object type itself. When done click ok. Click on the object type and see on the right side all the attributes belonging to it. You see if they are system attributes, whether it is mandatory or optional and also which class they have been added to. So this view shows you also all the inherited attributes. You see now also the attribute you just added. Going back to ADSI Edit and looking at the property "allowedAttributes" of the object type UserProperties does unfortunately not show this new attribute we just added. This shortcoming in ADSI Edit can unfortunately be rather inconvenient!

Let's finally go back and create an instance of this object type in our application partition "O=MyCompany,C=US". Navigate to the top container "O=MyCompany,C=US" in this partition, right click on it and select "New | Object" from the popup menu. But our object type does not show up. Create first a container of the name "Test". Now right click on the container "CN=Test" and select "New | Object" from the popup menu. This time it shows our object type and we can create an instance of it, for example a UserProperties object called Klaus. When you define an object type you also define which other object types are possible as superiors. By default this is only set to the object type "container" and this is why you can not create a new object of type UserProperties under the "O=MyCompany,C=US" container (because that one is of type "organization"). View the properties of the "O=MyCompany,C=US" container and look for the "objectClass" property. As you can see it is set to "top,organization", the right most being its type and the types to the left being its parents (so top is the parent). If you view the properties of the "CN=Test" container we created you see that the "objectClass" is set to "top,container" and new object types by default can have "container" as its parent. To change that we go back to the "ADAM Schema" snap-in and open up the properties of our UserProperties object type. Select the tab "Relationship" and click on the "Add Superior" button. Add the "organization" object type, restart the windows service for this ADAM instance, go back to ADSI Edit and now you can also create a UserProperties object under the top container "O=MyCompany,C=US".

You can also import or export existing schema definitions or directory objects using the "ldifde" tool, located in the folder "windows\adam". Here is the syntax how to import a schema definition (for example the "MS-User.ldf" file):

ldifde -i -f <file name> -s <machine name> -b <user name> <domain name> <password> -c "CN=Schema,CN=Configuration,DC=X" "CN=Schema,CN=Configuration,CN={GUID}"  

The option "-i" specifies a data import, the option "-f" is followed with the file name which has the schema definition to import, the option "-s" is followed with a windows credentials which has administrative access to ADAM and the option "-c" tells to replace the DN in the schema file with the proper DN of your DAM instance. See above how to find out the schema DN through ADAM ADSI Edit. Here is the syntax how to export a schema definition (for example the UserProperties object type we created):

ldifde -f <file name> -s <machine name> -b <user name> <domain name> <password> -d "CN=Schema,CN=Configuration,CN={GUID}" -r "(|(cn=UserProperties)(cn=HomeURL))"  

The default option is data export and with the option "-f" you specify the file to create, with "-d" the DN to connect to, in this example the DN to the schema partition for this directory store, and the option "-r" the filter to apply, in our case the name of the object and attribute type we created. This makes it easy to export your schema definition and then re-import at another ADAM instance. Another usage would be to export objects from one ADAM instance and then re-import it to another one.

ADAM, Active Directory, as well as most other directories, do not allow you to delete object or attribute types. You can mark types as dysfunctional by setting the "isDefunct" property to true. After restarting the window service for this ADAM instance you are no longer able to create objects or attributes of this type. Already existing objects of that type will still remain in the directory, but you will no longer see the class name but rather the object ID. So before you start changing your schema, make sure you know what changes you want to apply or if you need to be able to experiment around create a separate ADAM instance which you can delete afterwards.

You can use the tool "ldp" located in the folder "windows\adam" to create a new application partition. First you need to connect to an ADAM instance by choosing the menu "Connection | Connect". Enter the server name and the port, for example "localhost" and 389. Next you need to bind to the ADAM instance, meaning authenticate so you can access the directory. Go to the menu "Connection | Bind" and enter a user credential which has administrative access to the ADM instance (make sure to enter a domain name; choose the machine name if you are not part of a domain). Next you can create the partition. Go to the menu "Browse | Add Child" and enter the distinguished name for the new partition, for example "O=MyCompany,C=CA". Next you need to add two attributes. Enter in the text box "Attribute" the value "objectClass" and in the "Values" text box the value for this attribute. This value depends very much on the partition DN you entered. In our case the partition name ends with "O" for organization so we enter as value "organization". If the partition DN ends with "OU" enter "organizationalUnit", if it ends with DC you enter "domainDNS" and if it ends with "CN" then enter "container". Next click the "Enter" button to add this attribute/value pair to the list. Then enter in the "Attribute" text box "instanceType" and in the "Values" text box "5" and click again the "Enter" button" to add this attribute/value pair. Now click "Run" to add this new partition. You will see in the right side pane a message saying "Added {O=MyCompany,C=CA}". Any error shown needs to be resolved. Before you can access the new partition you need to re-start ADAM ADSI Edit so that the right security context is applied when binding to this new partition. You will be able to bind to the partition but not able to create any objects before you re-start ADAM ADSI Edit. In ADAM ADSI Edit you bind to this new partition as to any other naming context (see above). If you want to delete a partition, then bind to the Configuration naming context, go to the container "CN=Partitions", right click on the partition (shown on the right side) and select "Delete" from the popup menu. Be careful, deleting a partition is unrecoverable.

IIS and WinNT directory service provider

Directory Services is the .NET wrapper for ADSI (Active Directory Services Interface). LDAP is one of many ADSI providers available. Two other well known ADSI providers are IIS and WinNT. IIS allows access through ADSI to the underlying IIS met-database. You can browse existing settings, web sites and web folders as well as create new web sites and web folders or change the IIS settings. You connect to the IIS ADSI provider through IIS://<machine name>, for example "IIS://localhost". For example to list all web sites you would bind to "IIS://localhost/W3SVC. Later in the article it is explained how to create new web sites and web folders. The IIS ADSI provider has a known issue with reading and writing properties. This has been resolved with Windows 2003 SP1 and Windows XP SP2".

The WinNT ADSI provider gives access to the windows users, groups and windows services. You bind to this provider through WinNT://<machine name>, for example WinNT://localhost. Later in the article it is explained how to create users, groups and windows services.

Introduction to the .NET Directory Services

The .NET framework provides access to directory services through types build on top of ADSI. You need to reference the System.DirectoryServices.dll assembly and import the System.DirectoryServices namespace in your project. You first need to bind to a directory object through the DirectoryEntry type. When instantiating an instance of that type you provide the path to the directory object you want to access. The path consists of the provider, followed by the machine where the provider is residing, optional the port the provider is listening at and then the relative path to the actual directory object - "Provider://Machine:Port/Path". The path "LDAP://localhost:389/O=MyCompany,C=US" for example binds to the root container of the application partition we created in ADAM (see previous section).

By default DirectoryEntry uses the credentials of the windows user running the code. You can specify with the Username and Password property the user credentials to use when binding to the directory object. The Username can contain the domain name, for example "MyDomain\Administrator". The Children property returns a DirectoryEntries object which is a collection of child directory objects. It depends on the type of directory object you bind whether it can have children or not. For example if you bind to a container (CN=) or an organization (O=) then the directory can have child objects which you can access through the Children property. If you bind to an organizational person (CN=) or a user (CN=) then the Children property is an empty collection as these objects are not allowed to have children's. The following code sample shows how you can bind to a directory object and then add all its descendants to a tree view.

public void FillTreeView(string AdsiPath,TreeNodeCollection NodeCollection)
{
// connect to the selected directory path
DirectoryEntry DirEntry = new
DirectoryEntry(AdsiPath);
try
{
// now loop through all the children
foreach (DirectoryEntry ChildEntry in
DirEntry.Children)
{
// add the node to the tree view
TreeNode NewNode = NodeCollection.Add(ChildEntry.Path, ChildEntry.Name);
// add any child entries for this entry
FillTreeView(ChildEntry.Path, NewNode.Nodes);
}
}
// catch any exception accessing the directory object
catch
(Exception)
{ }
// close the directory object
finally
{
DirEntry.Close();
}
}

We first instantiate a DirectoryEntry object and pass along the directory path to bind to. Then we enumerate all child objects and add them to the TreeNodeCollection. For each child object found we call the function recursively to find any child objects it might have. This will find any descendants and add them to the tree view. We catch any exception happening. And because Directory Services works with ADSI COM components it is important to call Close() in the finalizer so we free up the underlying COM object. This lists any child object but sometimes it is very useful to apply a filter. You can do that by using the DirectorySearcher type. Here is a code snippet:

public void FillFilteredTreeView(string AdsiPath,string Filter,TreeNodeCollection NodeCollection)
{
// connect to the selected directory path
DirectoryEntry DirEntry = new
DirectoryEntry(AdsiPath);
// create a directory searcher which we wrap around the
// directory object we want to search
DirectorySearcher Searcher = new
DirectorySearcher(DirEntry);
// search only the immediate children's
Searcher.SearchScope = SearchScope.OneLevel;
try
{
// set the filter to apply - is a property=value collection
// with logical & and |, e.g. (|(cn=Klaus)(cn=Peter))
Searcher.Filter = Filter;
// perform the sarch and get the result collection back
SearchResultCollection ResultCollection = Searcher.FindAll();
// now loop through all the objects in the result collection
foreach (SearchResult Result in
ResultCollection)
{
// get the found directory entry
DirectoryEntry FoundEntry = Result.GetDirectoryEntry();
// add the node to the tree view
TreeNode NewNode = NodeCollection.Add(FoundEntry.Path, FoundEntry.Name);
// add any child entries for this found directory object
FillTreeView(FoundEntry.Path, NewNode.Nodes);
// close the found entry
FoundEntry.Close();
}
// dispose the search result collection
ResultCollection.Dispose();
}
// catch any exception accessing the directory object
catch
(Exception)
{ }
// close the directory and searcher object
finally
{
Searcher.Dispose();
DirEntry.Close();
}
}

First instantiate a DirectoryEntry object pointing it to the directory path to search. Next instantiate a DirectorySearcher object and pass along the DirectoryEntry object, telling it this is the directory path to search in. The DirectorySearcher.SearchScope property sets the search scope and has three different values - Base, OneLevel and SubTree. Base searches only the directory path you bound to. OneLevel searches the immediate children of the directory path bound to. And SubTree searches all descendants of the directory path bound to. The code sample sets it to OneLevel to search only the immediate children of the directory path bound to. The DirectorySearcher.Filter property sets the filter to apply for the search. You can filter on any property the directory object has and also use wildcards. The syntax used is property name equals value surrounded with parenthesis, for example "(cn=Klaus)". If you filter on more then one property then you need to specify the logical "&" or "|" operator. First you specify the logical operator followed by the list of property/name filters and the whole filter is again surrounded with parenthesis, for example "(&(cn=Klaus)(objectClass=user))" or "(|(cn=Klaus)(cn=Peter))". The first example finds any object with the name Klaus and of the type user. The second example finds any object with the name Klaus or Peter. You can use the greater, greater equal, equal, less and less then operators (>, >=, =, <, <=). Any combination is possible, for example "(&(objectClass=classSchema)(|(cn=organizational-unit)(cn=organization)))" searches for all object types with the name organizational-unit or organization.

After setting the search scope and filter you call FindAll() to find all matching directory objects. This returns a SearchResultColletion which you can loop through and for each found directory object you get a SearchResult object. The SearchResult.Path property returns the path of the found directory object. You can also get an instance of the found directory object via SearchResult.GetDirectoryEntry(). In our code sample we add each found directory object to the tree view and call FillTreeView() to add any descendants of the directory object to the tree view. Don't forget to call Close() for every directory object we get by calling SearchResult.GetDirectoryEntry(). When done looping through the SearchResultCollection call Dispose() to free up the search result collection. At the end we call Dispose() on the DirectorySearcher object and Close() on the DirectoryEntry object used to bind to the directory path we wanted to search.

The DirectoryEntry.Properties property gives you access to all properties of a directory object. It returns a PropertyCollection, the collection of properties for this directory object. PropertyCollection.PropertyNames returns a collection of all property names and PropertyCollection[PropertyName] gives you access to the value of this property. A property value can be single valued or it can be an array of object values. So you can loop through all property values. Here is a code snippet:

public void GetPropertyList(string AdsiPath,ListBox.ObjectCollection Collection)
{
// connect to the selected directory path
DirectoryEntry DirEntry = new
DirectoryEntry(AdsiPath);
try
{
// loop through all the properties and get the key for each
foreach (string Key in
DirEntry.Properties.PropertyNames)
{
string
PropertyValues = String.Empty;
// now loop through all the values in the property;
// can be a multi-value property
foreach (object Value in
DirEntry.Properties[Key])
PropertyValues += Convert.ToString(Value) + ";";
// cut off the separator at the end of the value list
PropertyValues = PropertyValues.Substring(0, PropertyValues.Length - 1);
// now add the property info to the property list
Collection.Add(Key + "=" + PropertyValues);
}
}
// catch any exception accessing the directory object
catch
(Exception)
{ }
// close the directory object
finally
{
DirEntry.Close();
}
}

First we instantiate a DirectoryEntry object and bind to the directory object path. Next we loop through all the property names and for each property we loop through all the values. Note that we don't know the type of each value so we use the type "object". We then convert each property value to a string and concatenate them together separated by a semicolon. Each property gets then added to the list box collection in the format property name equals value list. At the end we call again Close() on the DirectoryEntry object.

You can also programmatically find all available ADSI providers on your machine. This information is available in the registry under "HKLM\Software\Microsoft\ADs\Providers". These are typically the IIS, WinNT, LDAP, NDS and NWCOMPAT providers. NDS is the "Novell NetWare Directory Service" provider and NWCOMPAT is the "Novell Netware 3.x (compatible) Directory Service" provider. Here is a code snippet how to get a list of providers.

public static string[] GetListOfDirectoryProviders()
{
// get the HKLM registry key
RegistryKey RegKey = Registry.LocalMachine;
// open the sub-key which contains all the providers
RegistryKey ProviderKey = RegKey.OpenSubKey(ProviderRegKey);
// get the list of the sub-keys
string
[] SubKeys = ProviderKey.GetSubKeyNames();
// create the string array which will hold the provider list
string[] ListOfProviders = new string
[SubKeys.Length];
// now add all providers to the array; all providers are
// pointed to the local machine
for (int
Count = 0; Count < SubKeys.Length; Count++)
ListOfProviders[Count] = SubKeys[Count] + "://" + Environment.MachineName;
// return the list of providers
return
ListOfProviders;
}

First you get a reference to the HKEY_LOCAL_MACHINE registry key on your local machine by calling Registry.LocalMachine. Then you obtain a reference to the sub-key "\Software\Microsoft\ADs\Providers" which stores a list of all ADSI providers. Last you enumerate all its sub-keys by calling GetSubKeyNames() which returns the ADSI provider prefix IIS, WinNT, LDAP, NDS and NWCOMPAT and for each you add the local machine name so you have a valid directory path, e.g. "LDAP://klauslaptop". This gives you access to all IIS and WinNT directory objects. For LDAP you still need to add the local path, for example "O=MyCompany,C=US" or "RootDSE" if you want to discover all the available partitions in your LDAP directory (see above). 

Creating, updating and deleting directory objects

Directory Services allows you also to add new objects and update or delete existing objects. Each directory object has a parent so you need to first bind to a parent directory path and then add a new directory object to its Children collection. If the path you bind to does not allow child objects, for example organizational person (CN=) then you will get a DirectoryServicesCOMException exception. When creating an object you also need to specify its object type, for example "organization". Refer to the provider schema to obtain a list of available object types. Then you can set the property values and invoke methods the ADSI object might expose. Refer to the ADSI provider documentation to obtain a list of properties and methods each object exposes. At the end you call CommitChanges() on the newly created directory object, which writes the changes performed in the cache back to the underlying directory store. Here is a code snippet:

public void AddDirectoryObject(string AdsiParentPath,string ObjectName,string ObjectSchemaName,object[,] Properties,object[,] MethodsToInvoke)
{
// connect to the selected directory parent object
DirectoryEntry DirEntry = new
DirectoryEntry(AdsiParentPath);
try
{
// creates the new directory object
DirectoryEntry NewObject = DirEntry.Children.Add(ObjectName,ObjectSchemaName);
// now loop through all the properties and set them
if (Properties != null
)
{
for (int
Count = 0; Count < Properties.GetLength(0); Count++)
NewObject.Properties[Convert.ToString(Properties[Count,0])].Value = Properties[Count,1];
}
// now loop through all the methods and invoke them
if (MethodsToInvoke != null
)
{
for (int
Count = 0; Count < MethodsToInvoke.GetLength(0); Count++)
NewObject.Invoke(Convert.ToString(MethodsToInvoke[Count,0]),MethodsToInvoke[Count,1]);
}
// commit the changes
NewObject.CommitChanges();
NewObject.Close();
}
// catch any exception accessing the directory object
catch
(Exception e)
{ }
// close the directory object
finally
{
DirEntry.Close();
}
}

First it binds to the parent directory object and adds a new child object providing the name and object type. Next it sets all the property values and invokes all the methods provided by the directory object. At the end it commits the changes to the directory store and calls Close() on the newly created directory object as well as on the parent object we bound to. Updating an existing directory object works very similar. Here is a code snippet:

public void EditDirectoryObject(string AdsiObjectPath,string ObjectName,string ObjectSchemaName,object[,] Properties,object[,] MethodsToInvoke)
{
// connect to the selected directory object
DirectoryEntry DirEntry = new
DirectoryEntry(AdsiObjectPath);
try
{
// find the directory object to edit
DirectoryEntry AdsiObject = DirEntry.Children.Find(ObjectName,ObjectSchemaName);
// if we found the directory object then edit its properties
if (AdsiObject != null
)
{
// now loop through all the properties and set them
if (Properties != null
)
{
for (int
Count = 0; Count < Properties.GetLength(0); Count++)
AdsiObject.Properties[Convert.ToString(Properties[Count,0])].Value = Properties[Count,1];
}
// now loop through all the methods and invoke them
if (MethodsToInvoke != null
)
{
for (int
Count = 0; Count < MethodsToInvoke.GetLength(0); Count++)
AdsiObject.Invoke(Convert.ToString(MethodsToInvoke[Count,0]),MethodsToInvoke[Count,1]);
}
// commit the changes
AdsiObject.CommitChanges();
AdsiObject.Close();
}
}
// catch any exception accessing the directory object
catch
(Exception e)
{ }
// close the directory object
finally
{
DirEntry.Close();
}
}

First it binds to the parent directory object and searches for the child object with the name and object type we want to edit. Another way would be to bind to the directory object directly. Next it sets all the property values and invokes all the methods provided by the directory object. At the end we commit the changes back to the directory store by calling CommitChanges() on the updated directory object. And don't forget to call again Close() on the updated directory object and the parent directory object. You can also delete a directory object by binding to the parent directory object, then find the directory object in the children collection and then call Remove. Here is a code snippet:

public void DeleteDirectoryObject(string AdsiObjectPath,string ObjectName,string ObjectSchemaName)
{
// connect to the selected directory object
DirectoryEntry DirEntry = new
DirectoryEntry(AdsiObjectPath);
try
{
DirectoryEntry AdsiObject;
// find the directory object to delete; some providers like IIS
// need to specify the class; others we can search without a class
if (ObjectSchemaName != null
)
AdsiObject = DirEntry.Children.Find(ObjectName,ObjectSchemaName);
else
AdsiObject = DirEntry.Children.Find(ObjectName);
// if we found the directory object then remove it
if (AdsiObject != null
)
DirEntry.Children.Remove(AdsiObject);
}
// catch any exception accessing the directory object
catch
(Exception e)
{ }
// close the directory object
finally
{
DirEntry.Close();
}
}

First it binds to the parent directory object and searches for the child object with the name and object type we want to delete. Then we call on the child object collection Remove() and pass along the child object we want to remove. You do not need to call CommitChanges() but you need to call again Close() on the parent directory object. This will remove the object only if it does not have any child objects. Another way is to bind to the object you want to delete and then call DeleteTree(). Here is a code snippet:

public static void DeleteDirectoryTree(string AdsiObjectPath)
{
// connect to the selected directory object
DirectoryEntry DirEntry = new
DirectoryEntry(AdsiObjectPath);
try
{
// delete the whole object tree; removes also any child object
DirEntry.DeleteTree();
}
// catch any exception accessing the directory object
catch
(Exception e)
{ }
// close the directory object
finally
{
DirEntry.Close();
}
}

First it binds to the directory object it wants to delete and then calls DeleteTree() on it. This deletes also any descendant directory objects. Be careful as this operation is unrecoverable and might take a long time if the object has many descendants.

The attached VS 2005 sample application

The attached sample application demonstrates how to use Directory Services with the IIS, WinNT and LDAP ADSI providers. The LDAP provider has been used in conjunction with ADAM (Active Directory Application Mode). Please note that the IIS provider only works properly with Windows 2003 SP1 and Windows XP SP2. You can find more information about the IIS and WinNT objects, properties, methods and schema at the following MSDN articles:

The DirectoryServicesManager class provides static methods to create, edit and delete directory objects with the IIS, WinNT and LDAP provider. It also provides methods to create new object and attribute types. These methods are for demonstration purpose and therefore for the most part only update mandatory properties. It is fairly easy to expand these methods with the information provided by the MSDN articles above. Here is a brief summary about each IIS, WinNT and LDAP method:

WinNT provider

  • AddWindowsUser - Windows users are of the schema type "User". This method creates a new windows user, sets its user name, full name and invokes the SetPassword method to set the password.
  • EditWindowsUser - Edits an existing windows user and allows only to change the full name and password.
  • DeleteWindowsUser - Deletes an existing windows user.
  • AddWindowsGroup - Windows groups are of the schema type "Group". This method creates a new windows group and sets the group type and display name. The group type needs always to be set to 4.
  • EditWindowsGroup - Edits an existing windows group and allows to change the group type and display name.
  • DeleteWindowsGroup - Deletes an existing windows group.
  • AddWindowsService - Windows services are of the schema type "Service". This method creates a new windows service and sets its name, display name and path to the executable. The service type, startup type and error control type are set to fixed values. For more info see following article.
  • EditWindowsService - Edits an existing windows service and allows to change the display name and the path to the executable.
  • DeleteWindowsService - Deletes an existing windows service.

IIS provider

  • AddWebSite - IIS web sites are of the schema type "IIsWebServer". This method adds a new web site to IIS and sets its name and log type (possible values are 1 to enable logging and 0 to disable logging). The parent path needs to be "IIS://localhost/W3SVC". It calls AddWebfolder to create the root web folder and set its path.
  • EditWebSite - Edits an existing web site and allows to change the log type.
  • DeleteWebSite - Deletes an existing web site.
  • AddWebfolder - IIS web folders are of the schema type "IIsWebVirtualDir". This method allows to create a new web folder and set its name and path. The parent path needs to point to a web site, e.g. "IIS://localhost/W3SVC/1".
  • EditWebfolder - Edit an existing web folder and allows to change the path.
  • DeleteWebfolder - Deletes an existing web folder.

LDAP provider

  • AddLdapContainer - This method creates a new Ldap container of the type "container" and sets its name and display name. The parent needs to support children, e.g. be of the type organization or organizational unit.
  • EditLdapContainer - Edits an existing Ldap container and allows to change its display name.
  • DeleteLdapContainer - Deletes an existing Ldap container.
  • AddLdapUser - This method creates a new Ldap user of the type "user" and sets its display name and given name. This requires that the schema has been extended with this type. You can import the "MS-User.ldf" schema file (see above).
  • EditLdapUser - Edits an existing Ldap user and allows to change its display name and given name.
  • DeleteLdapUser - Deletes an existing Ldap user.
  • CreateLdapClass - This method creates a new object type. The directory path provided needs to point to the Schema partition. It sets the object type name, the class ID and the parent classes (via the "subClassOf" property). Only provide the last digit of the class ID which then gets prefixed with "1.2.840.113556.1.6.1.2.". The object type name and class ID need to unique in the LDAP schema.
  • CreateLdapAtribute - This method creates a new attribute type. The directory path provided needs to point to the Schema partition. It sets the attribute type name, the attribute ID, the attribute type and if the attribute is single valued or not. Only provide the last digit of the attribute ID which then gets prefixed with "1.2.840.113556.1.6.1.1.". The attribute type is a value of the LdapAttributeType enumeration which is then used to get via the method GetAttributeTypeInfo() the proper "oMSyntax" and "attributeSyntax" values.

This sample does not cover all possible operations these ADSI providers offer. But it provides a good overview how to use Directory Services. For more information please read the sample "readme.htm" file.

Summary

Directory Services like Active Directory or Active Directory Application Mode provide great ways how to store object information, discover objects and interact with the information in an object orientated fashion. It is very easy to extend the schema manually through the ADAM ADSI Edit tool or the ADAM Schema snap-in as well as programmatically. This schema extensibility makes it very easy to store additional objects or properties without a lot of code changes in your application. The provided "ldifde" tool makes it very easy to migrate schema or object information between different directories. ADAM is also a great way to develop any extensions for Active Directory, without having to worry about all the complexities of Active Directory in your development environment.

The .NET Directory Services types are very easy to use and provide a lot of flexibility how to search and discover directory objects. It makes it very easy to add new directory objects or edit and delete existing directory objects. The Directory Services types make it transparent with which ADSI provider you work. The only difference is the different directory path syntax between LDAP, IIS and WinNT. The MSDN articles above document very well all the objects, properties, methods and schema information for the IIS and WinNT providers. Next time you have to store user or application information which structure changes frequent or which you want to interact with it in an object orientated fashion, think about Directory Services. It can reduce the amount of code you have to write and removes you from the complexities how to replicate this information as your application grows.