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.