Chapter 14 PowerShell and Active Directory

IN THIS CHAPTER

Administrators have always desired a unified method for managing Active Directory from both the command line or through automation scripts. To some extent, tools (executables) have been developed by either Microsoft or third parties to manage Active Directory via the command line. When used in conjunction with automation interfaces, such as Active Directory Services Interfaces (ADSI) and Window Script Host (WSH), these administrators have always been able to jury-rig the management task at hand. With the introduction of PowerShell, the paradigm is changing. For the first time, there is now a single interface, both command line and scripting, for managing Active Directory.

In this chapter, you explore how PowerShell can be used to manage Active Directory. In doing so, you first discover the different approaches Windows Script Host (WSH) and PowerShell take for completing Active Directory management tasks. Next, after gaining insight into how PowerShell interacts with Active Directory, you then review the two cornerstones of Active Directory management: managing objects and searching for objects. To complete this task, both topics are reviewed in detail while stepping through a series of examples. Finally, you review a script scenario that uses the information presented in this chapter.

Understanding the Interfaces

Before learning how to use PowerShell to manage Active Directory, you need to first gain a better understanding for the existing automation interfaces that are used to manage Active Directory. The first interface that you need to understand is called Active Directory Services Interfaces. ADSI is the primary programming interface for managing Active Directory. Any Active Directory management tool typically uses ADSI to interact with Active Directory. Similarly, when managing Active Directory through a script, you usually use ADSI.

To use ADSI as a component in your scripts, you need to understand several key concepts. First, ADSI consists of a series of providers: Lightweight Directory Access Protocol (LDAP), Novell Directory Services (NDS), Novell NetWare 3.x (NWCOMPAT), and Windows NT (WinNT). These providers enable external programs and scripts to manage a variety of network-based directories and data repositories, such as Active Directory, Novell NetWare 4.x NDS, and NetWare 3.x Bindery, in addition to any LDAP-compliant directory service infrastructure (LDAP V2 and up). However, additional ADSI providers can be developed to support other types of data repositories. For example, Microsoft has an Internet Information Services (IIS) ADSI provider for managing IIS.

Second, an ADSI provider implements a group of COM objects to manage network directories and data repositories. For example, an administrator can use the ADSI WinNT provider to bind to and manage Windows domain resources because it includes objects for users, computers, groups, and domains, among others. Objects made available by an ADSI provider typically reside in the target resource you want to manage. By accessing the applicable ADSI provider, a program or script can bind to an object and manage it with a set of methods and properties defined for that object.

Third, ADSI provides an abstraction layer so that you can manage objects across different directory services and data repositories. This abstraction layer, called the IADs interface, defines a set of properties and methods as the foundation for all ADSI objects. For example, an ADSI object accessed through the IADs interface has the following features:

• An object can be identified by name, class, or ADsPath.

• An object’s container can manage that object’s creation and deletion.

• An object’s schema definition can be retrieved.

• An object’s attributes can be loaded into the ADSI property cache, and changes to those attributes can be committed to the original data source.

• Object attributes loaded into the ADSI property cache can be modified.

Fourth, ADSI provides an additional interface (IADsContainer) for objects that are considered containers (such as organizational units [OUs]). When bound to a container object, this interface provides a set of common methods for creating, deleting, moving, enumerating, and managing child objects.

Fifth, ADSI maintains a client-side property cache for each ADSI object you bind to or create. Maintaining this local cache of object information improves the performance of reading from and writing to a data source because a program or script needs to access the data source less often. What’s important to understand about the property cache is that object information it contains must be committed to the original data source. If new objects or object changes aren’t committed to the original data source, those changes will not be reflected.The second interface that needs to be understood is called ActiveX Data Objects (ADO). ADO allows applications or scripts to access data from different data sources by using a series of underlying Object Linking and Embedding Database (OLE DB) providers. One of these providers is an ADSI OLE DB (ADODB) provider that enables you to use ADO and its support for Structured Query Language (SQL) or LDAP to perform rapid searches in Active Directory.

Using the ADODB provider is an efficient method for conducting searches of Active Directory. When using this provider, searches can be performed through a single operation and without any need to bind to an existing object. However, the ADODB provider allows searches to be performed only in the LDAP namespace, and the record set that is returned from the provider is read-only.

Managing Active Directory Using WSH

In Window Script Host (WSH), two interfaces manage Active Directory. The first interface consists of employing WScript Object’s GetObject method to bind to Active Directory or an object in Active Directory. Then, once bound, you can perform additional actions to either the object or its child objects. An example of this is shown in the following script snippet, which uses ADSI’s LDAP provider and an object’s LDAP ADsPath:

image

image

As shown in the previous example, the objUser variable contains the resulting object that is returned from the GetObject method. In this case, that object happens to be the user object that has the CN=Garett Kopczynski. You can also perform the same task using ADSI’s WinNT provider. An example of this is shown in the following code snippet:

image

image

The only difference from the first example is that the WinNT provider and a WinNT ADsPath are used. In either case, an object is returned that relates to the user object named CN=Garett Kopczynski. However, the resulting object from the WinNT provider is different from an object created using the LDAP provider. The cause for this difference is based on the fact that the WinNT provider supports features that are available only within Windows NT domains. Because of this limitation, the WinNT provider exposes fewer attributes than the LDAP provider, which unfortunately restricts the number of management tasks that can be completed using this provider.

