18.3 ADSI

Before you can start writing scripts that use ADSI, you first need to understand the basic COM concept of interfaces and ADSI's concepts of namespaces, programmatic identifiers (ProgIDs), and ADsPaths.

18.3.1 Objects and Interfaces

A COM interface defines the properties associated with an item, how to access those properties, and how to access specific functionality of the item, more commonly referred to as an object. For example, WSH has a number of objects that represent files, shortcuts, network access, and so on. ADSI provides a specification for interfaces that each directory service provider must implement to maintain uniformity. Each ADSI interface normally supports methods that can be called to perform a specific action, and properties (or property methods) to retrieve information about the object.

A method is a procedure or function that is defined on an object and interacts with the object. So an interface to access Active Directory group objects would have Add and Remove methods, so that members could be added or removed from a group. Methods are normally represented as Interface::MethodName when referenced, and this is the form we adopt in this book. Objects also have properties that are retrieved using the IADs::Get or IADs::GetEx methods and set or replaced using the IADs::Put or IADs::PutEx methods.

Each ADSI object supports an IADs interface that provides six basic pieces of information about that object:

Name

Relative name for the object (RDN in the case of Active Directory)

ADsPath

Unique identifier for object

GUID

128-bit Globally Unique Identifier of object

Class

Objectclass of the object

Schema

ADsPath to the objectclass of the object

Parent

ADsPath to the parent object

If you wanted to retrieve the GUID property of an object, you would use the following:

strGUID = objX.Get("GUID")

You can see that we are calling the IADs::Get method on the object called objX; the dot (.) indicates the invocation of a property or method. The IADs::Get method takes as its one parameter the property to retrieve, which in this case is the GUID, and passes it out to a variable that we have called strGUID. So that you do not have to use the IADs::Get method for the most common properties, certain interfaces define these common properties with property methods. In these specific cases, you use the dotted method notation to retrieve the property by using the property method of the same name. In the previous GUID example, the GUID property has a property method of the same name (i.e., IADs::GUID). We could therefore retrieve the GUID with:

strGUID = objX.GUID

We won't go into the interfaces in any more depth here; we just want to give you a feel for the fact that methods and properties can be accessed on an object via ADSI interfaces. Although an object can support more than one interface without a problem, each object supports only the interfaces that are relevant to it. For example, the user object does not support the interface that works for groups. The other interfaces, of which there are around 40, begin with the prefix IADs. Interfaces can relate to many different types of objects, including objects that reside in directory services (e.g., IADsUser and IADsGroup), transient objects that don't exist in a directory service (e.g., IADsPrintJob), and security-related objects (e.g., IADsOpenDSObject and IADsAccessControlList). Note that not all objects have a specific IADs interface that applies its objectclass (e.g., IADsUser), so in those cases you have to use the more generic IADs or IADsContainer interfaces.

Because each directory service is slightly different, not every ADSI interface method and property works in every directory service. If you make a method call to a directory service that doesn't support that method, you'll receive an error message specifying that the provider doesn't support that method. According to the ADSI specification, each service provider must reject inappropriate calls with the correct ADSI error message.

18.3.2 Namespaces, ProgIDs, and ADsPaths

To reference different types of servers (e.g., Windows NT 4.0, NetWare, etc.) with ADSI, you must use the namespaces that correspond to the ADSI providers used by that directory service. ADSI uses a unique prefix called a ProgID to distinguish between these namespaces. Each ProgID is synonymous with a particular namespace and directory provider.

In a script, you specify the ProgID, which is used behind the scenes to correctly connect and bind to the corresponding directory service. For example, you specify WinNT:// to access individual Windows NT 3.51, 4.0, Windows 2000, and Windows Server 2003 systems; you use LDAP:// to access Active Directory and other LDAP directories. When ADSI encounters the ProgID, ADSI loads an appropriate ADSI-provider DLL to correctly process the bind request and method invocations.

ProgIDs are case-sensitive. WinNT:// will work, whereas WINNT:// will not.

Since each ProgID is synonymous with a particular namespace, the term ProgID usually is dropped. For example, individual systems are accessed using the PRogID WinNT:. However, conventionally, this namespace is referred to as the WinNT namespace rather than the WinNT ProgID. This is the convention adopted in the book.

This references JoeB, a user on computer MOOSE in WORKGROUP:

WinNT://WORKGROUP/MOOSE/JoeB

This references JoeB, a user on computer MOOSE:

WinNT://MOOSE/JoeB

As these examples show, you can reference each object by using only its name or, more properly, by using its name and type, if two or three identically named objects with different types exist.

Each namespace has a unique format for the ADsPath string, so you need to make sure that you're using the correct ADsPath notation. For example, each of these ADsPaths references a unique object.

This ADsPath references JoeB, a user in DOMAIN:

WinNT://DOMAIN/JoeB, User

This next one references JoeB, a user in the Finance Organizational Unit (OU) within the Mycorp organization of the IntraNetWare tree called MyNetWareTree:

NDS://MyNetWareTree/O=MYCORP/OU=FINANCE/CN=JoeB

This one references JoeB, a NetWare 3.x or 4.x (bindery services) user that exists on server MYSERVER:

NWCOMPAT://MYSERVER/JoeB

Finally, this one references the WWW service component of IIS running on the local host:

IIS://localhost/w3svc/1

In the preceding examples, NDS: refers to IntraNetWare 5.x and 4.x. (Because IntraNetWare 5.x is LDAP-compliant, you also can use LDAP paths with it.) NWCOMPAT: refers to NetWare 4.x, 3.2, 3.12, and 3.11 servers in bindery-emulation mode. IIS: refers to metabase paths on a host running IIS 3.0 or later.

One of the most commonly used namespaces is the LDAP namespace. You can use LDAP with ADSI to access a variety of directory services, including Active Directory. Although you can use the WinNT namespace to access Active Directory, you need to use the LDAP namespace to fully utilize all of ADSI's methods and properties. For this reason, our primary focus will be on the LDAP namespace.

You can use several formats to refer to LDAP directories. For example, all the following ADsPaths reference the Administrator object within the Users container of the moose directory server in the mycorp.com zone:

LDAP://cn=administrator,cn=users,dc=mycorp,dc=com
LDAP://moose.mycorp.com/cn=administrator,cn=users,dc=mycorp,dc=com
LDAP://moose/cn=administrator,cn=users,dc=mycorp,dc=com
LDAP://DC=com/DC=mycorp/CN=Users/CN=Administrator
LDAP://moose.mycorp.com/DC=com/DC=mycorp/CN=Users/CN=Administrator

In these examples, CN stands for common name, and DC stands for domain component. These examples show that you can specify the LDAP namespace ADsPath going down or up the hierarchical Directory Information Tree (DIT). Most people have adopted the naming style used in the first three examples, where the most specific element of an object is used first. Also note that you can specify a fully qualified Domain Name System (DNS) server name after LDAP://, using a forward slash character (/) to separate the DNS server name from the rest of the path.

If a name includes some unusual characters, such as a forward slash or a comma, you can use double quotation marks ("/") or a single backslash (\) to specify that the character should be interpreted as part of the ADsPath itself. For example, if you have a user called AC/DC on the server, this is wrong:

LDAP://cn=ac/dc,cn=users,dc=amer,dc=mycorp,dc=com

This will interpret the path using cn=ac followed by dc followed by cn=users and so on. As dc on its own is not a valid part of the path, the ADsPath is invalid. Here are the correct paths:

LDAP://cn=ac\/dc,cn=users,dc=amer,dc=mycorp,dc=com
LDAP://"cn=ac/dc",cn=users,dc=amer,dc=mycorp,dc=com

Obviously, as the backslash is a special character, you would need to do the following for an object called cn=hot\cold:

LDAP://cn=hot\\cold,cn=users,dc=amer,dc=mycorp,dc=com
LDAP://"cn=hot\cold",cn=users,dc=amer,dc=mycorp,dc=com

The first specifies that the character following the first backslash is to be interpreted as part of the name, and the latter says to specify that the whole first name is a valid string.[2]

[2] Unfortunately, the latter, while valid, will not work with VBScript's GetObject function due to the extra quotation marks ("/").

When to Use the LDAP and WinNT Namespaces

Contrary to popular belief, the fact that WinNT namespace is used to access Windows NT servers does not mean it is of little use to Windows 2000 and Windows Server 2003. Actually, while the LDAP namespace is used to access Active Directory, the WinNT namespace is used to access users, groups, and other objects on individual computers. Active Directory only exists on DCs in your forest. If you have a server or client that is a member of a workgroup or domain, that machine also has objects on it. These could be local users, such as Administrator or Guest, printers, shares, and so on. Obviously, these objects are not part of Active Directory if they are unique to the machine. As individual machines do not support direct access via LDAP, you have to use the WinNT namespace.

18.3.3 Retrieving Objects

Now that you know how to use ADsPaths to distinguish between different namespaces, we'll demonstrate how to establish a connection and authenticate to the server containing the directory service you want to access. Authenticating a connection isn't always necessary; some directories, such as Active Directory, can allow anonymous read-only access to certain parts of the directory tree if you configure it that way. In general, allowing anonymous access is not a good practice. It can make things much more difficult to troubleshoot if you discover that one of your domain controllers is being impacted by an overzealous client. When using ADSI, if authentication is not done explicitly, the credentials of the account the script is running under will be used. If the account running the script is not part of the Active Directory you want to query or in a trusted domain, you will not be able to do very much. That's why performing explicit authentication in ADSI scripts is generally the best way to go.

