In-Depth

Systems Engineering: Automating with ADSI

Active Directory Service Interfaces will make your Windows 2000 administrative work more versatile and flexible—and give you automation powers beyond the ordinary mortal.

If you’re preparing for a Windows 2000 migration, seeking a way to automate repetitive tasks in the administration of a Windows NT or NetWare enterprise, or looking for a means to configure an IIS Web site without going through the GUI, it’s worth taking the time to learn ADSI. In this article, I’ll give you some quick lessons in how to use ADSI in VBScript, primarily with Win2K. Some of the code comes from my book, Scripting Windows 2000.

Active Directory Service Interfaces is a Microsoft application programming interface (API) that allows access to a number of different types of enterprise directories. It’s a key tool in the administration of Win2K enterprises and can also be used to read from, write to, search, and manipulate NT Directory Services, Exchange 5.5 directories, Internet Information Server metabases, and NetWare 3.x and 4.x directories.

ADSI is useful for both developers and system administrators. In the Win2K world the use of ADSI lets coders directory-enable their applications, using Active Directory (AD) as both a source of data and a repository for application-specific information. There are caveats when doing this, of course. Since AD data is replicated throughout an enterprise to all domain controllers in a multimaster mode, you don’t want to use AD as a repository for frequently-changing information (unless you enjoy bogging down an enterprise).

Administrators can use ADSI to automate repetitive tasks or those jobs that are too complex when performed within the GUI. Suppose one of your offices gets hit with an area code change. Rather than going through each user’s property sheet and changing his or her phone numbers, you can accomplish your administrative task with less pain through a relatively short piece of ADSI code.

ADSI can be used by any programming language that can call automation/COM objects, including the Visual Basic family (VB, Visual Basic for Applications, and VBScript), Java, JScript, and several flavors of PERL. ADSI also provides lower-level interfaces for use by C and C++. The most recent version of ADSI, released with Win2K, is 2.5.

ADSI Architecture

ADSI is provider-based, meaning that different chunks of code translate standard ADSI calls into requests that a specific type of directory can understand. The four major providers that come with ADSI serve LDAP (Win2K, IIS, and Exchange 5.5), NT (NT Directory Services), NDS (NetWare 4.x), and NWCOMPAT (for NetWare 3.x directories). Client code calls ADSI functions, an ADSI router passes those calls to the particular provider needed, and the provider communicates with the actual directory service on the back end.

ADSI Object Model and Binding

Like any well-behaved API, ADSI’s structure can be represented by an object model, a hierarchical depiction of the objects it contains.

The Namespaces container is at the top of the ADSI object model. It contains Namespace containers for all the ADSI providers present on a given system. Here’s a brief bit of VBScript from that that lists all the providers on my overworked test system.

' [VBScript prov.vbs]
' this VBScript shows installed ADSI providers
dim ns
dim prov
dim coll
dim s
set ns = GetObject(“ADs:”)
for each prov in ns
s = s + prov.name + vbcrlf
next
wscript.echo s

If you’re familiar with calling automation objects, you’ll see that GetObject("ADs:") is the key to this code; it’s the command that connects, or binds, to the Namespaces container on the system. ADSI requires that you bind to an ADSI object or namespace before you can start mucking around with it. This is similar to the way you might work with any database; you have to connect to it before you can start the real effort.

Figure 1. prov.vbs lists available ADSI providers on the machine at hand.

To bind to a specific provider, just put the provider name into the GetObject, as in:

GetObject("WinNT:"), ("LDAP:"), ("NDS:"), ("NWCOMPAT:")

This will connect you to the root of the provider’s namespace, and from this starting point you’ll be able to access any object in the namespace. Bear in mind that the provider names are case-sensitive.

The LDAP and NT providers also allow you to bind to a domain or a DC, PDC, or BDC. This:

GetObject("WinNT://SW2K")

connects to the SW2K NT domain. This:

GetObject("LDAP://homelabdc")

binds to AD on the domain controller homelabdc. As a rule, you’re better off binding to a domain than to a particular system. That way your code won’t fail if the system in question’s down, not that that ever happens.

Two other types of serverless binding involve binding to the Global Catalog and RootDSE:

GetObject("GC://:")
GetObject("LDAP://RootDSE")

Binding to the GC is useful when you need access to objects from other domains for a query. Just remember that other domains don’t replicate all their properties to the Global Catalog.

From the RootDSE , you can connect to any of AD’s naming contexts.