Another limitation with the WinNT provider is that it supports only a flat namespace that is contrastingly different from the object hierarchy found in Active Directory. Because of this difference, the WinNT provider is unaware of objects, such as Organization Units, or certain object relationships, such as nested Global and Universal security groups. With this limitations in mind, it is generally recommend that the WinNT provider be used only for managing Windows NT 4.0 Domains or local account/group objects on Windows machines. However, the LDAP provider should be used for managing Active Directory.

NOTE

The Windows NT SAM database is not LDAP compliant. Because of this limitation, you cannot use the LDAP provider to manage Windows NT 4.0 Domains or account/group objects on Windows machines.

The second interface is to use WScript Object’s CreateObject method to create an ADODB object and conduct Active Directory searches. An example of performing an LDAP-based ADODB search is shown in the following example:

image

image

As shown in the previous example, the objConnection variable contains the resulting object that is returned from the CreateObject method. Next, an ADODB command object is created and set to the objCommand variable. Then, in the next few steps, any needed ADODB parameters about the search to be performed are defined. The most important of these parameters is the CommandText parameter, which contains the filter string that is used to perform the search. In this case, a search is being performed for a user within the companyabc.com domain who has a sAMAccountName=Tyson. Finally, the LDAP search is executed using the ADODB Execute method. If the user exists, the resulting ADO record set consists of the user’s sAMAccountName and distinguishedName.

Managing Active Directory Using PowerShell

In PowerShell, you can use two component classes within the .NET Framework’s System.DirectoryServices namespace to access, manage, and search for Active Directory objects: the DirectoryEntry class and the DirectorySearcher class. Both of these classes are explained in the next few sections.

DirectoryEntry [ADSI]

The DirectoryEntry class is used as an interface for completing several tasks around the management of directory objects. Some of these tasks include binding to objects, reading their properties, updating their attributes, creating new objects, moving objects, renaming objects, and even deleting objects. Although this class is primarily intended for managing Active Directory objects through either ADSI’s LDAP or WinNT providers, you can also use the class to manage all the other data repositories that are supported by ADSI.

To use this interface, use the New-Object cmdlet to create an instance of the DirectoryEntry class, as shown in the next example:

image

