Avoiding Replication Issues When Designing Applications that Use Active Directory

 

Microsoft Corporation

December 2001

Summary: This document details the replication issues of version skew, partial updates, and collisions in specific application scenarios, and explains how to avoid problems related to replication when designing an application that uses Active Directory. (18 printed pages)

Introduction

Microsoft® Active Directory™ directory service is a loosely coupled distributed system. As such, Active Directory can offer tremendous benefits in regard to remote access to data and the ability to perform updates on any replica. However, it is important to understand that replication in such a system can cause improperly designed applications to behave unpredictably. When designing an application that uses Active Directory, you need to understand version skew, partial updates, and collisions. This document details these issues in specific application scenarios.

When designing an application, a developer must determine what is acceptable performance in a distributed system. Is it okay that changes take time to replicate to all parts of the system (version skew)? Is it tolerable for users to retrieve only a subset of the changes (partial updates)? Is it permissible to have one administrator's changes overwritten by another's changes (collisions)? This document investigates various ways of avoiding or controlling these problems when developing and using Active Directory-enabled applications.

This document is divided into the following sections:

  • Accessing Replicated Data
    This section explains why you need to treat distributed systems differently from other storage systems. It includes examples of how the data you write is not always what you read due to version skew, partial updates, and collisions.
  • Using Server Affinity
    This section discusses several solutions that use server affinity to help you ensure that you retrieve the data that you expect. Server affinity refers to how you establish a relationship with a domain controller to perform Active Directory operations.
  • Provisioning Objects
    This section looks at some tasks required to create of objects in the directory. What happens if the provisioning task is interrupted? The application should perform provisioning in a way that eliminates problems with restarting the process.
  • Updating Multiple Objects
    This section discusses how to handle updates to the directory when the updates involve input from several property pages or affect multiple objects. Active Directory does not support transactional updates to multiple objects and does not provide a record-locking mechanism; so updating multiple objects can be problematic.
  • Versioning Information
    This section explains how versioning can ensure that a consistent set of data is always available to the application. The section discusses two different versioning methods: fixed versioning and time-based versioning.

This document assumes you are familiar with how replication works in Active Directory as well as the challenges inherent in a distributed system. For background information from the Microsoft MSDN® developer program, see Replication and Data Integrity.

Accessing Replicated Data

Usually, when you write data to storage, you expect to retrieve the same data that was written. For example, you might write a string to a file or database table. If no one else accesses the file or table, you expect to read the same string that you placed in storage. Because Active Directory is a distributed storage system, data written to one domain controller (DC) is replicated to other DCs in the same domain or forest. Until data replication is complete, you cannot be certain that the data at each DC is identical. Thus, it is possible that you might not read the data you wrote.

Dealing with possible inconsistencies between directory replicas is an important part of implementing and using a loosely coupled distributed system. These inconsistencies are categorized as follows:

  • Version skew
    All of the data for an object is out of synchronization.
  • Partial update
    Part of the data for an object is out of synchronization.
  • Collision
    Two sources change the same data for an object at the same time.

The following sections show how data inconsistencies can occur for a single user, for multiple users, and for users after an update collision.

Single-User Access

As an example of a data inconsistency for a single user, assume that the creation of a new set of objects in the directory includes writing a default message text of "Message not initialized" to each object. Now suppose that a user modifies each object, changing the message to "Hello World." After making the changes, the user proceeds to verify the message. Every once in a while the user reads "Message not initialized" even though the previous update modified the text.

Figure 1 shows how this happens. The user connects to server DC1 to perform the update to the directory. After completing the update, the user takes a break, returns to verify the change, and connects to server DC2. Because the updated data written on server DC1 has not yet replicated, it cannot be read from server DC2.

Figure 1. Version skew between servers DC1 and DC2 for a single user

This inconsistency can occur when performing a serverless bind (an LDAP bind that does not specify a server) to an object during a directory operation. During a serverless bind, the Active Directory locator service will randomly select a DC in the same site as the requesting user. This behavior of the locator permits load balancing and fault tolerance for applications. However, it can lead to the appearance of data loss.

Using a Serverless Bind

A serverless bind is often the preferred access method for applications because it balances requests between multiple servers and is tolerant of a server becoming unavailable. The following example code performs a serverless bind with an application programming interface (API) call using Active Directory Server Interfaces (ADSI):

ADsGetObject(L"LDAP://cn=myUser,dc=myCompany,dc=com", IID_IADs, 
      (void**)&pADs);