If you just want to use the current account's credentials to bind to a directory server to get a reference to an object, use the GetObject function:[3]

[3] Visual Basic and JScript also have the GetObject function.

Dim strPath      'path to the directory server
Dim objMyObject  'root object of the directory
   
strPath = "LDAP://dc=amer,dc=mycorp,dc=com"
   
Set objMyDomain = GetObject(strPath)

The code begins by declaring two variables with VBScript Dim statements. The first variable, strPath, is an ADsPath. The prefix str specifies that this ADsPath is a text string; see the sidebar about typical VBScript naming conventions. The second variable, objMyDomain, is a pointer to the object in the directory that the ADsPath represents. The prefix obj specifies that the variable is an object.

Next, we assign the strPath variable to the path of the directory server we want to bind to, in this case, LDAP://dc=amer,dc=mycorp,dc=com. You need to enclose this path in quotation marks, because it's a text string.

Finally, we use VBScript's Set statement with the GetObject method to create a reference between the variable we declared and the existing object we want to interact with. In this case, we're creating a reference between objMyObject and the existing object that the ADsPath LDAP://dc=amer,dc=mycorp,dc=com represents (i.e., the domain object of the amer.mycorp.com domain). After we've established this reference, we can use other IADs-based interfaces to interact with that object.

Variable Prefix Conventions

You can use whatever name you like for a variable. However, the consensus is to use a prefix with a descriptive name. The prefix, which represents the type of data, typically contains one lowercase character or three lowercase characters. Commonly used three-character prefixes include:

  • str = string

  • int = integer

  • bol = boolean

  • obj = object

  • arr = array

  • lgn = long integer

  • sgl = single precision value

  • dbl = double precision value

In the descriptive name, you capitalize the first letter of each word but don't put hyphens between words, for example: strMyPassword.

To explicitly authenticate to a directory server, use the IADsOpenDSObject interface, which contains only one method: OpenDSObject, which takes four arguments:

  • ADsPath to authenticate to

  • User DN or UPN to bind as

  • User's password

  • Additional security setting(s)

The following listing shows how to use IADsOpenDSObject::OpenDSObject to authenticate to a directory server. We begin by declaring three string variables (strPath, strUsername, and strPassword) and two object variables (objNamespaceLDAP and objMyObject):

Dim strPath            'path to authenticate to in the directory service
Dim strUsername        'DN of the username
Dim strPassword        'plain text password
Dim objNamespaceLDAP   'ADSI namespace object
Dim objMyObject        'root object of the directory
   
strPath = "LDAP://dc=amer,dc=mycorp,dc=com"
strUsername = "cn=Administrator,cn=Users,dc=amer,dc=mycorp,dc=com"
strPassword = "the password goes here in plain text"
   
Set objNamespaceLDAP = GetObject("LDAP:")
Set objMyObject = objNamespaceLDAP.OpenDSObject(strPath,strUsername,strPassword,0)

We then assign the strPath, strUsername, and strPassword variables the appropriate ADsPath, username, and password strings. The username string, which is also called the Distinguished Name (DN), references the username's exact location in the directory. A User Principal Name (UPN) can also be used in place of a DN. A UPN typically has the format of username@ForestDnsName (e.g., administrator@mycorp.com).

The strPath is used to authenticate to a specific point in Active Directory if you wish. This can be used if the user authenticating does not have permission to work at the root and has to authenticate further down the tree.

Next, we use a Set statement with GetObject to create a reference for the variable called objNamespaceLDAP. Notice that we're using "LDAP:" rather than strPath as an argument to GetObject. Using the LDAP namespace might seem unusual, but it is necessary so that in the next line, you can call the IADsOpenDSObject::OpenDSObject method on the LDAP namespace that ADSI returns. The last IADsOpenDSObject::OpenDSObject argument is to specify any security settings that should be applied to the connection. When set to 0 or left blank, no security is enabled for the connection. That is typically not the optimal choice, considering that all traffic between client and server will be sent in plain text over the network.

The following two constants are important to use if at all possible:

ADS_SECURE_AUTHENTICATION (0x1)

Negotiates with the server to use the most secure authentication possible. For the WinNT provider, NT LAN Manager (NTLM) will be used. For Active Directory, Kerberos is the first option with NTLM being used if Kerberos isn't available.

ADS_USE_ENCRYPTION/ADS_USE_SSL (0x2)

Encrypts the data between client and server. SSL must be available on the target domain controller.