PS > $User = new-object
DirectoryServices.DirectoryEntry("LDAP://CN=Garett
Kopczynski,OU=Accounts,OU=Managed Objects,DC=companyabc,DC=com")

image

Additionally, the DirectoryEntry class is represented in PowerShell by the built-in [ADSI] type accelerator. This type accelerator is similar to the [WMI] type accelerator because you specify the object path to which you’re connecting to. However, instead of a WMI object path, the [ADSI] type accelerator’s object path must be in the form of an AdsPath path, as shown in this example:

image

PS > $User = [ADSI]"LDAP://CN=Garett Kopczynski,OU=Accounts,OU=Managed
Objects,DC=companyabc,DC=com"

image

Although the preceding example uses ADSI’s LDAP provider, you can additionally choose to use of all the other providers that are available. For example, if you want to access the same user account but with ADSI’s WinNT provider, you might use the following command:

image

PS > $User = [ADSI]"WinNT://companyabc.com/garett"

image

DirectorySearcher [ADSISearcher]

Much like ADODB, the DirectorySearcher class is similarly used for performing read-only LDAP searches against an LDAP-based directory. However, instead of using ADODB, the DirectorySearcher class makes use of a component library called ADO.NET. This library is a base class library that is included with the .NET Framework and can be used to access and modify data stored in relational database systems. Although similar in nature to ADODB, ADO.NET tends to be considered the next evolution of the older interface by making many needed improvements in the areas of performance and standardization (via XML).

NOTE

Read-only LDAP searches against an LDAP-based directory are two important concepts that should be noted about the DirectorySearcher class.

Like the DirectoryEntry class, you must use the New-Object cmdlet to create an instance of the DirectorySearcher class. Then, after the instance is created, you can execute a search using that instance, as shown in the following example:

image

PS > $Searcher = new-object DirectoryServices.DirectorySearcher
PS > $Searcher.Filter = "(samAccountName=garett)"
PS > $User = $Searcher.FindOne().GetDirectoryEntry()

image

In the preceding example, a $Searcher object is first created using the New-Object cmdlet. Next, an LDAP filter statement is constructed to find the user account with a samAccountName=garett. Finally, the search is executed using the FindOne method, and the results are dumped into the $User variable.

Additionally, the CTP build of PowerShell 2.0 introduces the [ADSISearcher] type accelerator that is a representation of the DirectorySearcher class. The following example shows how this type accelerator is used:

image

PS > $Searcher = [ADSISearcher] [ADSI]""
PS > $Searcher.Filter = "(samAccountName=garett)"
PS > $User = $Searcher.FindOne()

image

For the most part, the steps shown are similar to the steps reviewed in the DirectorySearcher example. The only notable difference in the previous example is that the [ADSISearcher] type accelerator is used instead of the New-Object cmdlet to create an instance of the DirectorySearcher class.

DirectoryServices.ActiveDirectory

In addition to the DirectoryEntry and DirectorySearcher classes and their associated type accelerators, a third .NET Framework interface can be used to access and mange Active Directory. DirectoryServices.ActiveDirectory is a .NET Framework namespace that is not based on ADSI, and the components in it are purely dedicated for the management of Active Directory’s configuration and related infrastructure. The following code snippet is an example of using this namespace:

image

PS > $Forest =
[System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
PS > $Forest | select *role* | fl

image

In the previous example, the static method GetCurrentForest of the DirectoryServices.ActiveDirectory.Forest class is used to create an object that represents the forest, which is then dumped into the $Forest variable. Next, using that object, the Select-Object and Format-List cmdlets are used to create a report that contains the current Schema Master and Domain Naming Master FSMO role owners.

PowerShell’s Active Directory Imperfections

Although having access to the .NET Framework to manage Active Directory can be powerful, the way PowerShell interacts with these interfaces can prove frustrating and unintuitive at times. As a result, an administrator has to understand several nuances to effectively use PowerShell to manage Active Directory. An explanation of these issues follows:

• PowerShell’s versions of objects based on the DirectoryEntry class tend to lack the actual methods and properties that are useful to manage directory objects. As a result, you often have to use the PSBase method to expose the underlying base object.

• Even when the base object is exposed, not all of the relevant methods are exposed, which means that you will find yourself having to refer to MSDN documentation.

• The order of properties and methods for creating new objects can at times be different from what is required by ADSI.

The following steps demonstrate how to use PowerShell and the DirectoryEntry class to create a user account. When reviewing these steps, you should gain a better understanding about the difficulties that might be encountered when using PowerShell’s interpretation of the DirectoryEntry class to manage Active Directory objects.

1. First, use the following command to bind an OU:

image

PS > $OU = new-object
DirectoryServices.DirectoryEntry("LDAP://OU=Accounts,DC=companyabc,
DC=com")

image

2. Next, you need to use the Create method to create the user and the SetInfo method to write the new object into Active Directory. Use the following commands to complete this task:

image

PS > $User = $OU.Create("User", "CN=mkopczynski")
PS > $User.SetInfo()

image

3. Now, use Active Directory Users and Computers to review the changes that have been made.

4. Notice that the user still needs to be enabled and the account name attributes are not correctly defined. Next, execute the next couple of commands to enable the account and to correct the name attributes:

image

PS > $User.sAMAccountName = "mkopczynski"
PS > $User.sn = "Maiko"
PS > $User.givenName = "Kopczynski"
PS > $User.Description = "Kansai-ben is hard!"
PS > $User.userPrincipalName = "mkopczynski"
PS > $User.SetInfo()
PS > $User.SetPassword('H@rdPassword1')
PS > $User.PSBase.InvokeSet("AccountDisabled", $False)
PS > $User.SetInfo()

image

At any point in the preceding example, if you were to examine the objects in the $OU or $User variables using the Get-Member cmdlet, you would find missing properties and methods. In other words, when PowerShell constructs the PSObject it doesn’t correctly expose the underlying object’s members. To get at these members and use them in a command or script, you need to look at the base (PSBase) object. The following command is an example that shows how to list all of the properties in the PSBase object:

image

PS > $User.PSBase.Properties

image

Another problem with PowerShell’s interaction with the DirectoryEntry class is shown in the command that is issued to enable the account. In this command, the InvokeSet method from the PSBase object is being called to set the AccountDisabled attribute. The InvokeSet method is being used because of a bug in how PowerShell handles certain object properties. Because of this bug, there is inconsistent behavior when attempting to use PowerShell’s "." notation or using ADSI’s Put and PutEx methods to define object properties. If this type of behavior, or any other type of inconsistency, is encountered, it is recommended that you attempt to use the properties or methods from the based object (PSBase). If you find these problems too cumbersome, you can also turn to other third-party products or community projects to manage Active Directory through PowerShell.

NOTE

An example of a third-party product is Quest’s AD cmdlets, which can be used to manage and search for objects in Active Directory. These cmdlets can make a great addition to your toolbox, are free, and can easily be downloaded from Quest’s Web site.

PowerShell’s CTP2 Improvements

In early May 2008, the PowerShell team released an updated CTP version of PowerShell 2. As part of this release, an improvement was made in how the [ADSI] type adapter handled an object’s members. With this improvement, you can now directly access many of the “missing” methods and properties that were previously accessible only by using the PSBase method. For example:

image

PS > $User.PSBase.Properties
--becomes--
PS > $User.Properties

image

Managing Objects

Managing objects using PowerShell consists of binding to an object and then using its members to either read or modify its properties. In concept, this sounds simple. However, because of bugs or shortcomings in how PowerShell interacts with the DirectoryEntry class, managing objects can sometimes be frustrating. Over the next several sections, the goal is to reduce this frustration. To reach this goal, you read through a detailed explanation about how to bind to objects, and then you see examples about how to manage objects using PowerShell.

Binding

The DirectoryEntry class’s primary function in life is to facilitate the act of “binding” to objects so that they can then be managed. When you bind to an object, a relationship is created between a programmatic object and the specified object in Active Directory (or other supported data repositories). Then, by using this relationship, the programmatic object can be used to perform any number of tasks against the bound object from Active Directory. These tasks include, but are not limited to, reading properties, modifying properties, and moving or deleting objects. An example of this relationship being established is shown in the following code snippet:

image

PS > $OU = new-object
DirectoryServices.DirectoryEntry("LDAP://OU=Accounts,DC=companyabc,DC=com")

image

In the preceding example, the DirectoryEntry class is used to bind to the Accounts OU. The object that results from this action is then dumped into the $OU variable.

NOTE

Active Directory supports both the LDAP and WinNT ADSI providers for accessing and managing objects.

Binding Strings

Binding to an object in Active Directory requires a binding string. A “binding string” is a text-based string that uniquely identifies an object in Active Directory. More commonly referred to as AdsPath, a binding string typically consists of a unique name or path to an object appended to an ADSI provider moniker. For example:

image

"LDAP://OU=Accounts,DC=companyabc,DC=com"

image

The previous example’s binding string is composed of an LDAP provider moniker followed by the Distinguished Name for the Accounts OU. An LDAP provider moniker supports the following format for an AdsPath:

image

"LDAP://<Host>:<Port>/<ObjectName>"

image

HostSpecifies the host name, IP address, or domain name to use. [Optional]

PortSpecifies the port to use for the connection. If this parameter is not defined, port 389 is used. [Optional]

ObjectNameSpecifies the Distinguished Name or GUID of an object.

In addition to the LDAP provider moniker, Active Directory objects can also be accessed using both the WinNT and GC provider monikers. The GC provider moniker instructs ADSI to connect to a global catalog server in Active Directory using the LDAP provider. When using the GC provider moniker, the format for the AdsPath is exactly the same as the LDAP provider moniker. For example:

image

"GC://OU=Accounts,DC=companyabc,DC=com"

image

When using the WinNT provider moniker, the format for the AdsPath can be any of the following:

image

"WinNT://<domain name>"
"WinNT://<domain name>/<server>"
"WinNT://<domain name>/<path>"
"WinNT://<domain name>/<object name>"
"WinNT://<domain name>/<object name>,<object class>"
"WinNT://<server>"
"WinNT://<server>/<object name>"
"WinNT://<server>/<object name>,<object class>"

image

Special Characters

In LDAP, several special characters shouldn’t be used in an object’s name. These characters follow:

image

,/#+<>;"=

image

For more information about these special characters and their usages in LDAP, please see RFC 1779 (A String Representation of Distinguished). If a special character is used for an object’s name, the backslash “” character needs to be placed in front of the character in an ADsPath. For example:

image

"LDAP://CN=Kopczynski, Maiko,OU=Accounts,DC=companyabc,DC=com"

image

Failure to escape the "," character results in an error being raised when a connection to the object is attempted.

Authentication

By default, both the [ADSI] type accelerator and the DirectoryEntry class connect to a domain using the user context that created the object instance. If you want to authenticate using a different user or to a different domain that requires authentication, addition authentication information needs to be provided.

Unfortunately, the [ADSI] type accelerator does not provide an authentication interface, which means that the DirectoryEntry class must be used instead. To authenticate to a domain when using the DirectoryEntry class, your command should be structured as follows:

image

PS > $User = new-object
DirectoryServices.DirectoryEntry("LDAP://CN=Garett
Kopczynski,OU=Accounts,OU=Managed Objects,DC=companyabc,DC=com",
"<username>", "<password>")

image

In the previous example, the <username> and <password> strings would be replaced with username and password that would be used to authenticate to the domain.

Working with Objects

When using PowerShell to work with objects in Active Directory, several different interfaces are available for reading and modifying their properties. You can use PowerShell’s "." notation to access an objects members. For example:

image

PS > $User.description

image

Alternatively, you can use the methods that are supported by ADSI, as shown in the following example:

image

PS > $User.Get("description")

image

A complete list of these methods is provided in Table 14.1.

Table 14.1. ADSI Methods

Image

Finally, if issues are encountered with the other interfaces, you can also use the methods provided by the underlying base object. For example:

image

PS > $User.psbase.InvokeSet("description", "This will always work!")

image

A list of the more notable base object methods is provided in Table 14.2.

Table 14.2. Base Object Methods

Image

Examples

The next few examples are designed to better explain how the listed methods in the prior section are used to manage Active Directory objects. The goal of these examples is to illustrate some of the finer points and shortcomings of trying to use PowerShell to manage Active Directory.

Creating Objects

The following example shows how to create a new user account:

image

PS > $OU = new-object DirectoryServices.DirectoryEntry("LDAP://
OU=Managed Objects,DC=companyabc,DC=com")
PS > $User = $OU.Create("User", "CN=mkopczynski")
PS > $User.SetInfo()
PS > $User.sAMAccountName = "mkopczynski"
PS > $User.sn = "Maiko"
PS > $User.givenName = "Kopczynski"
PS > $User.Description = "Kansai-ben is hard!"
PS > $User.userPrincipalName = "mkopczynski"
PS > $User.SetInfo()
PS > $User.SetPassword('H@rdPassword1')
PS > $User.PSBase.InvokeSet("AccountDisabled", $False)
PS > $User.SetInfo()

image

The following example shows the steps involved with creating a new group:

image

PS > $OU = [ADSI]"LDAP://OU=Managed Objects,DC=companyabc,DC=com"
PS > $Group = $OU.Create("Group", "CN=MyFirstGroup")
PS > $Group.SetInfo()
PS > $Group.sAMAccountName = "MyFirstGroup"
PS > $Group.PSBase.InvokeSet("GroupType", "2147483656")
PS > $Group.SetInfo()

image

Modifying Objects

The following example shows how to modify a user object:

image

PS > $User = [ADSI]"LDAP://CN=Andrew Abbate,OU=Managed
Objects,DC=companyabc,DC=com"
PS > $User.PSBase.InvokeSet("Info", "Likes to play golf with the Pope.")
PS > $User.PutEx(3, "url", @("www.readmyblog.com"))
PS > $User.psbase.CommitChanges()

image

The following example shows how to add a group member:

image

PS > $Group = [ADSI]"LDAP://CN=MyFirstCEO,OU=Groups,OU=Managed
Objects,DC=companyabc,DC=com"
PS > $Group.Add("LDAP://CN=Irobin Peoples,OU=Accounts,OU=Managed
Objects,DC=companyabc,DC=com")
PS > $Group.psbase.CommitChanges()

image

Moving Objects

The following example shows how to move an object to a different container:

image

PS > $User = [ADSI]"LDAP://CN=Black Knight,OU=Accounts,OU=Managed
Objects,DC=companyabc,DC=com"
PS > $User.psbase.moveto("LDAP://OU=Managed
Objects,DC=companyabc,DC=com")

image

Disabling Accounts

The following example shows how to disable a user account:

image

PS > $User = [ADSI]"LDAP://CN=mkopczynski,OU=Managed
Objects,DC=companyabc,DC=com"
PS > $User.PSBase.InvokeSet("AccountDisabled", $True)
PS > $User.psbase.CommitChanges()

image

Deleting Objects

The following example shows how to delete an object from a container:

image

PS > $OU = [ADSI]"LDAP://OU=Managed Objects,DC=companyabc,DC=com"
PS > $OU.Delete("User", "CN=mkopczynski")

image

Searching for Objects

By default, an object that is based on the DirectorySearcher class does not need any of its properties defined to perform a search against Active Directory. If you want, you can use the default object that is created by either the New-Object cmdlet or the [ADSISearcher] type accelerator. An example of just using the default object is shown in the following sequence of commands:

image

PS > $Searcher = new-object DirectoryServices.DirectorySearcher
PS > $Searcher.FindAll()

image

Although the previous example returns all of the objects in the currently authenticated domain, the results are not useful. Making the search results useful means that you need to narrow the search based on a set of predetermined requirements. However, before you can do that, you need to gain a better understanding about how the DirectorySearcher class is used. To complete this task, in the next few sections, you review several important DirectorySearcher properties and how they play a role in performing Active Directory searches.

SearchRoot

The SearchRoot property is used to define the node where a search is to begin from. This is unlike ADODB, which takes an AdsPath as the base where a search is to begin. The value for the SearchRoot property must be a DirectoryEntry object or Null (which is translated as the currently logged onto domain). An example of using the SearchRoot property is shown here:

image

PS > $OU = [ADSI]"LDAP://OU=Managed Objects,DC=companyabc,DC=com"
PS > $Searcher = new-object System.DirectoryServices.DirectorySearcher
PS > $Searcher.SearchRoot = $OU

image

Filter

The filter property is used to define the query that is used during a search. The value for this property must be a string that is in an LDAP search filter syntax format as defined in RFC 2254. If the value is not defined, the default value "(objectClass=*)" is used, which retrieves all objects.

An LDAP search filter is a collection of conditions that a search is based on. When certain conditions are met within the collection, the related Active Directory objects are then included with the other objects that are returned from the search. Each condition in the collection is in the form of an expression that is enclosed in parentheses. The result from each expression returns a Boolean result and is constructed using relational operators, such as: =, ~=, >=, and <=.

NOTE

RFC 2254 states that the < and > operators are not supported.

Each expression is in the form of an <attribute> and <value>, which is then separated by any of the noted operators. For example:

image

"(cn=Daisuke)"

image

Logical Operators

To combine several conditions together into a into a single filter statement, you can use three additional logical operators. These operators and their usage follow:

&or the "And" operator, means that all conditions operated by "&" must be met for a record to be included in the returned set of objects.

|or the "Or" operator, means that any condition operated by "|" must be met for a record to be included in the returned set of objects.

!or the "Not" operator, means that the condition operated by "!" must return false for a record to be included in the returned set of objects.

To use these operators with a collection of conditions, nest the conditions within sets of parentheses. For example, to return the object that has both the objectClass=user and cn=Daisuke, use the following filter statement:

image

"(&(objectClass=user)(cn=Daisuke))"

image

To return objects that have an objectClass=user and cn=Daisuke or cn=Sophie, use the following filter statement:

image

"(&(objectClass=user)(|(cn=Daisuke)(cn=Sophie)))"

image

Finally, to return objects that have an objectClass=user and cn=Daisuke or cn=Sophie, but do not include the object with cn=Wen-Ai, use the following filter statement:

image

"(&(objectClass=user)(|(cn=Daisuke)(cn=Sophie))(!cn=Wen-Ai))"

image

Wildcards

By using the "*" character, you can add wildcards to conditions in an LDAP search filter. For example, you can use the following filter statement to retrieve user accounts that have a description:

image

"(&((objectCategory=person)(objectClass=user)(Description=*)))"

image

Alternatively, you can retrieve machine accounts that have DNS host names that start with sc1 but do not start with sc1-dc, as shown in the next example:

image

"(&((objectClass=computer)(dNSHostName=sc1*))(!(dNSHostName=sc1-dc*)))"

image

The only limitation to using the wildcard character is that it can’t be used with object attributes that are Distinguished Names (DN). For example, attempting to perform a wildcard search against the distinguishedName or memberOf attributes isn’t feasible.

Special Characters

Sometimes, the names of objects might have characters that are used to construct an LDAP search filter. If you encounter this scenario, use any of following escape sequences described in Table 14.3 to include those characters as literals in a filter statement:

Table 14.3. LDAP Search Filter Special Characters

Image

If the search filter contains binary data, that data needs to be represented such that each byte has the backslash "" escape character before two hexadecimal digits. For example, to retrieve the object with GUID ="659cd735f7fc4182b007b650b621d4de", use the following filter statement:

image

"(objectGUID=659cd735f7fc418207650621d4de)"

image

Other Examples

Instead of using both the objectCategory and objectClass property to search for user accounts, you can just use the sAMAccountType property, as shown here:

image

"(&((sAMAccountType=805306368)(Description=*)))"

image

To retrieve all user accounts that are disabled, use the following filter statement:

image

"(&((sAMAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.8
03:=2)))"

image

Conversely, to see all the accounts that are not disabled, use this filter statement:

image

"(&((sAMAccountType=805306368))(!(userAccountControl:1.2.840.113556.1.
4.803:=2)))"