This call does not specify a server to use. The locator selects a DC based on the criteria set by the object name and the client configuration (for example, client site). It might switch servers between calls if required by changes on the network (for example, the link to a DC goes down or the client’s site changes). Using a serverless bind can produce inconsistencies, so it may not be acceptable if you always expect to see the changes you just made.

Multiple-User Access

A variation of the preceding problem involves two users who are responsible for the same set of data. In Figure 2, User1 updates a set of records in Active Directory. A little later, User2 accesses the data, but does not see the changes that User1 made.

Figure 2. Version skew between servers DC1 and DC2 for two users

The discrepancy occurs because User1 made changes on server DC1. User2 connects to server DC2 to retrieve the data before it has been replicated from DC1.

Update Collisions

An update collision occurs when an object is updated at two different DCs during the same replication period. This section first presents how Active Directory tracks changes and reconciles collisions.

Tracking changes

Each object in the directory has a specific version number that is consistent at each replica. (For operating systems in the Microsoft Windows® 2003 Server family, link values also have version numbers.) For example, if the version number for an object is 73 at one server, it should be 73 at every server. This assumes that the value has not changed during the current replication period. If the object is modified, its version number is incremented to 74 and replicated to each DC along with the changed object value. If the object is modified twice within the replication period, the version number is incremented once more to 75 and replicated along with the object value of the second change.

Resolving collisions

When a DC receives an update request from two replicas for the same object, it first compares the version numbers of the replicas. The DC accepts the update request with the highest version number as the new value for the object. If both update requests have the same version number, this will constitute a collision and will be logged as such. To resolve the collision, the DC accepts the update request with the latest timestamp. If both requests have the same timestamp, the DC accepts the request that was received first. The version number takes precedence over the timestamp when reconciling updates of the same object from multiple DCs. This avoids the possibility that a DC with an incorrectly lagging time inadvertently causes the rejection of some of its updates by a receiving DC.

Data loss caused by versioning

Now it is possible to understand how using the version number for reconciliation can cause unexpected results. Suppose that an administrator modifies the user object for Mary by setting her department name to "Accounting." The administrator misspells the name for the first update and makes a second update to correct the mistake. The administrator later learns that Mary actually belongs in the "Executive" department and updates her name again accordingly. This time, however, the update is made on a different server. Table 1 shows the values for the object properties as the changes occurred.

Table 1. Data loss due to use of highest version number

Change Department Version Server Timestamp
First Accounting 74 DC1 10:42
Second Accounting 75 DC1 10:43
Third Executive 74 DC2 10:46

Because the change was made twice on the first server and once on the second server, the name "Accounting" is accepted as the final change. This happens because the second change on the first server has a higher version number, even though the administrator corrected the department name to "Executive" using the second server afterwards.

Using Server Affinity

The preceding section on accessing replicated data describes several ways that updates in a loosely-coupled distributed system can cause inconsistencies to appear when attempting to retrieve updated data. This section presents several variations of server affinity that you can use to avoid these types of inconsistencies. Server affinity is the relationship you set up to access a specific domain controller. This section considers two types of server affinity: single-master updates and alternating the preferred server.

Using Single-Master Updates

To ensure that your application always sees recently updated data, you can connect to a specific server. This method is referred to as using a single master for updates. This method can place an excessive load on the selected server, however, and it is not fault tolerant. When multiple administrators need to access the same application or collection of data, the load can be especially large. If an application is configured to access a specific server, it can fail if the server is unavailable. Care must be taken to ensure that applications are resistant to server unavailability.

Specifying a single master

The following example C/C++ code uses a single master when accessing the directory:

// Retrieve user information from the registry.
lRes = RegOpenKey(HKEY_CURRENT_USER, SERVER_KEY, &hKey);

if (ERROR_SUCCESS == lRes)
{
   lRes = RegQueryValueEx(hKey, "PreferredServer", 0, &dwType, 
      (LPBYTE) szServer, &dwSize);
}
RegCloseKey(hKey);
swprintf(szPath, L"LDAP://%S/cn=myUser,dc=myCompany,dc=com", szServer);
ADsGetObject(szPath, IID_IADs, (void**)&pADs);

The code reads the name of the server from the HKEY_CURRENT_USER\SERVER_KEY\PreferredServer key value in the registry, which was previously set, and adds it to the szPath string using the swprintf call. It then uses an ADSI call to get a pointer to an interface to the specified object.