Back to domains; once you’ve bound to a domain (we’ll use an NT domain for this example) , you can iterate through the objects it contains:

dim dom
dim obj
dim s
set dom = GetObject("WinNT://SW2K")
for each obj in dom
s = s + obj.name + vbcrlf
next
wscript.echo s

Since NT directories are pretty flat structures, with this code you’ll get a list of every object in the directory: users, groups, and computers. It’s an informative list, but you can’t do much with it. You can restrict the output to objects of a particular class with an if...then conditional if you wish:

for each obj in dom
if obj.class="User" then
s = s + obj.name + vbcrlf
end if
next

Case does matter when you’re working with class names. In the previous example, setting the class to be “user” rather than “User” will result in no items being returned at all. Binding to AD in a Win2K enterprise requires that you know a little something about LDAP; ADSI uses its LDAP provider to access AD. Here are two ways to bind to AD for scriptingwin2000.com:

set dom=GetObject("LDAP://SCRIPTINGWIN2000.COM")

set dom=GetObject("LDAP://DC=scriptingwin2000,DC=com")

Remember that an organization’s DNS structure is the underpinning to the design of its Active Directory. In the first example, we’re binding using the domain’s DNS name. In the second example, we’re using the Distinguished Name (DN) of the domain in the bind. Think of the DN as a description of what makes an object unique in AD.