image

To see what user accounts that are not disabled and have "Password Never Expires" set, use this filter statement:

image

"(&((sAMAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.8
03:=65536))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))"

image

To find users that are members of a particular group, use the following filter statement:

image

"(&((sAMAccountType=805306368)(memberOf=CN=All-Sales,OU=Groups,
OU=Managed Objects,DC=companyabc,DC=com)))"

image

To find universal security groups, use the next filter statement:

image

(&(objectCategory=group)((&(groupType:1.2.840.113556.1.4.803:=8)
(!(groupType:1.2.840.113556.1.4.803:=-2147483648)))))

image

To find users that have a badPwdCount greater than or equal to 1, use the following filter statement:

image

"(&((sAMAccountType=805306368)(badPwdCount >=1)))"

image

SearchScope

The SearchScope property is used to define the scope of the search to be performed. The value for this property can be one of the three following strings:

BaseLimits the search to the base object. The result for a scoped search of Base contains a maximum of one object.

OneLevelLimits the search to immediate child objects of the base object. The result for a scoped search of OneLevel excludes the base object.

SubtreeForces the search to be performed against an entire Subtree, which includes the base object and all its child objects. If no value is defined for the SearchScope property, a Subtree scoped search is performed.

The following is an example of using the SearchScope property:

image

