Using ADSI, LDAP, and Network Management Functions With Active Directory

 

Microsoft Corporation

February 2002

Summary: This white paper discusses how ADSI, LDAP, and Network Management Functions are used with Active Directory, and describes various good programming practices that will help you use these API elements efficiently. (13 printed pages)

Contents

Introduction
Using Good Programming Practices with ADSI
Using Good Programming Practices with LDAP
Using Good Programming Practices with NETAPI

Introduction

Several application programming interfaces (APIs) exist for building applications for the Microsoft® Active Directory® directory service including: Active Directory Service Interfaces (ADSI), Lightweight Directory Access Protocol (LDAP), and Network Management Functions (NETAPI). The Platform SDK contains information about these APIs and how to use them when writing directory-enabled applications.

Regardless of which API you use, incorporating good programming practices for the chosen API improves the application. Note that the platform you use and the experience level of your users often restrict the selection of an API.

This white paper helps you to understand how selected API elements work and describes various good programming practices that will help you use the API elements efficiently. The white paper contains the following sections:

  • Using Good Programming Practices with ADSI
  • Using Good Programming Practices with LDAP
  • Using Good Programming Practices with NETAPI

Each section about an API briefly describes the API, presents the benefits of using the API, and provides a link to the MSDN site where you can find in-depth information.

This paper assumes that you are familiar with building applications for Active Directory. For background information, see Active Directory.

Using Good Programming Practices with ADSI

Active Directory Service Interfaces (ADSI) is a COM-based interface that supports multiple directories and multiple languages such as C++, C#, Java, Visual Basic, and Microsoft Visual Basic Scripting Edition (VBScript). ADSI comes with providers that are used to access servers running systems such as the Microsoft Windows NT® Server 4.0 operating system, Novell NetWare 3, Novell NetWare 4, and LDAP. The ADSI interface has the following advantages:

  • ADSI requires less code and is simpler to use than a similar LDAP operation.
  • ADSI is fully scriptable.
  • ADSI supports multiple programming languages.

For more information about ADSI, see Active Directory Service Interfaces.

The following sections discuss good programming practices to follow when you create Active Directory–enabled applications with ADSI:

  • Reduce the Number of Binds
  • Optimize for Performance
  • Use a Serverless Bind for Fault Tolerance
  • Implement a Fast Bind
  • Use Multithreading Properly

Reduce the Number of Binds

One of the most time-consuming operations on a server running Active Directory is an authentication call that produces a bind; for example, calls to the ADsGetObject function, the ADsOpenObject function, or the IADsOpenDSObject::OpenDSObject method. LDAP terminology refers to this as an LDAP bind call.

A bind call produces a unique connection and handle to the server. Therefore, perhaps the most important thing you can do to improve an application is to minimize the number of binds generated by an application.

ADSI helps reduce binds by maintaining a bind cache. The bind cache contains connection handles to the appropriate servers based on the credentials provided by an application. The bind cache also contains connection characteristics, such as encryption and the authentication mechanism. ADSI can use one of these cached handles if the credentials — which include the server name, user name, password, and flags passed to the bind call — are equivalent.

Note   For operating systems in the Microsoft Windows® 2000 Server family, equivalent credentials include an identical user name, password, and flags when the server or domain is the same. For operating systems in the Microsoft Windows 2003 Server family, the identical flags restriction is relaxed. If the ADS_FAST_BIND and ADS_SERVER_BIND flags differ, another bind is not performed.

The bind cache keeps an entry until the application releases all object references to that connection. Therefore, keeping an outstanding object reference is of vital importance while using a server with the same user context.

Degrading Performance

The following Visual Basic script is an example of an expensive bind call. The code reuses the user object to bind to the server a second time. This results in degraded performance because of the time required to make the additional bind.

Dim oUser as IADsContainer

' Bind to a user object.
Set oUser = GetObject("LDAP://cn=myUser,dc=myComapany,dc=com")
Set oUser = Nothing

' Bind to a second user object.
' Because this reuses the oUser variable, it binds again to the server.
Set oUser = GetObject("LDAP://cn=myUser2,dc=myCompany,dc=com")

Improving Performance

