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 manydifferent ways. For example it can be a repository of users and groups or arepository of network entities like computers, printers, network shares,files, etc. A directory is nothing else then your yellow pages or whitepages where you can find objects and information about each object. Wheneveryou require to store objects and properties about each object and need to beable to search these objects plus bind to an object and retrieve itsproperties then Directory Services is a very good candidate.
But what is the difference between a Directory Service and a relationaldatabase like Microsoft SQL Server? A relational database provides access todata and Directory Services provide access to objects. Take as an exampleusers and groups to which users can belong to. In the case of a relationaldatabase you would create a User table, a Group table and a GroupAssigntable where you can assign users to a group. Finding all users of a groupentails querying the Group table to find the ID of the group, then queryingthe GroupAssign table to get all the assigned users and then querying foreach user the User table to get the information about each user. You coulddo this as well with one query which joins these tables together. Dependingon your choice, a DataReader or DataAdapter with a DataSet, you have thedata in flat format available in an array or DataSet. If you want the restof 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 directorycomes with its own schema which allows you to define new object types andattribute (property) types. It allows you then to create new objects of thistype, set the properties of the object and then store the object in adirectory container. It also allows you to bind to an object via theDirectoryEntry type or search your directory via the DirectorySearcher typeand then interact with these objects. You can read its properties or changeits properties and then persist it back to the directory. You interact withobjects and properties. And directories make it very easy to add new objecttypes and attributes through their schema.
There are many directory services available on the market, for example theIBM Tivoli Directory Server, the Novell eDirectory or the Microsoft ActiveDirectory. Active Directory is mostly utilized for managing your windowsnetwork infrastructure. The Active Directory schema can be extended soapplications can store its objects and properties. But this is rarely usedas IT managers want to protect the integrity of the network infrastructureand Active Directory is a key part to that. Microsoft addressed this issuewith Windows 2003 by releasing Active Directory Application Mode.
ADAM ? Active Directory Application Mode
Active Directory Application Mode is a standalone version of ActiveDirectory and runs only on Windows 2003. It has been released after Windows2003 has been shipped and can be downloaded from the followinglink. Active Directory Application Mode does not replace ActiveDirectory. Active Directory is intended for managing your windows networkinfrastructure 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 runsas a user service and does not require DNS at all. It is simple to installand to uninstall and you can run multiple instances of ADAM on one Windows2003 server, for example one for each application.
Follow the link above to download ADAM (you need the fileADAMRedistX86.exe), unzip the file and run the "adamsetup.exe" setup. On thefirst screen it asks whether you want to install "ADAM and ADAMadministration tools" or "ADAM administration tools only". Choose the lateroption if you want to install the ADAM administration tools to be able toadministrate an ADAM instance from a remote machine. In our case we want toinstall a fresh ADAM instance so we choose the first option. Next it askswhether to install "a unique instance" or "a replica of an existinginstance". The second option makes it very easy to replicate an existingADAM instance, for example you have created an ADAM instance on yourdevelopment machine then extended its schema and now you want to create aseparate QA or production ADAM instance. Setup will replicate all schemachanges to this new instance of ADAM. We choose the first option as we areinstalling 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 ADAMinstance will listen, by default 389 and 636 when using SSL. If you installmultiple instances of ADAM then each one would get its own unique port tolisten. Or if you want to protect access in production you might want tochoose a random port. The default port 389 is the standard port used forLDAP (Lightweight Directory Access Protocol). Leave the default ports foryour 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\MicrosoftADAM\<Instance name>\Data\adamntds.dit". A directory store isorganized 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 onlyhave one instance of a configuration partition and one instance of a schemapartition. These two partitions are always created as part of the ADAMinstallation. A directory can contain one or more application partitions.Each partition is given a "distinguished name" also called DN, a unique namehow to reference the partition. The three partition types are used for thefollowing purpose:
- Configuration partition ? This partition contains configurationinformation for ADAM. This includes replication information, securityinformation as well as a list of all partitions (under the containerCN=Partitions).
- Schema partition ? This partition contains all the object typesand attribute types defined in this directory. Through this partition youcan extend the out-of-the-box schema with your own object types andattribute types. Before you are able to create new objects or attributes ofthis type you need to define them in the schema partition.
- Application partition ? This partition holds the applicationspecific information. You can create multiple application partitions, eachfor a different application or for different purposes of the sameapplication. You can create containers (like folders) and new objects ineach application partition. The schema partition defines which type ofcontainers, 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 youcreate an application directory, then you need to enter the distinguishedname, a unique name, of the partition. The distinguished name consists ofone or multiple parts, l
ike node
s of a hierarchical tree. This allows you tobuild a hierarchy. Each part consists of DN attribute and a value. Thefollowing 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 theyallow you to build a hierarchy reflecting the customer's organization oryour 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 anapplication directory partition" and enter as partition name"O=MyCompany,C=US". On the next screen you enter the location where thedirectory store is placed. We leave the default at "Program Files\MicrosoftADAM\My application directory\data". Next you select the windows account touse for running this ADAM instance. Each ADAM instance creates a windowsservice with the name of the ADAM instance, in our example "My applicationdirectory" and this account is used to run this window service. Next youneed to select a windows user or group which has administrative rights tothis instance of ADAM. This user or group is allowed to use the ADAM toolsto view the directory objects, administrate the schema, etc. The last stepallows 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\MicrosoftADAM\>Instance name>" and a new windows service has been added withthe name of your ADAM instance. Go through the same process to install anadditional ADAM instance. Setup will detect that another instance is runningand 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" soyou can uninstall each ADM instance individually. Each entry is called "AdamInstance <Instance name>", in our case "Adam Instance My applicationdirectory". Uninstalling will only remove the selected ADAM instance andnever 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 andadministrate each partition in the directory store as well as create newpartitions. Right click on the item "ADAM ADSI Edit" in the list on the leftside and select "Connect to" from the popup menu. This allows you to connectto different partitions of any available ADAM instance (remote or local).Let's first connect to the Configuration and Schema partition. Select theradio button "Well-known naming context" and select from the drop down list"Configuration". Enter the server name and port address if the ADAM instanceis running on a different machine or on a different port. Click ok tonavigate the containers and objects of the Configuration partition. Navigatethe tree with the plus and minus sign in front of each node. The name of thetop 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 differentpartitions available in this directory store. The Schema partition, theConfiguration partition (the one you are just viewing) and the applicationpartition we created during the install.
Repeat the same process to connect to the Schema partition. The topcontainer in the Schema partition is named"CN=Schema,CN=Configuration,CN={GUID}". Selecting it shows on the right sideall attribute and object types defined in this schema. You see that both theConfiguration and the Schema partition have a GUID in the distinguished nameto make each unique. So how can you programmatically discover thesepartitions without knowing the GUID? There is a third well known namingcontext called RootDSE. You can connect to this naming context using theDirectoryEntry using the LDAP path "LDAP://<machine name>/RootDSE".The property "configurationNamingContext" gives you the distinguished nameto 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 allnaming contexts in the directory store, which includes all applicationpartitions, 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 butselect as well known naming context RootDSE. Navigating this naming contextwill show you only one container called RootDSE. Right click it and selectProperties from the pop-up menu. It shows you all properties and you willfind in the list the three properties mentioned above. You use the sameprocess to connect to the application partition created during the install.But this time you select "Distinguished name (DN) or naming context" andenter in the text box below the DN name of the application partition wecreated. In our example this is "O=MyCompany,C=US". Navigating thispartition shows you as top container "O=MyCompany,C=US". Under it you findthree containers called "CN=Roles", "CN=LostAndFound" and "CN=NTDS Quotas".
Let's see how we can create our own object and attribute type and thencreate an instance of that object in our application partition. Firstnavigate 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 wechoose "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. Youcan use the name of any other object type defined in this schema andtherefore 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 youneed 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", thelast digit being the unique ID you give your object. You can not create theobject 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 attributetypes (on the right side) and you will find your new object type you justcreated.
Next we create a new attribute type which we then assign to the newlycreated object type, meaning the object UserProperties is allowed to have aproperty 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 thevalue of "oMSyntax" and "attributeSyntax". Go to the MSDN article Choosinga Syntax, select the type you want for your attribute and note downthese two values. Enter the "oMSyntax" value, for example for a string thisis "20". For the "lDAPDisplayName" use the same value as for the commonname, for example "HomeURL". Next enter the value for "isSingleValued" whichis true for a single value property and false if the property can havemultiple values, an array of values. Next enter the "attributeSyntax" younoted 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 valuefor application attributes is always "1.2.840.113556.1.6.1.1.x", the lastdigit being the unique ID you give your attribute. You can not create theattribute 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 tothe end of the list of object and attribute types (on the right side) andyou will find your new attribute type you just created.
You can also register the object and attribute ID's so no one else can reusethem. For more information go to the MSDN article Obtainingan Object Identifier from Microsoft. Next we assign the attribute type"HomeURL" to the new object type "UserProperties". This unfortunately cannot be done through ADAM ADSI Edit. The logical choice would be to open theproperties of the "UserProperties" object type and then add the attribute tothe "allowedProperties" property. But this will give you the following errormessage "Modification of a constructed attribute is not allowed". This needsto be done through the "ADAM Schema" MMC snap-in. Go to "Start | Run" andtype in "mmc /a". This opens an empty Management Console and allows you toadd different snap-ins. Go to the menu "File | Add/Remove Snap-in", click onthe add button and select the "ADAM Schema" snap-in. When done this showsyou an entry called "ADAM Schema" in the list on the left side. Right clickon it and select "Change ADAM Server" from the popup menu. Enter the ADAMserver and the port address where the ADAM instance is running on, forexample "localhost" and "389". When done you see two entries in the list ?"Classes" and "Attributes". You can quite actually also create object andattribute types through this snap-in. You enter the same values but the userinterface is different.
Now find your object type under "Classes", right click on it and selectproperties. Go to the Attributes tab, click the Add button, select the"HomeURL" attribute and click ok. You can add as many attributes to theoptional attributes list as required but you can not add mandatoryattributes. If you need mandatory attributes then create the object type inthe "ADAM Schema" snap-in and select them while creating the object typeitself. When done click ok. Click on the object type and see on the rightside all the attributes belonging to it. You see if they are systemattributes, whether it is mandatory or optional and also which class theyhave been added to. So this view shows you also all the inheritedattributes. You see now also the attribute you just added. Going back toADSI Edit and looking at the property "allowedAttributes" of the object typeUserProperties 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 ourapplication 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. Createfirst 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 showsour object type and we can create an instance of it, for example aUserProperties object called Klaus. When you define an object type you alsodefine which other object types are possible as superiors. By default thisis only set to the object type "container" and this is why you can notcreate a new object of type UserProperties under the "O=MyCompany,C=US"container (because that one is of type "organization"). View the propertiesof 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 itstype and the types to the left being its parents (so top is the parent). Ifyou view the properties of the "CN=Test" container we created you see thatthe "objectClass" is set to "top,container" and new object types by defaultcan have "container" as its parent. To change that we go back to the "ADAMSchema" snap-in and open up the properties of our UserProperties objecttype. Select the tab "Relationship" and click on the "Add Superior" button.Add the "organization" object type, restart the windows service for thisADAM instance, go back to ADSI Edit and now you can also create aUserProperties object under the top container "O=MyCompany,C=US".
You can also import or export existing schema definitions or directoryobjects using the "ldifde" tool, located in the folder "windows\adam". Hereis 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 withthe file name which has the schema definition to import, the option "-s" isfollowed with a windows credentials which has administrative access to ADAMand the option "-c" tells to replace the DN in the schema file with theproper DN of your DAM instance. See above how to find out the schema DNthrough 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 thefile to create, with "-d" the DN to connect to, in this example the DN tothe schema partition for this directory store, and the option "-r" thefilter to apply, in our case the name of the object and attribute type wecreated. This makes it easy to export your schema definition and thenre-import at another ADAM instance. Another usage would be to export objectsfrom one ADAM instance and then re-import it to another one.
ADAM, Active
Directory, as well as most other directories, do not allow youto delete object or attribute types. You can mark types as dysfunctional bysetting the "isDefunct" property to true. After restarting the windowservice for this ADAM instance you are no longer able to create objects orattributes of this type. Already existing objects of that type will stillremain in the directory, but you will no longer see the class name butrather the object ID. So before you start changing your schema, make sureyou know what changes you want to apply or if you need to be able toexperiment around create a separate ADAM instance which you can deleteafterwards.
You can use the tool "ldp" located in the folder "windows\adam" to create anew application partition. First you need to connect to an ADAM instance bychoosing the menu "Connection | Connect". Enter the server name and theport, for example "localhost" and 389. Next you need to bind to the ADAMinstance, meaning authenticate so you can access the directory. Go to themenu "Connection | Bind" and enter a user credential which hasadministrative 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 cancreate the partition. Go to the menu "Browse | Add Child" and enter thedistinguished name for the new partition, for example "O=MyCompany,C=CA".Next you need to add two attributes. Enter in the text box "Attribute" thevalue "objectClass" and in the "Values" text box the value for thisattribute. This value depends very much on the partition DN you entered. Inour case the partition name ends with "O" for organization so we enter asvalue "organization". If the partition DN ends with "OU" enter"organizationalUnit", if it ends with DC you enter "domainDNS" and if itends with "CN" then enter "container". Next click the "Enter" button to addthis attribute/value pair to the list. Then enter in the "Attribute" textbox "instanceType" and in the "Values" text box "5" and click again the"Enter" button" to add this attribute/value pair. Now click "Run" to addthis 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 youcan access the new partition you need to re-start ADAM ADSI Edit so that theright security context is applied when binding to this new partition. Youwill be able to bind to the partition but not able to create any objectsbefore you re-start ADAM ADSI Edit. In ADAM ADSI Edit you bind to this newpartition as to any other naming context (see above). If you want to deletea partition, then bind to the Configuration naming context, go to thecontainer "CN=Partitions", right click on the partition (shown on the rightside) and select "Delete" from the popup menu. Be careful, deleting apartition is unrecoverable.
IIS and WinNT directory service provider
Directory Services is the .NET wrapper for ADSI (Active Directory ServicesInterface). LDAP is one of many ADSI providers available. Two other wellknown ADSI providers are IIS and WinNT. IIS allows access through ADSI tothe underlying IIS met-database. You can browse existing settings, web sitesand web folders as well as create new web sites and web folders or changethe IIS settings. You connect to the IIS ADSI provider throughIIS://<machine name>, for example "IIS://localhost". For example tolist all web sites you would bind to "IIS://localhost/W3SVC. Later in thearticle it is explained how to create new web sites and web folders. The IISADSI provider has a known issue with reading and writing properties. Thishas been resolved with Windows 2003 SP1 and Windows XP SP2".
The WinNT ADSI provider gives access to the windows users, groups andwindows services. You bind to this provider through WinNT://<machinename>, for example WinNT://localhost. Later in the article it isexplained how to create users, groups and windows services.
Introduction to the.NET Directory Services
The .NET framework provides access to directory services through types buildon top of ADSI. You need to reference the System.DirectoryServices.dllassembly and import the System.DirectoryServices namespace in your project.You first need to bind to a directory object through the DirectoryEntrytype. When instantiating an instance of that type you provide the path tothe directory object you want to access. The path consists of the provider,followed by the machine where the provider is residing, optional the portthe provider is listening at and then the relative path to the actualdirectory object ? "Provider://Machine:Port/Path". The path"LDAP://localhost:389/O=MyCompany,C=US" for example binds to the rootcontainer of the application partition we created in ADAM (see previoussection).
By default DirectoryEntry uses the credentials of the windows user runningthe code. You can specify with the Username and Password property the usercredentials to use when binding to the directory object. The Username cancontain the domain name, for example "MyDomain\Administrator". The Childrenproperty returns a DirectoryEntries object which is a collection of childdirectory objects. It depends on the type of directory object you bindwhether 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 objectswhich you can access through the Children property. If you bind to anorganizational person (CN=) or a user (CN=) then the Children property is anempty collection as these objects are not allowed to have children's. Thefollowing code sample shows how you can bind to a directory object and thenadd all its descendants to a tree view.
public void FillTreeView(stringAdsiPath,TreeNodeCollection NodeCollection) pan>
{
// connect to the selected directory path
DirectoryEntry DirEntry = new DirectoryEntry(AdsiPath);
try
{
// now loop through all thechildren
foreach (DirectoryEntry ChildEntry in DirEntry.Children)
{
// add the node to the treeview
TreeNode NewNode =NodeCollection.Add(ChildEntry.Path, ChildEntry.Name);
// add any child entries for thisentry
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 directorypath to bind to. Then we enumerate all child objects and add them to theTreeNodeCollection. For each child object found we call the functionrecursively to find any child objects it might have. This will find anydescendants and add them to the tree view. We catch any exception happening.And because Directory Services works with ADSI COM components it isimportant to call Close() in the finalizer so we free up the underlying COMobject. This lists any child object but sometimes it is very useful to applya filter. You can do that by using the DirectorySearcher type. Here is acode snippet:
public void FillFilteredTreeView(stringAdsiPath,string Filter,TreeNodeCollectionNodeCollection)
{
// 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 theobjects in the result collection
foreach (SearchResult Result in ResultCollection)
{
// get the found directoryentry
DirectoryEntryFoundEntry = Result.GetDirectoryEntry();
// add the node to the treeview
TreeNode NewNode =NodeCollection.Add(FoundEntry.Path, FoundEntry.Name);
// add any child entries for thisfound 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 pathto search. Next instantiate a DirectorySearcher object and pass along theDirectoryEntry object, telling it this is the directory path to search in.The DirectorySearcher.SearchScope property sets the search scope and hasthree different values ? Base, OneLevel and SubTree. Base searches only thedirectory path you bound to. OneLevel searches the immediate children of thedirectory path bound to. And SubTree searches all descendants of thedirectory path bound to. The code sample sets it to OneLevel to search onlythe immediate children of the directory path bound to. TheDirectorySearcher.Filter property sets the filter to apply for the search.You can filter on any property the directory object has and also usewildcards. The syntax used is property name equals value surrounded withparenthesis, for example "(cn=Klaus)". If you filter on more then oneproperty then you need to specify the logical "&" or "|" operator. First youspecify the logical operator followed by the list of property/name filtersand 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 canuse 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 ororganization.
After setting the search scope and filter you call FindAll() to find allmatching directory objects. This returns a SearchResultColletion which youcan loop through and for each found directory object you get a SearchResultobject. The SearchResult.Path property returns the path of the founddirectory object. You can also get an instance of the found directory objectvia SearchResult.GetDirectoryEntry(). In our code sample we add each founddirectory object to the tree view and call FillTreeView() to add anydescendants of the directory object to the tree view. Don't forget to callClose() for every directory object we get by callingSearchResult.GetDirectoryEntry(). When done looping through theSearchResultCollection call Dispose() to free up the search resultcollection. At the end we call Dispose() on the DirectorySearcher object andClose() on the DirectoryEntry object used to bind to the directory path wewanted to search.
The DirectoryEntry.Properties property gives you access to all properties ofa directory object. It returns a PropertyCollection, the collection ofproperties for this directory object. PropertyCollection.PropertyNamesreturns a collection of all property names andPropertyCollection[PropertyName] gives you access to the value of thisproperty. A property value can be single valued or it can be an array ofobject values. So you can loop through all property values. Here is a codesnippet:
public void GetPropertyList(stringAdsiPath,ListBox.ObjectCollection Collection)
{
// connect to the selected directory path
DirectoryEntry DirEntry = new DirectoryEntry(AdsiPath);
try
{
// loop through all the propertiesand get the key for each
foreach (string Key inDirEntry.Properties.PropertyNames)
{
string PropertyValues = String.Empty;
// now loop through all the values in the property;
// can be a multi-value property
foreach (object Value inDirEntry.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 tothe property list
Collection.Add(Key + "=" + PropertyValues);
}
}
// catch any exception accessing the directory object
catch (Exception)
{ }
// close the directory object
fi
nally
{
DirEntry.Close();
}
}
First we instantiate a DirectoryEntry object and bind to the directoryobject path. Next we loop through all the property names and for eachproperty we loop through all the values. Note that we don't know the type ofeach value so we use the type "object". We then convert each property valueto a string and concatenate them together separated by a semicolon. Eachproperty gets then added to the list box collection in the format propertyname equals value list. At the end we call again Close() on theDirectoryEntry object.
You can also programmatically find all available ADSI providers on yourmachine. 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 DirectoryService" provider and NWCOMPAT is the "Novell Netware 3.x (compatible)Directory Service" provider. Here is a code snippet how to get a list ofproviders.
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 yourlocal machine by calling Registry.LocalMachine. Then you obtain a referenceto the sub-key "\Software\Microsoft\ADs\Providers" which stores a list ofall ADSI providers. Last you enumerate all its sub-keys by callingGetSubKeyNames() which returns the ADSI provider prefix IIS, WinNT, LDAP,NDS and NWCOMPAT and for each you add the local machine name so you have avalid directory path, e.g. "LDAP://klauslaptop". This gives you access toall IIS and WinNT directory objects. For LDAP you still need to add thelocal path, for example "O=MyCompany,C=US" or "RootDSE" if you want todiscover 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 deleteexisting objects. Each directory object has a parent so you need to firstbind to a parent directory path and then add a new directory object to itsChildren collection. If the path you bind to does not allow child objects,for example organizational person (CN=) then you will get aDirectoryServicesCOMException exception. When creating an object you alsoneed to specify its object type, for example "organization". Refer to theprovider schema to obtain a list of available object types. Then you can setthe property values and invoke methods the ADSI object might expose. Referto the ADSI provider documentation to obtain a list of properties andmethods each object exposes. At the end you call CommitChanges() on thenewly created directory object, which writes the changes performed in thecache back to the underlying directory store. Here is a code snippet:
public void AddDirectoryObject(stringAdsiParentPath,string ObjectName,
stringObjectSchemaName,object[,] Properties,object[,]MethodsToInvoke)
{
// connect to the selected directory parent object
DirectoryEntry DirEntry = new DirectoryEntry(AdsiParentPath);
try
{
// creates the new directoryobject
DirectoryEntryNewObject = DirEntry.Children.Add(ObjectName,ObjectSchemaName);
// now loop through all theproperties 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 themethods 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 objectproviding the name and object type. Next it sets all the property values andinvokes all the methods provided by the directory object. At the end itcommits the changes to the directory store and calls Close() on the newlycreated directory object as well as on the parent object we bound to.Updating an existing directory object works very similar. Here is a codesnippet:
public void EditDirectoryObject(stringAdsiObjectPath,string ObjectName,
stringObjectSchemaName,object[,] Properties,object[,]MethodsToInvoke)
{
// connect to the selected directory object
DirectoryEntry DirEntry = new DirectoryEntry(AdsiObjectPath);
try
{
// find the directory object toedit
DirectoryEntryAdsiObject = DirEntry.Children.Find(ObjectName,ObjectSchemaName);
// if we found the directoryobject then edit its properties
if (AdsiObject != null)
{
// now loop through all theproperties 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 themethods 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();
&n
bsp; 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 childobject with the name and object type we want to edit. Another way would beto bind to the directory object directly. Next it sets all the propertyvalues and invokes all the methods provided by the directory object. At theend we commit the changes back to the directory store by callingCommitChanges() on the updated directory object. And don't forget to callagain Close() on the updated directory object and the parent directoryobject. You can also delete a directory object by binding to the parentdirectory object, then find the directory object in the children collectionand then call Remove. Here is a code snippet:
public void DeleteDirectoryObject(stringAdsiObjectPath,string ObjectName,string ObjectSchemaName)
{
// connect to the selected directory object
DirectoryEntry DirEntry = new DirectoryEntry(AdsiObjectPath);
try
{
DirectoryEntryAdsiObject;
// 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 childobject with the name and object type we want to delete. Then we call on thechild object collection Remove() and pass along the child object we want toremove. You do not need to call CommitChanges() but you need to call againClose() on the parent directory object. This will remove the object only ifit does not have any child objects. Another way is to bind to the object youwant 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 callsDeleteTree() on it. This deletes also any descendant directory objects. Becareful as this operation is unrecoverable and might take a long time if theobject has many descendants.
The attached VS 2005 sample application
The attached sample application demonstrates how to use Directory Serviceswith the IIS, WinNT and LDAP ADSI providers. The LDAP provider has been usedin conjunction with ADAM (Active Directory Application Mode). Please notethat the IIS provider only works properly with Windows 2003 SP1 and WindowsXP SP2. You can find more information about the IIS and WinNT objects,properties, methods and schema at the following MSDN articles:
- ADSI Objects of WinNT ? lists all WinNT directory objects; youfind for each object the methods and properties
- WinNT Schema's Mandatory and Optional properties ? listsfor each directory object the mandatory and optional properties
- WinNT Object Class Hierarchy ? lists the WinNT objecthierarchy
- IIS ADSI Provider ? lists all IIS directory objects; youfind for each object the methods and properties
- IIS ADSI Object hierarchy ? lists the IIS objecthierarchy
The DirectoryServicesManager class provides static methods to create, editand delete directory objects with the IIS, WinNT and LDAP provider. It alsoprovides methods to create new object and attribute types. These methods arefor demonstration purpose and therefore for the most part only updatemandatory properties. It is fairly easy to expand these methods with theinformation provided by the MSDN articles above. Here is a brief summaryabout each IIS, WinNT and LDAP method:
WinNTprovider
- AddWindowsUser ? Windows users are of the schema type "User".This method creates a new windows user, sets its user name, full name andinvokes the SetPassword method to set the password.
- EditWindowsUser ? Edits an existing windows user and allows onlyto 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 displayname. The group type needs always to be set to 4.
- EditWindowsGroup ? Edits an existing windows group and allows tochange 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 anderror control type are set to fixed values. For more info see following article.
- EditWindowsService ? Edits an existing windows service andallows to change the display name and the path to the executable.
- DeleteWindowsService ? Deletes an existing windows service.
IISprovider
- AddWebSite ? IIS web sites are of the schema type"IIsWebServer". This method adds a new web site to IIS and sets its name andlog type (possible values are 1 to enable logging and 0 to disable logging).The parent path needs to be "IIS://localhost/W3SVC". It calls AddWebfolderto create the root web folder and set its path.
- EditWebSite ? Edits an existing web site and allows to changethe 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 setits 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 changethe path.
- DeleteWebfolder ? Deletes an existing web folder.
LDAPprovider
- AddLdapContainer ? This method creates a new Ldap container ofthe type "container" and sets its name and display name. The parent needs tosupport children, e.g. be of the type organization or organizationalunit.
- EditLdapContainer ? Edits an existing Ldap container and allowsto 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 theschema 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 changeits display name and given name.
- DeleteLdapUser ? Deletes an existing Ldap user.
- CreateLdapClass ? This method creates a new object type. Thedirectory path provided needs to point to the Schema partition. It sets theobject type name, the class ID and the parent classes (via the "subClassOf"property). Only provide the last digit of the class ID which then getsprefixed with "1.2.840.113556.1.6.1.2.". The object type name and class IDneed 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 setsthe attribute type name, the attribute ID, the attribute type and if theattribute is single valued or not. Only provide the last digit of theattribute ID which then gets prefixed with "1.2.840.113556.1.6.1.1.". Theattribute type is a value of the LdapAttributeType enumeration which is thenused to get via the method GetAttributeTypeInfo() the proper "oMSyntax" and"attributeSyntax" values.
This sample does not cover all possible operations these ADSI providersoffer. But it provides a good overview how to use Directory Services. Formore information please read the sample "readme.htm" file.
Summary
Directory Services like Active Directory or Active Directory ApplicationMode provide great ways how to store object information, discover objectsand interact with the information in an object orientated fashion. It isvery easy to extend the schema manually through the ADAM ADSI Edit tool orthe ADAM Schema snap-in as well as programmatically. This schemaextensibility makes it very easy to store additional objects or propertieswithout a lot of code changes in your application. The provided "ldifde"
tool makes it very easy to migrate schema or object information betweendifferent directories. ADAM is also a great way to develop any extensionsfor Active Directory, without having to worry about all the complexities ofActive Directory in your development environment.
The .NET Directory Services types are very easy to use and provide a lot offlexibility how to search and discover directory objects. It makes it veryeasy to add new directory objects or edit and delete existing directoryobjects. The Directory Services types make it transparent with which ADSIprovider you work. The only difference is the different directory pathsyntax between LDAP, IIS and WinNT. The MSDN articles above document verywell all the objects, properties, methods and schema information for the IISand WinNT providers. Next time you have to store user or applicationinformation which structure changes frequent or which you want to interactwith it in an object orientated fashion, think about Directory Services. Itcan reduce the amount of code you have to write and removes you from thecomplexities how to replicate this information as your application grows. Ifyou have comments to this article, please contact me @ klaus_salchner@hotmail.com. Iwant to hear if you learned something new. Contact me if you have questionsabout this topic or article.
Download Source
About the author
Klaus Salchner has worked for 14 years in the industry, nine years in Europeand another five years in North America. As a Senior Enterprise Architectwith solid experience in enterprise software development, Klaus spendsconsiderable time on performance, scalability, availability,maintainability, globalization/localization and security. The projects hehas been involved in are used by more than a million users in 50 countrieson three continents.
Klaus calls Vancouver, British Columbia his home at the moment. His next biggoal is doing the New York marathon in 2005. Klaus is interested in guestspeaking opportunities or as an author for .NET magazines or Web sites. Hecan be contacted at klaus_salchner@hotmail.com or http://www.enterprise-minds.com.












No comments yet... Be the first to leave a reply!