PS > $Searcher = new-object System.DirectoryServices.DirectorySearcher
PS > $Searcher.SearchScope= "OneLevel"

image

PageSize

The PageSize property is used to define the maximum number of objects returned in a search. After the number of objects equals the PageSize in a search, the results are returned to the client. Then, if the search is not yet complete, the client restarts the search where it left off until the number of objects again equals the PageSize or the search is complete. If a value for the PageSize property is not defined, the default value of 0 is used.

The following example uses the PageSize property:

image

PS > $Searcher = new-object System.DirectoryServices.DirectorySearcher
PS > $Searcher.PageSize = 500

image

SizeLimit

The SizeLimit property is used to define the maximum number of objects that are to be returned in a search. The default value for the SizeLimit property is 0, or you can use the server-determined size limit. By default, Active Directory’s search size limit is 1000 objects, or an administrator can specify the limit by modifying the MaxPageSize attribute. If you want your search to return more records, either the limited specified in the MaxPageSize attribute or the SizeLimit property, you need to page the search by using the PageSize property.

The following is an example of using the SizeLimit property:

image

PS > $Searcher = new-object System.DirectoryServices.DirectorySearcher
PS > $Searcher.SizeLimit = 1000

image

PropertiesToLoad

When performing an ADSI-based search that uses the LDAP dialect, you must specify the name of the attributes that are to be retrieved. For example:

image

"<LDAP://DC=companyabc,DC=com>;(objectClass=*);cn,adspath,sn,givenName
;subTree"

image

In the previous example, the LDAP search filter returns all objects in the companyabc.com domain, in addition to the cn, adspath, sn, and givenName attributes and their associated values. However, when using PowerShell and the DirectorySearcher class to conduct searches, the attributes to be retrieved are specified by using the PropertiesToLoad property. The following code box shows an example:

image

PS > $Searcher = new-object System.DirectoryServices.DirectorySearcher
PS > $Searcher.PropertiesToLoad.Add("givenName")
PS > $Searcher.PropertiesToLoad.Add("sn")

image

After the shown commands are issued, the sn and givenName attributes are added into the PropertiesToLoad property. This doesn’t mean that you need to specify the properties that are to be retrieved. By default, the value for the PropertiesToLoad property is an empty StringCollection that retrieves all attributes of an object. Instead, the PropertiesToLoad property is used to reduce the amount of data retrieved by a search.

Putting It All Together

Now that you have an understanding about how the different properties of the DirectorySearcher class are used to perform searches, this last section puts all the pieces together and conducts an actual search for objects in Active Directory. To complete this task, execute the following series of commands:

image

PS > $OU = [ADSI]"LDAP://OU=Sales,OU=Managed
Objects,DC=companyabc,DC=com"
PS > $Searcher = new-object System.DirectoryServices.DirectorySearcher
PS > $Searcher.SearchRoot = $OU

PS > $Searcher.Filter = "(&((sAMAccountType=805306368)
(userAccountControl:1.2.840.113556.1.4.803:=2)))"
PS > $Searcher.SearchScope= "OneLevel"
PS > $Searcher.PageSize = 500
PS > $Searcher.PropertiesToLoad.Add("givenName")
PS > $Searcher.PropertiesToLoad.Add("sn")
PS > $Users = $Searcher.FindAll()

image

In the previous example, a search is completed that returns all disabled user accounts located directly under the Sales OU. To complete this search, a new DirectorySearcher object ($Searcher) is created. Then, the Filter, SearchScope, PageSize, PropertiesToLoad properties are defined for that object. Lastly, a search is executed using the FindAll method, and the returned objects are dumped into the $Users variable.

NOTE

When using the FindAll method, all the unmanaged resources from the SearchResultCollection are not released during garbage collection. In certain scenarios, this behavior can cause resource issues (memory leaks) while executing searches. To prevent these problems from occurring, you need to use the Dispose method to remove the SearchResultCollection object when you are done with it.