You use multiple constants by adding them togetheri.e., (ADS_SECURE_AUTHENTICATION + ADS_USE_ENCRYPTION) as they represent integer values. While these are defined constants, they cannot be used by name from VBScript. The entire set of values from the ADS_AUTHENTICATION_ENUM enumerated type can be found under the MSDN Library (http://msdn.microsoft.com/library/), by following this path: Networking and Directory Services Active Directory, ADSI and Directory Services SDK Documentation Directory Services Active Directory Service Interfaces Active Directory Service Interfaces Reference ADSI Enumerations ADS_AUTHENTICATION_ENUM.

We want to emphasize the importance of using encryption. If encryption is not used, anyone using a network sniffer such as NetMon on the network might be able to see the information being passed, including the username and password specified in the IADsOpenDSObject::OpenDSObject call.

The following code is slightly modified from the previous example to show how to enable ADS_SECURE_AUTHENTICATION and ADS_USE_ENCRYPTION for a connection:

Const ADS_SECURE_AUTHENTICATION = 1
Const ADS_USE_ENCRYPTION        = 2
   
Dim strPath            'path to authenticate to in the directory service
Dim strUsername        'DN of the username
Dim strPassword        'plain text password
Dim objNamespaceLDAP   'ADSI namespace object
Dim objMyObject        'root object of the directory
   
strPath = "LDAP://dc=amer,dc=mycorp,dc=com"
strUsername = "cn=Administrator,cn=Users,dc=amer,dc=mycorp,dc=com"
strPassword = "the password goes here in plain text"
   
Set objNamespaceLDAP = GetObject("LDAP:")
Set objMyObject = objNamespaceLDAP.OpenDSObject(strPath, _
                               strUsername, strPassword, _ 
               ADS_USE_ENCRYPTION + ADS_SECURE_AUTHENTICATION)

While securing the connection to the domain controller is an important precaution to take, including an administrator's password in a script can obviously be pretty insecure. If you don't want to include plain-text passwords, you have several options. The first option is to assign a value to strPassword from the VBScript InputBox function. The following listing shows this:

Const ADS_SECURE_AUTHENTICATION = 1
Const ADS_USE_ENCRYPTION        = 2
   
Dim strPath            'path to authenticate to in the directory service
Dim strUsername        'DN of the username
Dim strPassword        'plain-text password
Dim objNamespaceLDAP   'ADSI namespace object
Dim objMyObject        'root object of the directory
   
strPath = "LDAP://dc=amer,dc=mycorp,dc=com"
strUsername = "cn=Administrator,cn=Users,dc=amer,dc=mycorp,dc=com"
strPassword = InputBox("Enter the Administrator password","Password entry box")
   
Set objNamespaceLDAP = GetObject("LDAP:")
Set objMyObject = objNamespaceLDAP.OpenDSObject(strPath, _
                               strUsername, strPassword, _ 
               ADS_USE_ENCRYPTION + ADS_SECURE_AUTHENTICATION)

When you run the script, the InputBox prompts you to enter the administrator's password. However, the InputBox echoes the password in plain text while you type it into the password entry box, so this approach isn't terribly secure itself.

Three other options are secure. However, because VBScript doesn't natively support password retrieval boxes, you can't use these solutions without some work:

  • One solution requires that you obtain a custom ActiveX component for VBScript to extend WSH's functionality to natively support password dialog boxes. One such control is available for by downloading the code from the Windows Script Host Programmer's Reference by Dino Esposito, which can be found at http://www.wrox.com.

  • The second solution is to write a script in a language other than VBScript that supports password boxes natively. For example, you can use the Perl Tk modules to create an Entry widget with the -show parameter as an asterisk. For Perl aficionados, this Entry widget would look like this:

    $dlg->Entry(qw/-show * -width 35/)->pack(  ); # arbitrary width
  • The third solution requires that you write the script from within Active Server Pages (ASP). You use the password field in an ASP form to retrieve the password.

If you want to authenticate a connection but have already logged on to the directory, you can use the default credentials for your existing connection. You simply use the VBScript vbNullString constant in both the username and password fields, as the following listing shows:

Dim strPath            'path to authenticate to in the directory service
Dim objNamespaceLDAP   'ADSI namespace object
Dim objMyObject        'root object of the directory
   
strPath = "LDAP://dc=amer,dc=mycorp,dc=com"
   
Set objNamespaceLDAP = GetObject("LDAP:")
Set objMyObject = objNamespaceLDAP.OpenDSObject(strPath, vbNullString, _
  vbNullString,0)

Note the use of the underscore ( _ ) character on the second to last line. This tells VBScript that we have split this line from the next, but it should treat them as one long line. You can use multiple underscores to concatenate multiple lines together in this manner.

From now on, most of the scripts will use GetObject for simplicity, but if you need to, you can just as easily use IADsOpenDSObject::OpenDSObject without modifying any of the other code.



    Part II: Designing an Active Directory Infrastructure
    Part III: Scripting Active Directory with ADSI, ADO, and WMI