When performing binds that target a specific server, you should be careful to avoid high-use servers such as the primary domain controller (PDC) role holder. You can identify the PDC role holder for a domain from the fSMORoleOwner attribute of the DomainDNS object or from a call to the DSGetDcName function using the DS_PDC_REQUIRED flag. After you select a DC to use in your application, store its name in the registry, a database, or a configuration file.

Using multiple single masters

For fault tolerance when using single-master updates, you can perform updates on two or more specified DCs at the same time. Duplicate attribute updates are reconciled during replication. Although this causes an increase in replication traffic, it is preferable to inconsistent data. You should do this only for attribute changes and not for object creations. If you create the same object on two different DCs, the name for one of the objects will be changed during replication and that object becomes inaccessible. For additional discussion about creating objects, see the Provisioning Objects topic later in this paper.

Focusing updates

In a variation of using multiple, single masters, the administrator can create a site that includes only two DCs at which to focus updates. By doing this, an application can update each known DC in the site without having to determine the server names. Users and other applications that need to retrieve the data can do so by accessing either DC in the same site because both are up to date. The administrator can now switch out DCs in the site without having to reconfigure the application or clients that need to retrieve the data. The administrator can also experiment with creating more than two DCs without having to reconfigure the application. Figure 3 shows how you could do this when connecting two sites. The users in each case represent either people or applications.

Figure 3. Using two domain controllers to focus updates

Alternating the Preferred Server

Another technique for server affinity uses a preferred server to which you connect that is selected at random the first time an application is started. On successive activations, the application determines whether it is safe to switch servers, and if it is, selects another server at random.

Usually, based on the default settings for replication, a change at a DC replicates to all DCs in a site within fifteen minutes. By performing all of your updates at one DC during this interval, you are assured of always accessing accurate information. If no changes are made during this interval, it is safe to switch to a different DC. This helps to balance the load of requests among multiple DCs within your site.

One way to implement this technique is to store a preferred DC name in the registry on your local computer along with the last update time, timeout interval, and a preferred site name. The timeout interval is the number of minutes it takes to replicate data to each server in the preferred site. If the last update time is zero, or the timeout interval has passed since the last update, use the DSGetDcName function to select a DC at random from your site. Store this as the preferred DC. If there are no DCs within your site, the administrator should select a site. If the site changes, you should modify the timeout interval to match the maximum replication time for the site, which usually should not change. The following pseudocode shows how to implement the alternation of the preferred server:

Read the preferred DC name, last update time, timeout interval, and a 
      preferred site name from the registry.

If (the preferred DC name is NULL, OR
    the last update time is zero, OR
    the current time is greater than the last update time plus the 
      timeout interval),
   call DSGetDcName to retrieve a DC name from the preferred site.
   Store the DC name as the preferred DC in the registry.
End if

Connect to the preferred DC.
Make updates to the directory.
Store the last update time in the registry.

Selecting a site at random is not appropriate when the site is connected over a slow link or when the site has only one DC.

Provisioning Objects

In Active Directory, provisioning refers to the creation and initialization of a set of objects needed to represent an entity. For example, the provisioning of an account for an individual may require the creation of a user account, a medical account, and a dental account. When performing provisioning operations, you ideally perform them so that you can restart if interrupted before completion. The following pseudocode shows how you can do this by performing the operations as one transaction:

If object does not exist,
   create and initialize the object.

The following sections describe how to perform restartable provisioning operations using an LDIF file and a Microsoft Visual Basic® development system script. In each case, the object is created and its attributes initialized as one transaction. The final section discusses the consequences of provisioning collisions when they occur.

Provisioning with an LDIF File

Restartable provisioning can be accomplished by using an LDIF file to create a user object along with the associated medical and dental objects. When using the command-line LDIFDE utility to import the file:

  • Use the /k option to ignore any objects that already exist.
  • Use the /s option to indicate a specific server on which to create the objects. This avoids creating the objects twice at two different domain controllers.

The following command line conditionally (the /k option) imports (the /i option) the contents of the Provision.ldf file (the /f option) into Active Directory on the myServer server (the /s option):

ldifde /i /k /f provision.ldf /s myServer

For example, the Provision.ldf file might have the following contents:

dn: CN=Charles Crane,ou=engineering,dc=myCompany,dc=com
changetype: add
description: Program Manager
displayName: Charles Crane
employeeID: 12345
givenName: Charles
objectClass: user
samAccountName: CCrane
sn: Crane