There are two ways to improve performance. The first way is to use two different user objects to bind to the server, as shown in the following Visual Basic script:

Dim oUser as IADsContainer
Dim oUser2 as IADsContainer

' This Getobject call creates a bind to the server.
Set oUser = GetObject("LDAP://cn=myUser,dc=myCompany, dc=com")

' These GetObject calls do not create additional binds because
' the oUser object has an outstanding reference to the server.
Set oUser2 = GetObject("LDAP://cn=myUser2,dc=myCompany,dc=com")
Set oUser2 = GetObject("LDAP://cn=myUser3,dc=myCompany,dc=com")

The second way to improve performance is to keep a reference to the RootDSE object to prevent additional bind requests to the server, as shown in the following Visual Basic script:

Dim oRootDSE as IADsContainer
Dim oUser as IADsContainer

' This GetObject call creates a bind to the server.
Set oRootDSE = GetObject("LDAP://RootDSE")

' These GetObject calls do not create new binds because
' the oRootDSE has an outstanding reference to the server.
Set oUser = GetObject("LDAP://cn=myUser,dc=myCompany,dc=com")
Set oUser = Nothing
Set oUser = GetObject("LDAP://cn=myUser2,dc=myCompany,dc=com")

Using ADsGetObject and ADsOpenObject

When you use the ADsGetObject function, you use the default credentials, which are for the currently logged on user. If you use the ADsOpenObject function instead of the ADsGetObject function, you can explicitly specify credentials instead of using the defaults. By explicitly specifying equivalent credentials when binding to an object, you can use the cached connection on subsequent calls.

If you use the current user context (username = NULL and password = NULL) with the ADsOpenObject function, the bind cache is reused only when the entire user context is exactly the same on different calls. If you are impersonating a different user on different calls, a connection in the bind cache is used only when there is already a connection to the server in question and it has the same user context.

Optimize for Performance

Searching the directory is by far the most common operation performed on a directory. Different types of searches impact server performance differently. This section describes what you can do to reduce the number of searches and the amount of data needed for searches

The following Visual Basic script gets the distinguished name and name attributes for a user called myUser. Subsequent examples show how to improve the performance of this code.

Dim oUser as IADsContainer
Dim dn as IADs
Dim name as IADs

' The following statement does a base object search
' for only the objectClass attribute.
Set oObj = GetObject("LDAP://cn=myUser,dc=myCompany,dc=com")

' The following statement does a base object search
' for all attributes of the object.
Set dn = oObj.Get("distinguishedName")

' The following statement does not contact the server
' because the preceding statement caches all attributes.
Set name = oObj.Get("Name")

You can optimize the preceding code by using the GetInfoEx method. The GetInfoEx method loads into the cache only the information you want.

Dim oUser as IADsContainer
Dim dnName as IADs
Dim dn as IADs
Dim name as IADs

' The following statement does a base object search
' for only the objectClass attribute.
Set oObj = GetObject("LDAP://cn=myUser,dc=myCompany,dc=com")

' The following statement does a base object search
' for only the distinguishedName and Name attributes of the object.
Set dnName = oObj.GetInfoEx(Array("distinguishedName", "Name"), 0)

' The following statement does not contact the server
' because the preceding statement caches the attribute.
Set dn = oObj.Get("distinguishedName")

' The following statement does not contact the server
' because a preceding statement caches the attribute.
Set name = oObj.Get("Name")

The following code example shows the same optimization using C++:

HRESULT hr;
IADs*   pADs;
VARIANT var;

// The following statement does a base object search
// for only the objectClass attribute.
hr = ADsGetObject(L"LDAP://cn=myUser,dc=myCompany,dc=com",
                  IID_IADs, (void**)&pADs);

VariantInit(&var);

// The following statements do a base object search
// for only the distinguishedName and Name attributes of the object.
LPWSTR pszAttrs[] = { L"distinguishedName", L"Name" };
DWORD  dwNumber   = sizeof( pszAttrs ) /sizeof(LPWSTR);

hr = ADsBuildVarArrayStr( pszAttrs, dwNumber, &var );
hr = pADs->GetInfoEx(var, 0);
VariantClear(&var);
 