Just as objects in a file system can be accessed through a path ("C:\WINNT\System32\another.dll", so can objects in a directory service. When using ADSI, this is referred to as an object’s ADsPath property. In the case of our LDAP binds, dom.ADsPath returns LDAP://DC=scriptingwin2000,DC=com. The ADsPath of Sam, a user in the MyDen organizational unit, is LDAP://HOMELABDC.SCRIPTING WIN2000.COM/CN=Sam, OU=MyDen, DC=SCRIPTINGWIN2000, DC=com. This is where a little bit of LDAP goes a long way. Sam’s ADsPath includes a CN, or common (canonical) name, an OU (organizational unit), plus two DC entries.

Listing and Modifying Properties

Now that we can bind to Sam, let’s discover more about him. The following code does the bind, then creates another object from Sam’s schema property. With the new object, we can list all of the properties, mandatory and optional, that Sam can have.

dim sam, binder,schobj,prop
set sam = GetObject("
LDAP://HOMELABDC.SCRIPTINGWIN2000.COM/CN=Sam,
OU=MyDen, DC=SCRIPTINGWIN2000, DC=com")

binder=sam.schema
set schobj=GetObject(binder)
wscript.echo "MANDATORY PROPERTIES:" & vbcrlf
for each prop in schobj.mandatoryproperties
wscript.echo prop
next
wscript.echo vbcrlf & "OPTIONAL PROPERTIES:" & vbcrlf
for each prop in schobj.optionalproperties
wscript.echo prop
next

Figure 2 shows a portion of the resulting output.

Figure 2. Some properties of the Active Directory user object.

Now we know how to bind to an AD object and how to list its properties. Changing a property is a two-step process. First, reassign the property, and then call the object’s SetInfo method:

sam.title = "sidekick"
sam.SetInfo

The first line places the property assignment into a local cache; it’s the SetInfo statement that actually commits the change to AD.

Alternatively, you can assign property values using the Put method:

sam.Put "title", " sidekick"

It’s a little clunkier than a standard property assignment, but it does emphasize that you’re working with an object in a database.

Creating and Deleting Objects

Let’s go through the process of creating a new user with a script. It’s a brief process, but worth explaining. First, you need to bind to the container in which you’re creating the user. Then you call the Create method of the container object, passing in the class of the new object, and in the case of a user, a CN. You also need to assign the new user a sAMAccountName—a username understandable by down-level (NT) domains and users.

At this point, you need to do a SetInfo. Then set the user’s AccountDisabled property to false, thereby enabling the account and do one more SetInfo. You’ve just created a user. This short script lets you pass in the value for the CN and sAMAccountName as one parameter:

set obj = GetObject(
"LDAP://HOMELABDC.SCRIPTINGWIN2000.COM/CN=Users,
DC=SCRIPTINGWIN2000, DC=com”)
set AddMe = obj.create("user",
"CN=" & wscript.arguments(0))
AddMe.put "samAccountName", wscript.arguments(0) addme.setinfo
addme.AccountDisabled=False
addme.setinfo

When you create objects on Win2K Professional systems or on Win2K Server member servers, you’re using a local database and need to use the NT provider. This is less complicated than with the LDAP provider:

dim obj
set obj = Get Object("WinNT://MyDomain/MyServer")
obj.create ("User", "Shemp")

Deleting objects involves using the Delete method when you’re in a container. Navigate to a container, then call the delete with a class name and CN:

obj.delete "user","CN=exuser"

Moving Objects

Moving an object from one container to another requires the use of the MoveHere method. You can’t move an object between namespaces. Don’t try to move a NetWare user to AD or vice versa this way; it doesn’t work.

set newobj = obj.movehere("LDAP://CN=SmallGuy, CN=Users, DC=scriptingwin2000,DC=com", "CN=Small Guy")

This snippet will move SmallGuy to whatever container obj is bound to. You can also rename the object during the move, if you wish.

Don’t Want To Script?

If white type against a black background gives you the willies, add the ADSI Edit snap-in to your MMC console and go graphical. (See Figure 3.)

Figure 3. The Windows 2000 Server Resource Kit includes two useful MMC snap-ins—ADSI Edit and AD Schema—for those who want to avoid the command line. (Click image to view larger version.)

ADSI Edit lets you view and modify the ADSI properties of objects in your AD, as well as allowing object creation and deletion. Likewise, the AD Schema snap-in lists the classes and attributes contained in the AD Schema.

It’s probably (make that definitely) a good idea to not allow wide distribution of these two snap-ins within your organization. These are administrator tools, and their indiscriminate use could seriously whack your well-thought-out architecture. Both snap-ins come with the Windows 2000 Server Resource Kit.

ADSI and IIS

The ADSI IIS provider allows for programmatic configuration of IIS Web servers. The following code takes a directory (Yow) living under my server’s Webroot, makes it an IIS virtual directory, turns it into an application called, “Yow!”, and turns on basic (cleartext) authentication for the directory.

dim MyWeb
dim mywd
set MyWeb=GetObject("IIS://localhost/w3svc/1/Root")
set mywd = myweb.create ("IIsWebVirtualDir", "Yow")
mywd.setinfo
mywd.appcreate True
mywd.appfriendlyname="Yow!"
mywd.AuthBasic=True
mywd.setInfo

Note the similarities in structure and process with the LDAP and WinNT providers, even though the database we’re configuring is an IIS metabase. The GetObject binds to the root of the first Web site instance on the local server by using its metabase path. We use a Create method, passing it a class (IIsWebVirtualDir) and an object name (Yow), and call SetInfo for these mandatory properties. The appcreate method makes the virtual directory a Web application, which we give the “appfriendlyname” of Yow!, then set basic authentication to True, and finally, a last SetInfo commits the additional property assignments.

Just as ADSI Edit and Active Directory Schema are useful snap-ins for ADSI programmers of AD, you’ll find Microsoft’s Metaedit, a visual metabase editor that comes with the IIS Resource Kit, a helpful aide to your IIS ADSI coding. Similarly, you’ll find Microsoft’s adsutil.vbs script in your Inetpub\AdminScripts directory. adsutil lets you set, list, create, modify, and delete metabase entries without having to write a lick of ADSI. Put the following four lines of code into a batch file (and make sure adsutil.vbs is in the same directory or the system Path), and you’ll duplicate the functionality of the VBScript above.

cscript adsutil.vbs create w3svc/1/root/Yow
"IisWebVirtualDir"
cscript adsutil.vbs appcreateoutproc w3svc/1/root/Yow
cscript adsutil.vbs set w3svc/1/root/Yow/AppFriendlyName
"Yow!"
cscript adsutil.vbs set w3svc/1/root/Yow/AuthBasic True

Searching Active Directory

There are times when you’ll want to query AD for all objects that satisfy one or more conditions (such as all users who have access to the payroll database server and no access to the color printer in building 13). To do this, you need to use a combination of ADSI and Active Data Objects (ADO) code.

ADO, like ADSI, is provider-based. When you set up an ADO connection to AD, you need to specify the ADsDSOObject. This script serves as an example.

dim connex, comm, results, feeled, binder
dim bindobj, mgrobj, resultsmgr
set connex = CreateObject("ADODB.Connection")
set comm = CreateObject("ADODB.Command")
connex.provider = "ADsDSOObject"
connex.open "Active Directory Provider"
set comm.ActiveConnection=connex
comm.commandtext= ;(
&(objectCategory=Person)(objectClass=user)
(sn=Presley)); name,manager;base;"
set results=comm.execute
results.movefirst
do while not results.eof
set binder=GetObject("LDAP://CN="
& results.fields("name")
& ",OU=MyDen,DC=SCRIPTINGWIN2000,DC=com")
resultsmgr = binder.manager
set mgrobj=GetObject("LDAP://"& resultsmgr)
wscript.echo "MyDen user "& results.fields("name")
&"’s manager is & mgrobj.cn
results.movenext loop

After declaring variables, we create an ADODB Connection object, and from that, an ADODB Command object. Think of Command as a command prompt for your ADO session. Command.text then becomes a command line at which we can run a query: comm..commandtext=yourqueryhere This particular query is going to look for users in the MyDen OU with the last name (sn, or surname) of Presley. For any object that satisfies the conditions of the query, it’s going to return the name of the object’s manager (manager is a property of organizationalPerson, from which class user is derived. “Base” in the query refers to its scope; other options are oneLevel, which searches one level down, and subTree, which searches down to the bottom level of that section of Active Directory.

So, the query returns a resultset (essentially a recordset, for those of you not up on ADO). We iterate through the resultset, using ADSI binds to retrieve the CN of the manager for each result and then echo that result to the command line.

The query used in the previous example was an LDAP query:

" (
&(objectCategory=Person)(objectClass=user)(sn=Presley));
name,manager;base;"

It uses an LDAP path to find its starting point, follows it with a number of conditional tests, and then ends up with the values to be extracted and scope. If the syntax is a little daunting, you have another option, one with which you might be more familiar; a SQL query. Here’s the equivalent query in SQL:

comm.commandtext="select name, manager from
''
WHERE object.Category = 'Person' AND objectClass='user'
AND sn='Presley'"

It’s a standard Select query, using the LDAP path as the pointer to the right location in AD.

Figure 4. The results of a query to AD.

No matter which query dialect you use, try to be fussy about case, proper use of single and double quotes, and additional spaces in your queries. When you least expect it, you can run up against these issues.

Homework

Need a less goofy example of a query? Try this one as an exercise on your own, using the previous script as a starting point. The area code of your office in AnyTown is changing from 555 to 111. The office is its own domain: anytown.mycorp.com, and users can be found in a number of containers in this domain. Query the active directory for all user objects in anytown.mycorp.com and then change the area code on those users’ office phone numbers. You’ll also have to check their home numbers and fax numbers to see if they need the area code change.

This exercise will incorporate ADSI, ADO, and a bit of old-fashioned string parsing. Have fun.

Finally

I thought it was a tough task paring down ADSI scripting to a single chapter in the book; it’s been even tougher to cover the basics in one article. What I wanted to show is that ADSI coding, particularly in scripts, is both an alternative and a supplement to GUI-based AD manipulation. I stuck with VBScript for the purposes of this article, though everything discussed can also be coded in JScript, several flavors of Perl—any language that can invoke automation interfaces. Let’s not forget VBA and Visual Basic, either; you’ll want to declare variables as specific ADSI types, but the programming environments, be it the Office Visual Basic Editor or the Visual Basic IDE, are a tad more robust than Notepad, don’t you think?

Additional Information

Load up on Resource Kits, TechNet, and MSDN. Remember that the MSDN Library is available online at http://msdn.microsoft.com/default.asp, and it includes more details than you’ll probably ever want to know about Active Directory objects, the WinNT provider, IIS metabase objects, Exchange 5.5 structure, and the two NetWare providers.

Take a peek inside any sample scripts you can find and analyze how they operate. Sample scripts are made for reverse engineering; you can learn a lot by following a script through execution. You’ll find plenty of scripts for study included in my book, Scripting Windows 2000, published by Osborne McGraw-Hill (ISBN 007212444X).

—Jeffrey Honeyman

The next time you’re faced with a directory-focused administrative task and have some time to spare, try writing an ADSI script instead of going through the GUI. The more familiar you become with ADSI, the more versatile you’ll become. Get comfortable with ADSI—and then take a look at how it can integrate with Windows Management Instrumentation (WMI) scripting. WMI adds extensions to ADSI, so that you can invoke WMI objects, properties, and methods through ADSI calls. Between these two APIs, there’s not a lot you can’t automate in the administration and troubleshooting of Win2K enterprises.

Featured