Active Directory Service Interfaces (ADSI) is a COM-based set of interfaces that allow you to interact with and manipulate directory service interfaces.

That means it's a cool way for scripts and code to add users, change passwords, create network groups, control IIS programmatically, and start and stop services. In this article, I'll cover the basic ADSI syntax and give you some example code to use in your own applications.

ADSI lends itself to automating a number of system administration tasks. When used in conjunction with the Windows Scripting Host (WSH), it can give you nearly full control of your Windows system (see John Petersen's article "The Windows Scripting Host" in the Spring 2000 issue of CoDe). Although perfect for system administrators, developers can also leverage this technology for tasks such as automating installations and controlling IIS when deploying web applications. You can take advantage of NT's security system in a web application by adding users via ADSI and letting NT do the work of validating them from that point on.

Background

ADSI provides a common set of interfaces for various network providers, so you can use a consistent interface. By default, the following ADSI providers are installed:

  • WinNT: Windows NT Networking provider
  • LDAP: Lightweight Directory Access Protocol (Exchange, Win2000)
  • NWCOMPAT: Novell NetWare 3.1
  • NDS: Novell Directory Services
  • IIS : Internet Information Server (installed during IIS installation)
  • ADSI is available for Windows NT by installing the NT4 Option Pack, but is installed automatically in NT service packs SP3 and above, and in Windows 2000. Microsoft also has client components for Windows 95 and 98, which can be used to access directory services on a networked Windows NT or 2000 machine. The examples shown here assume Windows NT or 2000.

This article is primarily centered around using the WinNT provider, although most of the concepts and code will be nearly identical for any of the other providers.

Every ADSI object exposes at least 6 properties: GUID - a unique identifier, Parent - the parent object, AdsPath - the Active Directory path to the object, Class - the class it belongs to, Schema - the schema that defines the object, and Name - our object's name.

Here's some Visual FoxPro code to show how this works. You will need to replace the name "OMNIBOOK_DEV" (the name of my notebook) with your system name. It seems that we should be able to use LOCALHOST as the system name, but ADSI expects the real system name. You cannot give it an IP address or a name that will be resolved into an IP address, such as LOCALHOST. To find the system name under Windows 2000, go to Control Panel, click the System icon, then click the Network Identification tab. The system name is listed as "Full computer name:". Under NT 4.0, go to Control Panel and double-click Network. You'll find the machine name and domain under the Identification tab.

oComputer=GetObject("WinNT://OMNIBOOK_DEV,computer")
? oComputer.guid
? oComputer.Parent
? oComputer.AdsPath
? oComputer.Class
? oComputer.Schema

On my system, this returns the following:

GUID:	{DA438DC0-1E71-11CF-B1F3-02608C9E7553}
Parent:	WinNT://WORKGROUP
AdsPath:	WinNT://WORKGROUP/OMNIBOOK_DEV
Class:	Computer
Schema:	WinNT://WORKGROUP/Schema/Computer

You can see that Parent returns the parent of the current AdsPath, and Class is from the second parameter in the GetObject call. So where did the name "WORKGROUP" come from? The WinNT provider gives us access to various properties of the NT security model. In this security model, users can be members of a Domain or Workgroup; a server can be a Primary Domain Controller (PDC), Backup Domain Controller (BDC), or standard server. We're getting a bit beyond the scope of this article, but in this case, "WORKGROUP" just happens to be the name of my computer's workgroup. It could just as easily have been named something else.

Because of these distinctions, sometimes it's necessary to specify in the path the system/user's workgroup or domain. NOTE: you can use the workgroup name instead of the domain name if, like me, you're part of a workgroup instead of a domain.

Finally, there's the GUID (Globally Unique Identifier). This is just the standard kind of GUID that Windows assigns to every COM object. You could use this to "bind" (create a reference to) the ADSI container object. Although this is faster than having the system look up the GUID for you, the number isn't very user-friendly. Use of the GUID in referencing the ADSI object also has a few other limitations, especially related to distribution.

Now, let's take a closer look at the syntax and the provider's "namespace". A namespace is a bounded area, or container, where names can be resolved into information or object references. In the GetObject call, we referenced the ADSI provider, passing it my machine name and the word "computer". In this case, "computer" is the name of one of the objects available in the namespace. The actual syntax and availability of objects will vary, depending on the namespace in use. Namespaces can also be used to implement polymorphism. Each ADSI provider has a set of namespaces specific to the provider, but can also use the same names as other providers. Information about the available objects can be found in documentation or through the Schema object in the namespace. More on that in a bit.

How to help yourself

The easiest way to learn about ADSI is to use the wealth of information available on MSDN (see the online resources listing). However, it's not always easy to make sense of the information provided or to figure out why something doesn't work as expected. One area that can cause some confusion is the number of terms needed to talk about ADSI.

We've already looked at the syntax of the GetObject and seen how to bind to an ADSI provider. Each provider may require different syntax (called the binding string), just to keep you on your toes.

Each object in ADSI implements a number of interfaces, which are collections of the Properties/Events/Methods (PEMs) we're all accustomed to manipulating. Here's a little bit of trivia: All COM objects must, at minimum, implement the IUnknown interface. By checking the interfaces available for each object in your namespace, and looking at the PEMs they support, you can get a clue about what you can do with each object. If you understand a normal class hierarchy where subclasses inherit the PEMs of their parents, you can understand interfaces. They're not quite the same thing, but are very similar in that they can extend the functionality of objects. This understanding can take you a long way toward deciphering the MSDN docs and making the most of ADSI.

Examples

Enough with the theory! Let's see how we can use ADSI to actually do something. Keep in mind that you must have rights to perform these tasks, and the examples will not work if you're logged in as a Guest. Logging in as the Administrator is probably your best bet for trying out these examples.

Listing all users on a machine (VFP Code):

oComputer=GetObject("WinNT://OMNIBOOK_DEV,computer")

FOR EACH oMember in oComputer
   IF oMember.Class = "User"
      ? oMember.Class + ": " + oMember.name
   ENDIF
ENDFOR

You'll notice that we're using the Class property to filter the result to users only. There is actually a Filter property that could be used by passing an array with the items you want (in this case, "User"). Unfortunately, the Filter property expects a Safe Array, which VFP can't create. Since we have access to the Class property, we have a simple workaround.

In VB, here's what that code might look like (note that the syntax differences between VFP and VB are minor):

Dim oComputer

Set oComputer = GetObject("WinNT://OMNIBOOK_DEV,computer")
oComputer.Filter = Array("User")

For Each oMember In oComputer
	Print oMember.Class & ": " & oMember.Name
Next

You can try the code without any filtering to see the other categories you can manipulate with ADSI.

I previously mentioned using the Schema object to determine the PEMs of the various namespaces, and grabbing the schema is pretty easy. (NOTE: This example works only in VFP 7 - it appears there is enhanced support for Safe Arrays, although I was still unable to pass an array to the Filter property.) You can also try this under VB with just a few syntax changes.

Creating a simple VB wrapper

Here's an example of how you might create an ActiveX wrapper in Visual Basic. This example shows one possible way to get around VFP's inability to pass an array to the ADSI filter. To start, fire up VB. When you're first prompted to create a new project, select ActiveX DLL and click Open. You will be prompted with a window where you can add the following code:

Dim aSafe As Variant

' Set our variant variable to an empty array.
' A variant is an undefined variable. VFP 
' variables are considered variant because 
' their type isn't set until we assign a value
' to them.

Public Sub Clear()
   aSafe = Array()
End Sub

' Create a public method called Add. We accept 
' an item called vNewItem, passed by value, as
' a variant variable.

Public Sub Add(ByVal vNewItem As Variant)
  ' Get the array size, resize it then save the
  ' passed in value to the array.
  nASize = Ubound(aSafe)
  nASize = nASize + 1
  ReDim Preserve aSafe(nASize)
  aSafe(nASize) = vNewItem
End Sub

' Let/Gets are similar to VFP's assign & access methods.

Public Property Let aArray(ByVal vNewArray As Variant)
  If IsArray(vNewArray) Then
    aSafe = vNewArray
  End If
End Property

Public Property Get aArray() As Variant
  aArray = aSafe
End Property

' Do the real work, we pass in an object 
' reference to the ADSI object. Then we set its 
' filter property to our local array.

Public Sub SetFilter(ByRef oADSI As Object)
  If IsObject(oADSI) Then
    OADSI.Filter = aSafe
  End If
End Sub

' Like VFP's Init() method. This just resets

Listing a schema (VFP 7 only):


oComputer = GetObject("WinNT://OMNIBOOK_DEV,computer")
oSchema = GetObject(oComputer.Schema)

? "Computer Schema object: " + oComputer.Schema

IF oSchema.Container
	? "This is a container object with the following children:"

	FOR EACH ObjectType IN oSchema.Containment
		? "Child: " + ObjectType
	ENDFOR




' our array.




Private Sub Class_Initialize()
  Me.Clear
End Sub

After typing in the code, select the root node of the treeview in the Project Window on the right side of the screen. It should be labeled "Project 1 (Project 1)". If you look at the properties window, you should see a property called "Name" that is set to Project1. Let's change that to: vbutils. This is the name of the ActiveX control that will be created. Now, click on the Class node of the project tree. Set the name property here to SafeArray and save this project by clicking on File, then Save Project. Set the class name when prompted to SafeArray and the Project name to VbUtils.

We're finally ready to compile and test our control. Click on File, then on Make VbUtils.dll.

Let's exit out of VB and start up VFP to test our new control. From the command window in VFP, try the following code (remember to replace the OMNIBOOK_DEV machine name with your own).

oComputer = GetObject("WinNT://OMNIBOOK_DEV,computer")

* Create an instance of our new ActiveX control
* Notice how we reference the control: 
* Project Name.Class Name
* this is exactly the same way it works in VFP
* when creating COM objects.

oSafe = CreateObject("VBUtils.SafeArray")

* Add the item "User" to our filter
oSafe.Add("User")

* Set the ADSI filter property
oSafe.SetFilter(oComputer)

* Walk through each member object 

FOR EACH oMember IN oComputer
   ?oMember.Class
ENDFOR

If everything is correct, you should see a listing of only the "User" classes when you run the above code. Wasn't that easy?

ENDIF

?
? "This object has the following properties:"

FOR EACH ObjectProperty IN oSchema.MandatoryProperties
	? ObjectProperty + " (mandatory)"
ENDFOR

FOR EACH ObjectProperty IN oSchema.OptionalProperties
	? ObjectProperty + " (optional)"
ENDFOR

After running this, you should see that the computer object contains a number of children objects, such as User. There are a few optional properties, but no mandatory ones.

Creating a new user (VFP):

This example not only creates a new user, but assigns that user to the Administrators group. One thing to watch out for is that this example requires a "guest" account which is not disabled.

oDomain = GetObject("WinNT://WORKGROUP/OMNIBOOK_DEV") 
oUser = oDomain.Create("user", "testuser") 
oUser.SetPassword("MyPassword") 
oUser.FullName = "ADSI Test" 
oUser.Description = "ADSI Test Account"
oUser.SetInfo() 

oGroup = GetObject("WinNT://OMNIBOOK_DEV/Administrators,group") 
oGroup.Add(oUser.AdsPath) 

In this code, I specifically referenced the domain, although I could just as easily have referenced the system name. Why did I do that? To make a point, of course. When creating or modifying a user on a network with both primary domain controllers (PDC) and backup domain controllers (BDC), the application must reference the PDC. Attempting to SetInfo() on the BDC will return an error. (Thanks to Randy Pearson for finding this one.)

Editing a user (VFP):

To edit a user, just pass the account name in your GetObject call. In this example, we're using the guest account. Pretty simple, isn't it?

oUser = GetObject("WinNT://OMNIBOOK_DEV/guest")
oUser.FullName = "Guest Account"
oUser.Description = "New description"
oUser.SetInfo()

Hey, this doesn't work!

While using ADSI, you're bound to come across examples that work under VB or ASP, but fail under VFP. You may have to use the Get() and Put() methods instead of accessing properties directly. How did I know that there are even Get() and Put() methods? Check out the interface that every ADSI object inherits from (IADs) and you'll have your answer.

If you just can't get something to work under VFP, try running it under VB. If it works, you may have found a compatibility issue with VFP (such as with Safe Arrays). In this case, you might want to write an ActiveX "wrapper" in VB (see sidebar for an example).

Another aspect of ADSI that's easy to overlook is that it runs under the NT security system. That means it's bound by the same security limitations of any normal user account. If it didn't, you would have a huge security issue. If your code looks good but still fails to run, make sure that you or your application actually have the rights to do what you're attempting. This is especially a concern when running scripting code through ASP on the web. Check those rights!

Here's another big problem that's easy to miss: the provider names are case-sensitive. "WinNT:" is not the same as "winnt:"! Case-sensitive coding errors can be very difficult to track down, so keep that in mind as you code.

Summary

ADSI gives developers and system administrators powerful control over many different directory services. We've covered just a few of the ways it can but used, but I hope you see how ADSI's interface simplifies tasks that once required a C++ or Win32 interface guru. Please do yourself a favor and check out the Online Resources, where you'll find many more code samples. If you come up with any cool uses of this technology, drop me a note. I'd love to hear your ideas.