// The following statements do not contact the server
// because preceding statements cache the attribute.
hr = pADs->Get(L"distinguishedName",&var);  
printf("distinguishedName = %S\n", V_BSTR(&var));
VariantClear(&var);

// The following statements do not contact the server
// because preceding statements cache the attribute.
hr = pADs->Get(L"Name", &var);
printf("Name = %S\n", V_BSTR(&var));
VariantClear(&var);

Using a Serverless Bind for Fault Tolerance

When using the ADsOpenObject and ADsGetObject functions, do not specify a domain name or a server name for the path name. This case, known as a serverless bind, automatically connects to any available server in your domain. A serverless bind prevents your code from failing when a specified server is unavailable.

The following code example shows how to use the ADsOpenObject function to perform a serverless bind:

ADsOpenObject("LDAP://DC=domain, DC=com",...)

If you must specify a server, use the ADS_SERVER_BIND flag to avoid unnecessary or incorrect queries to the DNS server. For more information, see the Knowledge Base article ADsOpenObject(), ADsGetObject(), OpenDSObject() Functions May Generate Incorrect DNS Queries.

The following code example shows how to use the ADsOpenObject function to bind to a specific server using the ADS_SERVER_BIND flag:

ADsOpenObject("LDAP://server/domain.com/DC=domain, DC=com",..., ADS_SERVER_BIND)

Implement a Fast Bind

When binding to an object using ADSI, ADSI makes a request to the server to determine the object class of the object in the bind call. ADSI does this to retrieve the appropriate interfaces and ADSI extension interfaces for the object.

If you are a knowledgeable user and are writing an application that has no need for an advanced interface, you may want to use the ADS_FAST_BIND flag. When you set this flag in a bind call, ADSI does not query the objectClass property. Doing this exposes only the base interfaces supported by all ADSI objects. You can use this option to boost performance in a series of object manipulations that involve only methods of the base interfaces. Examples of base interfaces are the IADs, IADsContainer, IDirectoryObject, and IDirectorySearch interfaces.

The following code example shows how to use the ADS_FAST_BIND flag to establish a fast bind and how to use the results of the bind:

Dim oNamesp as IADsUser
Dim oObj as IADs
Dim dnName as IADs
Dim dn as IADs
Dim name as IADs

' Get a container for the provider.
Set oNamesp = GetObject("LDAP:")

' Bind using the ADS_FAST_BIND flag to save one roundtrip to the server.
Set oObj = oNamesp.OpenDSObject("LDAP://cn=myUser,dc=myCompany,dc=com", 
     vbNullString, vbNullString, ADS_FAST_BIND | ADS_SECURE_AUTHENTICATION)

' Get just the name and distinguishedName attributes of the object
' using a base object search, asking for the distinguishedName and
' name attributes.
Set dnName = oObj.GetInfoEx(Array("distinguishedName", "Name"), 0)

' Get just the distinguishedName attribute, which is now in the cache.
' This requires no roundtrips to the server.
Set dn = oObj.Get("distinguishedName")

' Get just the name attribute, which is now in the cache.
' This requires no roundtrips to the server.
Set name = oObj.Get("Name")

' Although you can still use the IADs::Get method to return the
' name attribute, you cannot change the password using the
' IADsUser::SetPassword method.

Use Multithreading Properly

The default LDAP provider for ADSI is not thread safe, but multithreaded applications can access the ADSI interface. If you develop a multithreaded ADSI application, you should carefully coordinate access of data among the threads by properly using synchronization objects, such as semaphores, mutexes, critical sections, and so forth.

When more than one thread binds to a specific object in the directory, a roundtrip to the server for each thread is necessary to retrieve information. This occurs because ADSI caching is done independently for each thread. However, each thread uses the same connection when it binds with the same credentials, which reduces the performance of each thread. To increase performance of a multithreaded application, start a separate process for each thread and make a new connection in each process. This adds overhead to each bind operation, but it also increases overall performance because there are multiple connections.

Using Good Programming Practices with LDAP