dn: CN=M12345,ou=engineering,dc=myCompany,dc=com
changetype: add
doctor: Robert Mitchell
medicalRecord: SAP-1703
objectClass: medical
phone: 425.555.1234

dn: CN=D12345,ou=engineering,dc=myCompany,dc=com
changetype: add
dentalRecord: SAP-4579
dentist: Harold Sharp
objectClass: dental
phone: 425.555.5678

When importing this file, the LDIFDE utility adds each object only if it does not exist because of the use of the /k option.

Provisioning with a Visual Basic Script

Restartable provisioning can also be accomplished by using a Visual Basic script that is the equivalent of the preceding LDIF file. In this case, the code checks each provisioning container to determine whether the record for an object already exists. If a record does not exist, the code creates it.

' Bind to the directory and set focus on a specific organizational unit (OU).
Set ou = GetObject("LDAP://myServer/ou=engineering,dc=myCompany,dc=com")

' Retrieve the user's record from the OU.
Set user = ou.GetObject("user", "cn=Charles Crane")

' If the record does not exist, create it.
If err.number = E_ADS_BAD_PATHNAME Then
   ' Create the User object in the OU.
   Set user = ou.Create("user", "cn=Charles Crane")

   ' Set the properties on the User object.
   user.Put "description", "Program Manager"
   user.Put "displayName", "CN=Charles Crane"
   user.Put "employeeID", "12345"
   user.Put "givenName", "Charles"
   user.Put "samAccountName", "CCrane"
   user.Put "sn", "Crane"
   user.SetInfo
End if

' Retrieve the user's medical record from the OU.
Set medical = ou.GetObject("medical", "cn=M12345")

' If the record does not exist, create it.
If err.number = E_ADS_BAD_PATHNAME Then
   ' Create the Medical object in the OU.
   Set medical = ou.Create("medical", "cn=M12345")

   ' Set the properties on the Medical object.
   medical.Put "doctor", "Robert Mitchell"
   medical.Put "medicalRecord", "SAP-1703"
   medical.Put "phone", "425.555.1234"
   medical.SetInfo
End if

' Retrieve the user's dental record from the OU.
Set dental = ou.GetObject("dental", "cn=D12345")

' If the record does not exist, create it.
If err.number = E_ADS_BAD_PATHNAME Then
   ' Create the Dental object in the OU.
   Set dental = ou.Create("dental", "cn=D12345")

   ' Set the properties on the Dental object.
   dental.Put "dentalRecord", "SAP-4579"
   dental.Put "dentist", "Harold Sharp"
   dental.Put "phone", "425.555.5678"
   dental.SetInfo
End if

To avoid collisions, this Visual Basic script specifies the myServer domain controller for the operations.

Handling Collisions During Provisioning

You should specify a server for the provisioning process. Specifying a server avoids a collision, which occurs when the system creates the same object on two different domain controllers (DCs) at the same time. If the same object is unavoidably created on two different DCs, or if a naming value is duplicated for two different objects, Active Directory detects the duplication during the replication process. The following sections describe how Active Directory responds when it detects a duplication of several naming attributes.

DN attribute

If directory objects with the same name are created on separate DCs with the same Distinguished Name (DN), Active Directory changes all but one of the names. The most recently named object keeps the original DN attribute. Active Directory renames the remaining duplicates to a name with the form "originalRdn#CNF:objectGuid" where:

  • originalRdn is the original DN attribute.
  • # is the linefeed character; it is used to avoid conflicts between a system-assigned name and an application-assigned name. Applications cannot use the linefeed character (#) in names.
  • CNF is a tag to denote that the object was renamed due to a name conflict.
  • objectGuid is the hexadecimal representation of the objectGuid attribute; using this avoids conflicts between system-assigned names.

This process is called name mangling. If name mangling occurs, you must delete the duplicate object. Be sure to copy any data out of the duplicate object that was not placed in the original object. For example, if the object cn=john,DC=MyDomain is created on two different DCs, one of the objects might have its name changed to cn=john#CNF:321234123-4312412-34123,DC=MyDomain.

SamAccountName attribute

The samAccountName attribute can be duplicated when the same value is entered for two different user objects. A collision with this attribute behaves differently depending on which operating system the DC is running on:

  • For Windows 2000 Server, Windows 2000 Advanced Server, and Windows 2000 Datacenter Server (the Windows 2000 Server family), the attributes are not changed, but one of the samAccountName attributes is unusable. An event log entry is created to disclose the collision. The user can still logon with the distinguished name.
  • For the Windows 2003 Server family, one of the samAccountName attributes is converted to a unique value with the form "$Duplicate-GUID", where GUID is some unique value.

UPN attribute

If multiple user objects have the same User Principal Name (UPN) attribute, Active Directory does not notice it during replication. However, you will not be able to use any of the UPNs for logon after the duplication is noticed.

Updating Multiple Objects

Update operations on Active Directory often require modifications to several objects at one time. For example, Figure 4 shows the MS Local Properties dialog box, which is used to update the connection properties for several objects. When updating such objects, your system can be susceptible to partial updates, where one application reads the set of objects while another is writing to the same set.

Figure 4. MS Local Properties dialog box with multiple objects to update

Your application may be similar if it requires multiple dialog boxes to perform a single operation that requires changes to more than one object. When your application uses objects stored in Active Directory, you must take care to minimize inconsistencies caused by partial updates and collisions.

Attributes for an object are sometimes interdependent. For example, you might set the minimum and maximum range for a value along with the default value. The default value should always fall within the range of the minimum and maximum values. For this reason you should always modify all related attributes concurrently. This ensures that the version number and timestamp for all the attributes are identical. Consequently, reconciliation results in one consistent set of attributes rather than some inconsistent combination.

To avoid having your changes overwritten by someone else, or overwriting someone else's changes inadvertently, you can block changes to an object or set of related objects while you are accessing them. One way to do this is to use a lock, which can control write permission to objects in the directory. You should use a database or other nondistributed storage to hold this lock. When creating a lock, you can assume either pessimistic concurrency or optimistic concurrency.

Note   Windows 2003 Server applications have the ability to create objects with temporary life spans. Therefore, you could create a lock object with a lifetime of five minutes. After five minutes, the object deletes itself. To do this, objects must be in an Application partition unless the forest is in Windows .NET forest mode.

Assuming Pessimistic Concurrency

Pessimistic concurrency assumes that the data you want to update might change while you are accessing it. With this approach, you can protect your data while accessing it. The following procedure gives general steps for how you can use a lock to protect a series of objects in the directory. In this case, you set the lock before modifying any of the objects.

To use pessimistic concurrency:

  1. Set a lock that reserves the set of objects.
  2. Read the data from the directory into the editing dialogs.
  3. Permit the user to make changes to the data.
  4. When the user closes the last dialog:
    1. Write the changes to the directory.
    2. Release the lock.

The disadvantage of this approach is that it leaves the objects locked for the entire time you are modifying the objects.

Assuming Optimistic Concurrency

Optimistic concurrency assumes that the data you want to update will not change while you are accessing it. With this approach, you do not lock the data while accessing it. To ensure that the data has not changed before making any updates, you check the usnChanged attribute value for the objects to determine whether someone else has modified any of the objects. If the objects are unchanged, you change all the objects and then set a lock on the objects while you write the changed objects to the directory. This prevents the objects from being locked for an indeterminate period.

To use optimistic concurrency:

  1. Read the usnChanged attribute value for each object.
  2. Read the data from the directory into the editing dialogs.
  3. Permit the user to make changes to the data.
  4. When the user closes the last dialog:
    1. Set a lock that reserves the set of objects.
    2. Read the usnChanged attribute value for each object.
    3. If the values have not changed, write the changes to the directory.
    4. Release the lock.

The disadvantage to this approach is that someone else can modify the objects before the update is complete. You should inform the user when an attempt is made to update a record with changed data.

Versioning Information

Versioning is a technique that produces insensitivity to replication issues such as version skew and partial updates. When using versioning, you define an unchanging snapshot of your information. After you create a version for your objects, it is a trusted source for configuring an application, a user profile, or a device. You can use different versioning techniques depending on how often the data changes. This section considers versioning in general and then describes two variations: fixed versioning and time-based versioning.

Storing Data

You should store each set of data for a specific version in an appropriately labeled container. This makes it easier to protect access to the data and to perform operations on all data in a specific version. For example, by deleting the container "Version 1" you are assured that all of the data for version one is deleted. You should place the version containers within the container for the application or company that they represent. Figure 5 shows how you can store the data.

Figure 5. Using a container for each version of data

Validating Data

Applications that access a folder that contains all the data for one version should validate that the information in the folder is complete and unchanged. To do this you can use a checksum, a control object, or a designated folder name.

Using a checksum

One way to ensure that the set of data in a version is complete and consistent is to use a checksum. You can add a custom attribute to the schema for each object in the container to participate in the calculation of the checksum. When you have created all the data for a version, you calculate the checksum and store it in the version container. When an application accesses the data, it calculates the checksum from the data and compares it with the stored checksum value in the container. If the calculated and stored checksums agree, the version data is complete and unchanged.

Using a control object

The checksum technique of versioning is not practical when a version container stores large amounts of data. In such a case, you can create an additional control object in the container after creating all the version data to indicate that all the data is present. An attribute of this control object could indicate the total number of objects in the container as a validation mechanism. Using a control object is not as definite as a checksum, however, because each version data member is not individually represented. You cannot detect any modification of individual data members and the resulting version skew upon replication.

Changing a folder name

Another approach to marking the completion of an update for a version is the changing of the version container name. For example, you could set the container name to "temp" while you are writing the data. After you have written all the data, you change the name of the container to a valid name.

There is a problem with this approach: if the data for the container has not been replicated before the container name is changed, the DC will replicate the new container name first because the system determines that the container needs to be replicated to create a place to store its data. When replicating the container, all current changes, including the rename, are replicated. This can give the impression that all of the data in the container has been replicated, when in fact it may not have been. To avoid this problem, the application can wait until the data has replicated to each DC before changing the container name. This may not be practical for many applications.

Using Fixed Versioning

With fixed versioning, one set of data represents configuration information—for example, for an application or device installation. There can even be a separate set of version information for different releases of the same software or device. Installation of the software or device typically creates this information. When the application starts up, it accesses the data that corresponds to its version number. When you develop the application, you must decide what should happen if the version container is missing or the checksum is invalid. Possible options are:

  • Wait and retry later, hoping the data will be ready then.
  • Use a previous version of the data, if it exists.
  • Exit the program and log the error.

Using Time-Based Versioning

With time-based versioning, the information for one installation can change with time. For example, the information might be for a user profile or a network configuration. Often it is important that when this information changes, the configuration be accessed as a complete set. In such a case, first make a copy of the data to be updated and place it into a temporary container, where the changes are made. When the updates are complete, change the name of the temporary container to match when the updates occurred. The folder name can be the date or time, day of the week, or some other indication of when the change occurred. The application that reads the data looks for a container whose name is based on a predictable version designation, for example, the current date. If the desired container is not available, the application looks for a container with the previous version designation.

A variation of this approach is to use multiple-valued attributes to store transitory data. When updates are made to the attributes, they are prefixed with a timestamp. This way only one container gets used, and a lot less data is duplicated and replicated.

Applying Garbage Collection

Over a period of time, versioning information can occupy a lot of space. When you no longer need a version of the data, you can remove the data from the directory. Delete the unneeded version only after a newer version is fully created. The method you use to remove earlier versions of data is based on how you version your data:

  • With fixed versioning, you remove version containers when you uninstall the application that uses them or when you no longer use the devices that use the data.
  • With time-based versioning, you determine how many versions of the data are needed. You base cleanup frequency on how often you expect to create updated versions.

 

The information contained in this document represents the current view of Microsoft Corporation on the issues discussed as of the date of publication. Because Microsoft must respond to changing market conditions, it should not be interpreted to be a commitment on the part of Microsoft, and Microsoft cannot guarantee the accuracy of any information presented after the date of publication.

This White Paper is for informational purposes only. MICROSOFT MAKES NO WARRANTIES, EXPRESS, IMPLIED OR STATUTORY, AS TO THE INFORMATION IN THIS DOCUMENT.

Complying with all applicable copyright laws is the responsibility of the user. Without limiting the rights under copyright, no part of this document may be reproduced, stored in or introduced into a retrieval system, or transmitted in any form or by any means (electronic, mechanical, photocopying, recording, or otherwise), or for any purpose, without the express written permission of Microsoft Corporation.

Microsoft may have patents, patent applications, trademarks, copyrights, or other intellectual property rights covering subject matter in this document. Except as expressly provided in any written license agreement from Microsoft, the furnishing of this document does not give you any license to these patents, trademarks, copyrights, or other intellectual property.

© 2001 Microsoft Corporation. All rights reserved.

Microsoft, Active Directory, MSDN, Visual Basic, and Windows are either registered trademarks or trademarks of Microsoft Corporation in the United States and/or other countries.

The names of actual companies and products mentioned herein may be the trademarks of their respective owners.