Scripting Scenario: ChangeLocalAdminPassword.ps1

The ChangeLocalAdminPassword.ps1 script was developed to address a time-consuming task for systems administrators. This task is the routine (as in scheduled) or forced (because the network was attacked) local administrator password change. Changing this password ranks as one of the biggest chores of systems management activities, and administrators often neglect this task because it’s so tedious.

For example, companyabc.com operates a Windows Server 2003 server farm of 500 servers. As part of the company’s security practices, the IT department tried to change the local administrator password routinely on all 500 servers, usually every 30 days or when a systems administrator left the company. Because of the time and effort to change the administrator password on 500 servers, the IT department tended to fall behind schedule in completing this task. Eventually, the department stopped trying to change local administrator passwords, which soon resulted in a major security incident: An external entity took advantage of the lapse in password management practices to commandeer a number of companyabc.com’s servers and demanded a ransom to return control of these systems.

This incident prompted the IT department to seek a way to change local administrator passwords quickly and en masse. The department decided to use an automation script that creates a list of servers in a specified OU, and then connects to each server and changes the local administrator password. To meet this need, the ChangeLocalAdminPassword.ps1 script was developed.

A working copy of this script can be downloaded from the www.informit.com/title/9789780768687187. Running this script requires defining one parameter: OUDN. This parameter’s argument should be set to the distinguishedName of the OU containing the servers that need to have their local administrator passwords changed. Here’s the command to run the ChangeLocalAdminPassword.ps1 script:

image

PS D:Scripts> .ChangeLocalAdminPassword.ps1 "OU=Servers,OU=Managed
Objects,DC=companyabc,DC=com"

image

Figures 14.1 and 14.2 show the ChangeLocalAdminPassword.ps1 script being executed.

Figure 14.1. The ChangeLocalAdminPassword.ps1 during execution

Image

Figure 14.2. The ChangeLocalAdminPassword.ps1 script after execution has finished

Image

The ChangeLocalAdminPassword.ps1 script performs the following sequence of actions:

1. The script dot sources the LibraryCrypto.ps1 library file, which contains a function for randomly generating passwords.

2. The script creates a new DataTable object ($ServersTable) by using the .NET System.Data.DataSet class. This DataTable object is used later in the script to store status information about machines in the specified OU.

3. In addition, the script creates an error log named ChangeLocalAdminPassword_Errors.log by using the Out-File cmdlet. This error log displays detailed error information to users.

4. The script connects to the current logon domain by using the Get-CurrentDomain function. Using the object returned from this function, the script then writes the domain’s name to the PowerShell console. If this connection fails, the script halts.

5. The script verifies that the specified OU exists in the current domain by using the Get-ADObject function. If the OU is not valid, the script halts.

6. The script uses the Set-ChoiceMesssage and New-PromptYesNo functions to ask users whether they want a randomly generated password or one they specify. For randomly generated passwords, the script uses the New-RandomPassword function from the LibraryCrypto.ps1 library file to generate a password of a specified length that’s stored as a secure string ($Password) and returned to the user for verification. For user-specified passwords, the script uses the Read-Host cmdlet with the AsSecureString property to collect the password and store it in a secure string ($Password).

7. The script uses the DirectoryEntry class to bind to the specified OU in Active Directory and then the DirectorySearcher class to create a $Searcher object. The SearchRoot property for the $Searcher object is set to the bound OU object, and an LDAP search is performed to populate the $Computers variable with all servers in the OU.

8. The script uses the System.Net.NetworkInformation.Ping class to ping each server that is in the $Servers object collection. If a server replies, a new row is added into the $ServersTable DataTable, which consists of the server’s name and its "Online" status. If a server doesn’t reply, a new row is still added into the $ServersTable DataTable; however, that server’s status is set to "Offline".

9. The script uses the System.Net.NetworkInformation.Ping class to ping each server in the $Computers object collection. If a server replies, a new row is created in the $ServersTable DataTable consisting of the server’s name and its "Online" status. If a server doesn’t reply, a new row is created in the $ServersTable DataTable with the server’s status set to "Offline".

10. The listing of servers and their status is sent to the script’s error log for future reference by using the Out-File cmdlet.

11. The script uses the System.Runtime.InteropServices.Marshal class to convert the secure string stored in the $Password variable to a regular string that can be used later in the script.

12. For each server with an "Online" status in $ServersTable, the Get-WmiObject cmdlet is used to connect to the server and return a list of user accounts. The local administrator account has a security ID (SID) ending with "-500". The script binds to this account by using the ADSI WinNT provider and changes its password to the string now stored in the $Password variable.

Here’s the LibraryCrypto.ps1 library file:

image

image

ChangeLocalAdminPassword.ps1 uses the New-RandomPassword function from the LibraryCrypto.ps1 file to generate random passwords of a specified length based on a predetermined set of allowed characters. To do this, the function uses the .NET System.Security.Cryptography.RNGCryptoServiceProvider class as a cryptographically strong random number generator.

A random number generator improves the strength of passwords, even those consisting of both characters and numbers. The New-RandomPassword function uses the random number generator to generate random characters for passwords. To do this, the function first takes the specified length of the random password and creates a System.Byte array ($Bytes) of the same length. It then defines a character array ($Chars) consisting of all possible characters that can make up the random passwords.