Lightweight Directory Access Protocol (LDAP) is based on RFC 1823 and permits low-level access to a directory from C and C++ applications. Usually, LDAP provides faster access than ADSI, but LDAP is also more difficult to code than ADSI. The LDAP library accompanies the Microsoft Visual C++® development system. The LDAP interface has the following advantages:

  • LDAP is thread safe.
  • LDAP makes creating multiple connections easy.
  • LDAP provides access to more functionality than ADSI.
  • LDAP has extended operations.
  • LDAP provides access to controls.
  • LDAP requires less overhead.
  • LDAP can be asynchronous.
  • LDAP supports multiple platforms and directories.

For more information about LDAP, see Lightweight Directory Access Protocol API.

The following sections discuss good programming practices to follow when you create Active Directory-enabled applications with LDAP:

  • Establish a Connection Using a Fully Qualified Domain Name
  • Use Timeouts with Operations
  • Avoid Simple Binds
  • Retrieve the Correct Error Code
  • Use Multithreading and Connections Carefully

Establish a Connection Using a Fully Qualified Domain Name

When connecting to a server running LDAP, always try to specify the fully qualified DNS domain name in the HostName parameter of the ldap_init function. When you specify a DNS domain name, you get the following benefits:

  • Fault tolerance

    If your domain controller goes down, LDAP will transparently reconnect to another domain controller in the domain.

  • Kerberos security

    You are more likely to find a server that supports Kerberos protocol as opposed to Windows NT Challenge/Response (NTLM) protocol. This also means that you will be able to optionally seal and sign your LDAP traffic.

  • Mutual authentication

    When using Kerberos security, you can perform mutual authentication.

If you cannot specify the fully qualified DNS domain name, you can use other forms for the HostName parameter. The formats for specifying a HostName parameter are (in order of preference):

  1. Fully Qualified Domain Name (FQDN), such as dev.myCompany.com
  2. Fully Qualified Machine Name (FQMN), such as myComputer.dev.myCompany.com
  3. Flat domain name, such as dev
  4. NETBIOS computer name, such as myComputer
  5. IP address, such as 127.0.0.1

Note   If you specify a NETBIOS computer name or an IP address, you cannot perform mutual authentication.

You might also want to keep the following information in mind when making a connection:

  • If you cannot specify a domain name, use the ldap_set_option function to specify the LDAP_OPT_AREC_EXCLUSIVE flag before calling the ldap_init function. This instructs the DNS server to perform an A-type record lookup for the server to prevent incorrect DNS queries from being sent over the network. For more information, see the Knowledge Base article ADsOpenObject("LDAP://RootDSE", ....) Call Generates Incorrect DNS Queries on the Network.
  • LDAP tries to connect to the same domain controller when there are multiple calls from the same process. The locator caches the domain controller name and reuses it on subsequent calls.
  • To specify a timeout when performing an LDAP connection, use an ldap_init function call, an ldap_set_option function call to specify an LDAP_OPT_TIMELIMIT value, and then an ldap_connect function call.
  • The default version of a newly created LDAP connection is LDAP_VERSION2. When you know that the server supports version 3, you must change the version to LDAP_VERSION3. Prior to making a bind, set the LDAP_OPT_VERSION option of the ldap_set_option function.

Use Timeouts with Operations

Try to always use timeouts with LDAP operations. In a distributed system, the availability of a specific server is not certain because maintenance occasionally takes them out of service. If you use timeouts, your synchronous calls time out instead of never returning. Most synchronous calls accept timeouts. If a synchronous operation does not take a timeout, use the asynchronous counterpart.

Avoid Simple Binds

Avoid using simple binds. While they are part of the LDAP specification, Microsoft considers them to be a security risk because they send clear text passwords over the network. In addition, if referral chasing is turned on, the LDAP client will not rebind when the primary connection uses a simple bind. This is because it would be forced to send the credentials across in clear text. Not rebinding might lead to inconsistent results or no results. Instead, use the LDAP_AUTH_NEGOTIATE authentication method with the ldap_bind_s function.

Retrieve the Correct Error Code

A call to the LdapMapErrorToWin32 function does not always return the expected results when an error occurs in the Microsoft Win32® application programming interface. Instead, use the ldap_get_option function to find out more precisely what happened. Use the LDAP_OPT_SERVER_ERROR option to obtain the string value of the most recent server error or the LDAP_OPT_SERVER_EXT_ERROR option to obtain the Win32 error code of the most recent Win32 server error.

The following code example shows how to obtain the string value:

PWCHAR pString;
ldap_get_option(ld, LDAP_OPT_SERVER_ERROR, (void*)&pString);
// Do what you want with the string here.
ldap_memfree(pString);

The following code example shows how to obtain the Win32 error code:

ULONG error;
ldap_get_option(ld, LDAP_OPT_SERVER_EXT_ERROR, (void*)&error);

Use Multithreading and Connections Carefully

The rules for multithreaded applications do not depend on whether each thread shares a connection or creates its own connection. One thread will not block while another thread is making a synchronous call over the same connection. By sharing a connection between threads, an application can save on system resources. However, multiple connections give faster overall throughput.

Most LDAP calls are thread safe even when sharing the same connection handle. The one exception is the LDAP bind. Do not attempt LDAP binds simultaneously from two threads using the same connection.

When making LDAP calls, use the return code from the specific call, or use the ldapGetLastError function to determine the success of an operation. Using the connection handle to read the error code is not safe because an LDAP call from another thread may overwrite it.

If you are using multiple connections in one thread, check the return code from each call, or use the ldapGetLastError function after each LDAP call. The ldapGetLastError function keeps one error code per thread, and successive calls from one thread overwrite this value.

Using Good Programming Practices with NETAPI

Network Management Functions (NETAPI) provides the ability to manage user accounts and network resources from C and C++ applications. The NETAPI interface has the following advantages:

  • NETAPI provides functionality that is not available in other APIs.
  • NETAPI can be used when Active Directory is not available.

For more information about NETAPI, see Network Management.

Note   Where possible, you should use ADSI to access Active Directory and not NETAPI calls.

The following sections discuss good programming practices to follow when you create Active Directory-enabled applications with NETAPI:

  • Balance Accesses Across Multiple Domain Controllers
  • Avoid Enumeration Calls
  • Let Active Directory Perform Access Checks
  • Ask for Only What You Need

Balance Accesses Across Multiple Domain Controllers

When accessing information in Active Directory, use function calls that support load balancing across multiple domain controllers instead of targeting a single domain controller. For example, the NetGetDCName function always returns the name of the primary domain controller. Conversely, the DsGetDcName function returns the name of a different domain controller on successive calls, depending on the specified domain controller selection criteria. So whenever appropriate, use the DsGetDcName function instead of the NetGetDCName function.

Avoid Enumeration Calls

Avoid enumerating the entire domain because doing so returns information about each object type in a domain, and that can be a very time-consuming operation. For example, use the NetUserEnum, NetGroupEnum, and NetQueryDisplayInformation functions to enumerate user and group accounts and other related information in a domain.

The NetQueryDisplayInformation function quickly returns user, computer, and group account information for display in user interfaces because it executes faster than the NetUserEnum and NetGroupEnum functions.

Conversely, the NetUserEnum function returns detailed account information. First, it calculates the total number of users from Security Access Manager (SAM). Then, it does a second enumeration to get each user's relative ID. Later, it makes a call to the SamQueryInformationUser function for each user to retrieve the required account information.

The NetQueryDisplayInformation function in a Windows 2000 Server domain still can have low performance when compared to a Windows NT Server 4.0 domain. This is because a Windows 2000 Server domain can include many more objects than a Windows NT Server 4.0 domain.

Let Active Directory Perform Access Checks

When executing an operation, your application might do its own access check to determine whether the specified user is allowed to access the needed resource. It might use the NetUserGetGroups function to retrieve a list of global groups to which the specified user belongs. Such group queries can severely affect server performance. Also, an access check done this way is not always accurate because it is incomplete. When Active Directory computes an access token, it not only includes the list of groups, but it also includes privileges, owner SID, and additional information. A preferred scenario is to attempt to access the resource, and let Active Directory determine whether the user has the appropriate credentials to perform the operation.

Ask for Only What You Need

Functions like the NetUserGetInfo function and the NetGroupGetInfo function return different amounts of information depending on parameters that you specify. When calling these functions, be sure to consult the documentation for these functions. Specify the parameters that provide only the information you require. This minimizes network traffic.