October 10th, 2007

Hey, Scripting Guy! How Can I Search Multiple OUs in Active Directory?

Hey, Scripting Guy! Question

Hey, Scripting Guy! I have an HTA that searches Active Directory for user accounts and then displays those user names. We have 10 OUs in Active Directory, but I only want to retrieve the users from three of those OUs. How do I do that?

— DP

SpacerHey, Scripting Guy! AnswerScript Center

Hey, DP. Here’s a little secret for you: it’s very easy to tell when a writer is getting lazy and isn’t even trying any more. How can you tell that? Because they will inevitably start off an article by using some tired old cliché, like the old “I’ve got some good news for you and some bad news for you” routine. Just something everyone should watch out for; you know how the Scripting Guys feel about lazy writers who don’t even try anymore. Now, as for your question about searching Active Directory, well, we’ve got some good news for you and some bad news for you. Let’s go over the bad news first. You want to search Active Directory for user accounts, but you only want to retrieve the accounts found in 3 of your 10 OUs. Well, guess what? You can’t do that. (At least not very easily.) That’s because, oddly enough, Active Directory does not have a property that specifies the OU where a user account (or any other object) resides. In theory, we could write a complicated wildcard-based query that might do the trick, but we can’t do anything as simple and straightforward as this hypothetical Where clause:

Where OU=’Finance’ OR OU=’Research’ OR OU=”Shipping’

It just can’t be done.

Anyway, sorry about the bad news, DP. See you all tomorrow!

Oh, right. As the Scripting Editor has … helpfully … pointed out, any time you do the old good news-bad news routine it’s customary to tell people the good news as well. (Unfortunately, that’s also a lot more work, which is why we didn’t tell you the good news in the first place. Besides, we’d always heard that no news was good news.) But, as you know, the Scripting Editor’s word is law. Therefore, DP, the good news is that we can still retrieve users from a specified set of OUs; we just have to perform multiple searches in order to do so. (But don’t worry; Active Directory searches – particularly searches targeted towards a single OU – are pretty darn fast.) And there’s even better news: we’ll also show you an HTA that displays all your OUs in a list box, lets you select as many (or as few) of those OUs as you wish, and then retrieves information about the user accounts found in each of the selected OUs.

We agree: that’s also the best news that we’ve heard all week.

But, then again, we work at Microsoft, so we don’t have much of a chance to hear really good news. (Scripting Guys advice: Don’t ever say the letters MTPS in front of the Scripting Editor. Trust us.)

At any rate, here’s the code:

<SCRIPT Language=”VBScript”>

Const ADS_SCOPE_SUBTREE = 2 Const ADS_SCOPE_ONELEVEL = 1

Set objConnection = CreateObject(“ADODB.Connection”) Set objCommand = CreateObject(“ADODB.Command”) objConnection.Provider = “ADsDSOObject” objConnection.Open “Active Directory Provider” Set objCommand.ActiveConnection = objConnection

objCommand.Properties(“Page Size”) = 1000

Sub Window_onLoad objCommand.Properties(“Searchscope”) = ADS_SCOPE_SUBTREE objCommand.CommandText = _ “SELECT Name, ADsPath FROM ‘LDAP://dc=fabrikam,dc=com’ WHERE ” & _ “objectCategory=’organizationalUnit’ ORDER By Name” Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst

strHTML = “<select size = ’20’ name=’OUList’ style=’width:300px’>”

Do Until objRecordSet.EOF Set objOption = Document.createElement(“OPTION”) objOption.Text = objRecordSet.Fields(“Name”).Value objOption.Value = objRecordSet.Fields(“ADsPath”).Value MyOUs.Add(objOption) objRecordSet.MoveNext Loop

End Sub

Sub SearchForUsers For i = 0 to (MyOUs.Options.Length – 1) If (MyOUs.Options(i).Selected) Then strSearchOU = MyOUs.Options(i).Value objCommand.Properties(“Searchscope”) = ADS_SCOPE_ONELEVEL

objCommand.CommandText = _ “SELECT Name FROM ‘” & strSearchOU & “‘ WHERE objectCategory=’user'” Set objRecordSet = objCommand.Execute

If objRecordSet.RecordCount > 0 Then objRecordSet.MoveFirst Do Until objRecordSet.EOF strNames = strNames & objRecordSet.Fields(“Name”).Value & “<BR>” objRecordSet.MoveNext Loop End If End If Next UserList.InnerHTML = strNames End Sub

</SCRIPT>

<body> <select size=”10″ name=”MyOUs” style=”width:400″ multiple></select><p> <input type=”button” value=”Get Users” onClick=”SearchForUsers”><p> <div id=”UserList”></div> </body>

You’re right: this is a big, long chunk of code, isn’t it? But don’t worry; we’ll explain how it all works.

Note. Well, except for the parts where we don’t explain how it all works. For example, we won’t talk much about the basic fundamentals of searching Active Directory; for that, we recommend you take a peek at our two-part Tales From the Script series Dude, Where’s My Printer? And we won’t spend a lot of time discussing the process by which we create a dynamic list box for displaying OUs; after all, we already have a column that discusses the process for creating dynamic list boxes. And – well, you get the idea.

Let’s start by looking at the HTML objects we’re using in this HTA:

<select size=”10″ name=”MyOUs” style=”width:400″ multiple></select><p>
<input type=”button” value=”Get Users” onClick=”SearchForUsers”><p>
<div id=”UserList”></div>

As you can see, our HTA consists of three objects:

A multi-select list box named MyOUs. How do we know that this is a multi-select list box? Because we added the multiple parameter. What is a multi-select list box? That’s simply a list box that allows you to make multiple selections: you can click on as many items as you wish, and our subroutine (which we’ll talk about in a minute or two) will run against each of the selected items.

A button with the label Get Users. When this button is clicked, a subroutine named SearchForUsers is triggered; as the name implies, this is the subroutine that searches for users in the selected OUs. The basic premise here is that you select as many (or as few) OUs from the list box as needed, then click the Get Users button; in turn, the SearchForUsers subroutine will grab user account information from the selected OUs and then – well, good question: what will the subroutine do with that information? (Hint: The answer can be found in the next bullet.)

A <DIV> with the ID UserList. A <DIV> is nothing more than a named area of an HTA, an area whose contents can be manipulated programmatically. As you can see, right now our <DIV> doesn’t actually have any content. But that’s only temporary; the SearchForUsers subroutine will eventually write the retrieved user names to this portion of the HTA.

Got all that? Good; now it’s time to talk scripting.

Note. Ah, good question: is talking scripting good news or bad news? Guess we’ll all find out in a minute, won’t we?

To begin with, you might have noticed that we started off our HTA with the <SCRIPT> tag, then immediately added in the following lines of code, code that we did not include in a subroutine:

Const ADS_SCOPE_SUBTREE = 2
Const ADS_SCOPE_ONELEVEL = 1

Set objConnection = CreateObject(“ADODB.Connection”) Set objCommand = CreateObject(“ADODB.Command”) objConnection.Provider = “ADsDSOObject” objConnection.Open “Active Directory Provider” Set objCommand.ActiveConnection = objConnection

objCommand.Properties(“Page Size”) = 1000

Why didn’t we put these commands in a subroutine? Well, what we’re doing here is defining a pair of constants (ADS_SCOPE_SUBTREE and ADS_SCOPE_ONELEVEL); creating a pair of objects (ADODB.Connection and ADODB.Command); and then configuring some of the properties for these objects. We want these constants and objects to be global; that is, we want them to be available to all the subroutines in our HTA. The easiest way to do that is to place the code inside the <SCRIPT> tag, but not inside a subroutine. So that’s exactly what we did.

Note. Incidentally, these commands are explained in more detail in our Tales From the Script series.

Now we have our two subroutines – Window_OnLoad and SearchForUsers – to deal with. To begin with, we should note that we didn’t pick the name Window_OnLoad simply because we liked the way that sounded. (Although it does tend to just roll off the tongue, doesn’t it?) Instead, we chose this name because any subroutine with the name Window_OnLoad automatically runs any time an HTA is opened or refreshed. By putting our code for retrieving a list of OU names in a subroutine named Window_OnLoad, we guarantee that each time we start the HTA our list box will be populated with all the OUs in Active Directory.

As for how we go about retrieving OU names, well, again, we won’t discuss that in any detail today. Instead, we’ll simply focus on the SQL query we employed:

objCommand.CommandText = _
    “SELECT Name, ADsPath FROM ‘LDAP://dc=fabrikam,dc=com’ WHERE ” & _
        “objectCategory=’organizationalUnit’ ORDER By Name”

As you can see, we’re requesting that the script return the values of two attributes (Name and ADsPath) for all the objects in the domain (fabrikam.com) that have an objectCategory equal to organizationalUnit. (In addition, we’re also sorting these objects alphabetically by Name.) How do we know that we’ll get back all the OUs in the domain? Because, right before we execute the query, we set the Searchscope to ADS_SCOPE_SUBTREE:

objCommand.Properties(“Searchscope”) = ADS_SCOPE_SUBTREE

When we do that, our script searches not only the specified container (fabrikam.com) but also any and all sub-containers (e.g., any and all OUs and sub-OUs). Because we started the search in our Active Directory root, the script will methodically search the entire directory service, and bring back each of the OUs it finds lurking in there.

Once we get back a recordset containing all the OU names we then use this block of code to add each OU to the list box:

Do Until objRecordSet.EOF  
    Set objOption = Document.createElement(“OPTION”)
    objOption.Text = objRecordSet.Fields(“Name”).Value
    objOption.Value = objRecordSet.Fields(“ADsPath”).Value
    MyOUs.Add(objOption)
    objRecordSet.MoveNext
Loop

Without going into too much detail, for each record (OU) in our recordset we create an instance of the Option object. We assign the OU Name to the Text property (the label displayed in the list box) and the ADsPath to the Value property (the actual information to be used by our SearchForUsers subroutine). Once that’s done we call the Add method to add this new option to the list box.

Isn’t that a lot of work just to get our HTA up and running? Well, sure; the bad news is that there is a little bit of prep work required to get things going here. But if there’s bad news that can only mean one thing: there must be some good news as well. And the good news is that we’re now ready to start searching for user accounts.

Before we can do that, of course, we need to figure out which OUs we’re supposed to search. The easiest way to do that? By executing this block of code:

For i = 0 to (MyOUs.Options.Length – 1)
    If (MyOUs.Options(i).Selected) Then
        strSearchOU = MyOUs.Options(i).Value

What we’ve done here is set up a For Next loop that runs from 0 to the Length of the list box, minus 1. Why 0? Because, in a VBScript collection, the first item always has the index number 0. And why the number of items minus 1? Well, suppose we selected three items in the list box. The first item has the index number 0; the second item has the index number 1; and the third item has the index number 2. The last item in a collection always has an index number 1 less than the total number of items in that array.

Crazy, but that’s how it works.

For each item in the list box we check to see if the Selected property is True; if it is, that means that this OU was selected and needs to be searched. To carry out that search we grab the Value property (which just happens to be the ADsPath for the OU) and stash that information in a variable named strSearchOU.

Now we have two other chores to carry out. First, we need to set the SearchScope to ADS_SCOPE_ONELEVEL:

objCommand.Properties(“Searchscope”) = ADS_SCOPE_ONELEVEL

Why? Because this constant tells the script to search only the specified OU; it will not search any child OUs. That’s how we manage to limit a search to a single OU.

Second, we need to define our SQL query:

objCommand.CommandText = _
    “SELECT Name FROM ‘” & strSearchOU & “‘ WHERE objectCategory=’user'”

In this query, we’re simply searching the selected OU, retrieving the Name of every object that has an objectCategroy equal to user. Needless to say, that’s going to bring back a recordset containing information about each user in the OU. And then for each user in that recordset we add the user name and a <BR> tag (the HTML equivalent of the carriage return-linefeed character):

strNames = strNames & objRecordSet.Fields(“Name”).Value & “<BR>”

What if we selected more than one OU in the list box? That’s fine; after we finish with the first OU we loop around and continue looking for more selected items. If we find one, we repeat this process using that OU.

When we’ve finished with all the items in the list box we then use this line of code to write the user names to our <DIV>:

UserList.InnerHTML = strNames

That should do it, DP. The bad news is that this isn’t a very fancy HTA; you’ll undoubtedly need to do some additional work to ensure that the returned information is displayed in a coherent fashion. (Incidentally, here’s one cool way to do that.) The good news is that even though this is a bit of a workaround, at least it enables you to retrieve user accounts from three different OUs, even if it does require three separate searches.

Oh, and the even better news? That’s right: we’re finally done for today! See you all tomorrow.

Author

0 comments

Discussion are closed.