Next, New-RandomPassword creates a random number generator ($Crypto) by using the System.Security.Cryptography.RNGCryptoServiceProvider class. The GetNonZeroBytes method uses $Crypto to populate the $Bytes array with a cryptographically strong sequence of random nonzero values. For each byte in the $Bytes array, the function performs a modulo operation (the remainder of dividing one number by another) to determine which character from the $Chars array is added to the $Password variable. The end result is a random password returned to the caller as a secure string.

The next code snippet contains the header for the ChangeLocalAdminPassword.ps1 script. This header includes information about what the script does, when it was updated, and the script’s author. Just after the header is the script’s parameter OUDN, shown here:

image

image

Next, the script loads the Set-ChoiceMessage and New-PromptYesNo functions, as seen in the following code snippet:

image

image

In PowerShell, you’re sometimes prompted to make a choice before a command continues. For example, as you learned in Chapter 5, “Understanding PowerShell Security,” PowerShell might prompt for confirmation before running a script that isn’t signed by a trusted entity, depending on your execution policy setting, or PowerShell prompts you for confirmation before running a command when a cmdlet is used with the confirm parameter, as shown in this example:

image

PS > get-process | stop-process -confirm

Confirm
Are you sure you want to perform this action?
Performing operation "Stop-Process" on Target "~e5d141.tmp (792)".
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help
(default is "Y"):

image

With the Set-ChoiceMessage and New-PromptYesNo functions, you can build a menu of Yes or No choices to display to users in the PowerShell console. The Set-ChoiceMessage function creates a collection of choice objects and is used with the New-PromptYesNo function to generate the choice menu. To generate this menu, New-PromptYesNo uses the PromptForChoice method from the $host.UI object, which is just an implementation of the System.Management.Automation.Host.PSHostUserInterface class.

In the following code snippet, variables that will be used later in the script are defined. In addition, two library files are dot sourced into the script’s scope. The first file, LibraryGen.ps1, is a general library file that contains the script usage and Active Directory functions. The second file is the LibraryCrypto.ps1 library, which, as mentioned previously in this section, contains the New-RandomPassword function, as shown in this example:

image

image

After defining the script’s variables and dot sourcing any library files, the next step is to check if the user needs any usage help or if the required OUDN parameter was defined. This step is shown in the next code snippet:

image

image

Next, the script creates a DataTable object. This is a new concept that uses a .NET DataTable object (from the System.Data.DataTable class, part of the ADO.NET architecture):

image

image

DataTable objects are the equivalent of a database table, except the table is located in memory. Your scripts can use this table to hold data retrieved from other sources or data you specify manually.

In this script, a DataTable is used to hold status information about the servers queried from Active Directory. The script first creates a DataTable named $ServersTable by using the New-Object cmdlet and System.Data.DataTable class. When you first create a DataTable, it’s empty and lacks structure, so you must define the structure before you can store data in it. For $ServersTable’s structure, the script uses the Add method to add Name and Status columns to its Columns collection. Later in the script, the Add method is used to add rows of data to $ServersTable’s Rows collection.

In the next code snippet, the Out-File cmdlet is used to create an error log and write header information to it. Then, the Get-ScriptHeader function is used to indicate to the script operator that the automation portion of the script has started:

image

image

The next step is for the script to verify that there is a valid domain connection. To accomplish this task, the script uses the Get-CurrentDomain function. If a valid domain connection doesn’t exist, the script halts and returns the script status to the operator. If a connection does exist, the script continues execution and writes the domain name to the console. Then, the script uses the Get-ADObject function to validate if the string in the $OUDN variable is a valid distinguished name. If an object is returned from the function, the variable is valid; if no object is returned, the variable is considered invalid, and the script halts, as shown in the next code snippet:

image

image

The following code snippet contains the logic for defining the password that will be used. First, the script asks the user if a password should be generated or specified by the user. If a password is to be generated, the script asks for the password length. Then, based on the defined length, a password is generated using the New-RandomPassword function. If the user chooses to specify the password, the script uses the Read-Host cmdlet with the AsSecureString switch to collect the password from the user:

image

image

Now that the script has the password that will be used, it must next get a list of machines that will have their passwords changed. The next code snippet contains the code that accomplishes this task. In this code, you see usage of the DirectoryServices.DirectorySearcher class to perform a search for computer objects (servers) under the defined OU. Then, for each computer object that is returned from the search, the script pings the server and adds a row to the $ServersTable DataTable that contains the server’s dNSHostName and its status:

image

image

The next task is to change the passwords on all the online servers. First, the script converts the secure string in the $Password variable back to a regular string. Next, the script defines the $OnlineServers variable with all the server objects that have an online status using the DataTable Select method. The script uses WMI to connect to the server and figure out which account is the Administrator account. WMI then sets its password to the string that is in the $Password variable:

image

image

Summary

In this chapter, you explored many different aspects about how PowerShell interacts with Active Directory. Starting from its usage of the .Net Framework to understanding how to manage and search for objects, you saw firsthand what can be accomplished. In addition, you saw some of PowerShell’s rough edges when it comes to support for the DirectoryEntry class.

Given time and maturity, the Active Directory support in PowerShell should get better, and it’s even possible that improvements will make it into PowerShell 2.0 RTM. Until then, even with its shortcomings, PowerShell is a powerful tool for managing Active Directory. This power can even be further enhanced by utilizing additional cmdlets, functions, or scripts that others in the community or vendors created. Although these enhancements were not covered in this chapter, they exist, and if an administrator wants to quickly work past PowerShell’s quirkiness, we suggest using these tools.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset