In Part 1 and Part 2, we covered how to connect to objects, read and write attributes, call methods, and create users and groups. Now let's look at two more items. Enumeration and search.
By enumeration, we mean get a list of objects within an OU, or more precisely, get a collection of objects by accessing the enumerator of a container. The enumerator of a container is a collection of the objects within the container, be they users, groups, sub-OU's, etc. The following script returns the names of the objects within a container.
use Win32::OLE;
$ou=Win32::OLE->GetObject("LDAP://ou=HR,dc=myDomain,dc=net"); foreach $obj (in $ou){ print "$obj->{name}\n"; }
Here we've introduced a few new things. The Perl keyword: foreach, which loops through each item in a list or collection, and the Win32::OLE keyword in, which returns the enumerator from our OU object. The name attribute returns a string that includes the CN= for a user or group, and an OU= for an OU.
If you had various object types in your OU (users, groups, and OU's), you'll see that you may not want to return all types of objects. You may be interested in just users or just groups. There are a few ways to filter out the items that you don't want. The first way is to use a filter. A filter is a property of a container, that specifies the object class or classes that you want returned in the enumeration. The filter is an array of class names, so we create an array, then set the filter attribute of the OU to that array. For example, if we want to see only the sub-OU's, we setup our filter and specify the organizationalUnit class.
use Win32::OLE;
$ou=Win32::OLE->GetObject("LDAP://ou=HR,dc=myDomain,dc=net");
@filter=("organizationalUnit"); $ou->{filter}=\@filter;
foreach $obj (in $ou){ print "$obj->{name}\n"; }
Or if you only wanted to see users and groups, you could filter this way:
use Win32::OLE;
$ou=Win32::OLE->GetObject("LDAP://ou=HR,dc=myDomain,dc=net");
@filter=("user","group"); $ou->{filter}=\@filter;
foreach $obj (in $ou){ print "$obj->{name}\n"; }
Alternatively, you could return all object classes, then look at the object class within the loop, like this:
use Win32::OLE;
$ou=Win32::OLE->GetObject("LDAP://ou=HR,dc=myDomain,dc=net");
foreach $obj (in $ou){ if($obj->{objectClass}[1] eq "organizationalUnit"){ print "$obj->{name}\n"; } }
You might notice this is slower than using a filter, since it's returning a larger set of objects, and you're retrieving an attribute (objectClass) from each one. Notice the [1] after the {objectClasss}. That's needed because {objectClass} returns an array of classes. If you're familiar with object-oriented programming, you may be familiar with class inheritance. One object class inherits part of its design (some if its attributes and methods) from a parent class. In the case of Active Directory, all objects inherit their design from the base object class, called top. So the {objectClass} array contains all of the object classes that the object inherited from it's native class and all inherited classes. {objectClass}[0] will always be top. Not very useful. But if we look at {objectClass}[1], we see the classes we're looking for, like group, user, or organizationalUnit. Enough on that.
You can envision a script that enumerates the root of the domain, and for each OU, enumerates that OU, and so on down the tree. Easily done, but I'm not so sure that's ever what you really want to do. Do you really want to ask your domain controller to spit out information from every object in your domain? Probably not, especially if you have a large domain. That's where search comes in.
I've already covered searches in many of my earlier posts but let's go over it again.
Active Directory is searchable just like a database (well, it is a database). And we use Microsoft ActiveX Data Objects (ADO) as the interface to do the search. Searches don't return actual objects, like enumeration does, instead it returns only the attributes we specify. For example, the following script returns the name of each OU in the domain:
use Win32::OLE;
$base="<LDAP://dc=myDomain,dc=net>";
$connection = Win32::OLE->new("ADODB.Connection"); $connection->{Provider} = "ADsDSOObject"; $connection->Open("ADSI Provider"); $command=Win32::OLE->new("ADODB.Command"); $command->{ActiveConnection}=$connection; $command->{Properties}->{'Page Size'}=1000; $rs = Win32::OLE->new("ADODB.RecordSet");
$command->{CommandText}="$base;(objectCategory=OrganizationalUnit);name;subtree";
$rs=$command->Execute; until ($rs->EOF){ $name=$rs->Fields(0)->{Value}; print "$name\n"; $rs->MoveNext; }
Looks a little complicated, but let's look at each line. First, we define a searchbase ($base) that points to the root of our domain. Then we create an ADODB connection object and tell the connection to use the ADsDSOobject provider, which is the database provider for AD. We then call the Open method on the connection and give it a name (which is pretty much useless, but required).
Next we create an ADODB command object, link the command to the connection, set the page size property, and the command text property.
The page size property defines how you want large result sets handled. By default, 1000 records will be returned and no more. If you want more than 1000 records returned, you must specify a page size. You should experiment with page size to improve the performance of searches that return large result sets. The optimum page size will be determined mostly by the load and available memory on your domain controllers. Setting the page size too large will use up a lot of memory on your domain controllers, while setting the size too small will slow down your searches.
The command text is the actual search criteria. In this case we're asking ADO to search the location specified by $base, search for objects whose objectCategory is OrganizationalUnit, return the name, and search the entire subtree (within all sub-OU's).
The format of the command text can be one of two dialects. LDAP dialect, which I used in the example above, or SQL dialect. LDAP dialect is formatted as:
"searchBase;searchFilter;attributesToRetrieve;searchScope"
searchBase, as shown above, is the ADsPath of your domain or OU, with the protocol prefix (LDAP:// or GC://) and opening and closing <> brackets.
searchFilter is an LDAP search filter, which may take some time to master. In its simplest form, it's (attribute=value), but you can combine statements using binary operators like & (and) and ¦ (or). For example, (&(objectCategory=User)(cn=Harry)) will find our friend Harry.
The attributes to retrieve can be seperated by commas, and finally the searchScope specifices how many levels deep you want to search: just the location specified by the searchBase (base), one level deep (onelevel) or the entire subtree (subtree).
As an alternative to LDAP dialect, you can use SQL dialect, as shown below:
use Win32::OLE;
$base="LDAP://dc=myDomain,dc=net";
$connection = Win32::OLE->new("ADODB.Connection"); $connection->{Provider} = "ADsDSOObject"; $connection->Open("ADSI Provider"); $command=Win32::OLE->new("ADODB.Command"); $command->{ActiveConnection}=$connection; $command->{Properties}->{'Page Size'}=1000; $rs = Win32::OLE->new("ADODB.RecordSet");
$command->{CommandText}="select name from '$base' where objectCategory = 'OrganizationalUnit'";
$rs=$command->Execute; until ($rs->EOF){ $name=$rs->Fields(0)->{Value}; print "$name\n"; $rs->MoveNext; }
I won't get into SQL dialect (I always use LDAP dialect), but you SQL programmers will get the idea.
So those are the basic mechanics of getting around in Active Directory. What remains is to get a handle on the Active Directory object classes and their attributes, and the challenges of the occasional special data type.
Please feel free to post comments and questions. I'll do my best to help out.
2 comments:
I want to thank you for doing such a nice page on ADO and ADSI search using Perl. I've looked at a couple of hundred pages and not found as much useful information in all of them as I have found in this page. Thanks
That's great to hear. Especially that you are using Perl. It's a great language and we need to keep it alive!
Post a Comment