By Klaus Salchner
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. If
you have comments to this article, please contact me @ klaus_salchner@hotmail.com. I
want to hear if you learned something new. Contact me if you have questions
about this topic or article.
Download Source
About the author
Klaus Salchner has worked for 14 years in the industry, nine years in Europe
and another five years in North America. As a Senior Enterprise Architect
with solid experience in enterprise software development, Klaus spends
considerable time on performance, scalability, availability,
maintainability, globalization/localization and security. The projects he
has been involved in are used by more than a million users in 50 countries
on three continents.
Klaus calls Vancouver, British Columbia his home at the moment. His next big
goal is doing the New York marathon in 2005. Klaus is interested in guest
speaking opportunities or as an author for .NET magazines or Web sites. He
can be contacted at
klaus_salchner@hotmail.com or http://www.enterprise-minds.com.