Chapter 19. User Management

by Mar 26, 2012

User administration in the Active Directory was a dark spot in PowerShell Version 1. Microsoft did not ship any cmdlets to manage AD user accounts or other aspects in Active Directory. That's why the 3rd party vendor Quest stepped in and published a free PowerShell Snap-In with many useful AD cmdlets. Over the years, this extension has grown to become a de-facto standard, and many PowerShell scripts use Quest AD cmdlets. You can freely download this extension from the Quest website.

Beginning with PowerShell Version 2.0, Microsoft finally shipped their own AD management cmdlets. They are included with Server 2008 R2 and also available for download as "RSAT tools (remote server administration toolkit). The AD cmdlets are part of a module called "ActiveDirectory". This module is installed by default when you enable the Domain Controller role on a server. On a member server or client with installed RSAT tools, you have to go to control panel and enable that feature first.

This chapter is not talking about either one of these extensions. It is introducing you to the build-in low level support for ADSI methods. They are the beef that makes these two extensions work and can be called directly, as well.

Don't get me wrong: if you work a lot with the AD, it is much easier for you to get one of the mentioned AD extensions and use cmdlets for your tasks. If you (or your scripts) just need to get a user, change some attributes or determine group membership details, it can be easier to use the direct .NET framework methods shown in this chapter. They do not introduce dependencies: your script runs without the need to either install the Quest toolkit or the RSAT tools.

Topics Covered:

Connecting to a Domain

If your computer is a member of a domain, the first step in managing users is to connect to a log-on domain. You can set up a connection like this:

$domain = [ADSI]""

If your computer isn’t a member of a domain, the connection setup will fail and generate an error message:

out-lineoutput : Exception retrieving member "ClassId2e4f51ef21dd47e99d3c952918aff9cd":
"The specified domain either does not exist or could not be contacted."

If you want to manage local user accounts and groups, instead of LDAP: use the WinNT: moniker. But watch out: the text is case-sensitive here. For example, you can access the local administrator account like this:

$user = [ADSI]"WinNT://./Administrator,user"
$user | Select-Object *

We won’t go into local user accounts in any more detail in the following examples. If you must manage local users, also look at net.exe. It provides easy to use options to manage local users and groups.

Logging On Under Other User Names

[ADSI] is a shortcut to the DirectoryServices.DirectoryEntry .NET type. That’s why you could have set up the previous connection this way as well:

$domain = [DirectoryServices.DirectoryEntry]""

This is important to know when you want to log on under a different identity. The [ADSI] type accelerator always logs you on using your current identity. Only the underlying DirectoryServices.DirectoryEntry .NET type gives you the option of logging on with another identity. But why would anyone want to do something like that? Here are a few reasons:

  • External consultant: You may be visiting a company as an external consultant and have brought along your own notebook computer, which isn’t a member of the company domain. This prevents you from setting up a connection to the company domain. But if you have a valid user account along with its password at your disposal, you can use your notebook and this identity to access the company domain. Your notebook doesn’t have to be a domain member to access the domain.
  • Several domains: Your company has several domains and you want to manage one of them, but it isn’t your log-on domain. More likely than not, you’ll have to log on to the new domain with an identity known to it.

Logging onto a domain that isn’t your own with another identity works like this:

$domain = new-object DirectoryServices.DirectoryEntry("LDAP://","domainuser", `
"secret") $ scriptinternals $domain.distinguishedName DC=scriptinternals,DC=technet

Two things are important for ADSI paths: first, their names are case-sensitive. That’s why the two following approaches are wrong:

$domain = [ADSI]"ldap://"              # Wrong!
$useraccount = [ADSI]"Winnt://./Administrator,user"          # Wrong!

Second, surprisingly enough, ADSI paths use a normal slash. A backslash like the one commonly used in the file system would generate error messages:

$domain = [ADSI]"LDAP:\"                          # Wrong!
$useraccount = [ADSI]"WinNT:\.Administrator,user"          # Wrong!

If you don’t want to put log-on data in plain text in your code, use Get-Credential. Since the password has to be given when logging on in plain text, and Get-Credential returns the password in encrypted form, an intermediate step is required in which it is converted into plain text:

$cred = Get-Credential
$pwd = [Runtime.InteropServices.Marshal]::PtrToStringAuto( 
Runtime.InteropServices.Marshal]::SecureStringToBSTR( $cred.Password )) $domain = new-object DirectoryServices.DirectoryEntry("LDAP://",$cred.UserName, $pwd) $ scriptinternals

Log-on errors are initially invisible. PowerShell reports errors only when you try to connect with a domain. This procedure is known as “binding.” Calling the $domain.Name property won’t cause any errors because when the connection fails, there isn’t even any property called Name in the object in $domain.

So, how can you find out whether a connection was successful or not? Just invoke the Bind() method, which does the binding. Bind() always throws an exception and Trap can capture this error.

The code called by Bind() must be in its own scriptblock, which means it must be enclosed in brackets. If an error occurs in the block, PowerShell will cut off the block and execute the Trap code, where the error will be stored in a variable. This is created using script: so that the rest of the script can use the variable. Then If verifies whether an error occurred. A connection error always exists if the exception thrown by Bind() has the -2147352570 error code. In this event, If outputs the text of the error message and stops further instructions from running by using Break.

$cred = Get-Credential
$pwd = [Runtime.InteropServices.Marshal]::PtrToStringAuto( 
Runtime.InteropServices.Marshal]::SecureStringToBSTR( $cred.Password )) $domain = new-object DirectoryServices.DirectoryEntry("LDAP://",$cred.UserName, $pwd) trap { $script:err = $_ continue } &{ $domain.Bind($true) $script:err = $null } if ($err.Exception.ErrorCode -ne -2147352570) { Write-Host -Fore Red $err.Exception.Message break } else { Write-Host -Fore Green "Connection established." } Logon failure: unknown user name or bad password.

By the way, the error code -2147352570 means that although the connection was established, Bind() didn’t find an object to which it could bind itself. That’s OK because you didn’t specify any particular object in your LDAP path when the connection was being set up..

Accessing a Container

Domains have a hierarchical structure like the file system directory structure. Containers inside the domain are either pre-defined directories or subsequently created organizational units. If you want to access a container, specify the LDAP path to the container. For example, if you want to access the pre-defined directory Users, you could access like this:

$ldap = "/CN=Users,DC=scriptinternals,DC=technet"
$cred = Get-Credential
$pwd = [Runtime.InteropServices.Marshal]::PtrToStringAuto( 
Runtime.InteropServices.Marshal]::SecureStringToBSTR( $cred.Password )) $users = new-object DirectoryServices.DirectoryEntry("LDAP://$ldap",$cred.UserName, $pwd) $users distinguishedName ----------------- {CN=Users,DC=scriptinternals,DC=technet}

The fact that you are logged on as a domain member naturally simplifies the procedure considerably because now you need neither the IP address of the domain controller nor log-on data. The LDAP name of the domain is also returned to you by the domain itself in the distinguishedName property. All you have to do is specify the container that you want to visit:

$ldap = "CN=Users"
$domain = [ADSI]""
$dn = $domain.distinguishedName
$users = [ADSI]"LDAP://$ldap,$dn"

While in the LDAP language pre-defined containers use names including CN=, specify OU= for organizational units. So, when you log on as a user to connect to the sales OU, which is located in the company OU, you should type:

$ldap = "OU=sales, OU=company"
$domain = [ADSI]""
$dn = $domain.distinguishedName
$users = [ADSI]"LDAP://$ldap,$dn"

Listing Container Contents

At some point, you’d like to know who or what the container contains to which you have set up a connection. The approach here is somewhat less intuitive because now you need the PSBase object. PowerShell wraps Active Directory objects and adds new properties and methods while removing others. Unfortunately, , PowerShell also in the process gets rid of the necessary means to get to the contents of a container. PSBase returns the original (raw) object just like PowerShell received it before conversion, and this object knows the Children property:

$ldap = "CN=Users"
$domain = [ADSI]""
$dn = $domain.distinguishedName
$users = [ADSI]"LDAP://$ldap,$dn"


Accessing Individual Users or Groups

There are various ways to access individual users or groups. For example, you can filter the contents of a container. You can also specifically select individual items from a container or access them directly through their LDAP path. And you can search for items across directories.

Using Filters and the Pipeline

Children gets back fully structured objects that, as shown in Chapter 5, you can process further in the PowerShell pipeline. Among other things, if you want to list only users, not groups, you could query the sAMAccountType property and use it as a filter criterion:

$ldap = "CN=Users"
$domain = [ADSI]""
$dn = $domain.distinguishedName
$users = [ADSI]"LDAP://$ldap,$dn"
$users.PSBase.Children | Where-Object { $_.sAMAccountType -eq 805306368 }

Another approach makes use of the class that you can always find in the objectClass property.

$users.PSBase.Children | Select-Object -first 1 | 
ForEach-Object { $_.sAMAccountName + $_.objectClass } admin top person organizationalPerson user

As it happens, the objectClass property contains an array with all the classes from which the object is derived. The listing process proceeds from the general to the specific so you can find only those elements that are derived from the user class:

$users.PSBase.Children | Where-Object { $_.objectClass -contains "user" }

Directly Accessing Elements

If you know the ADSI path to a particular object, you don’t have to resort to a circuitous approach but can access it directly through the pipeline filter. You can find the path of an object in the distinguishedName property:

$users.PSBase.Children | Format-Table sAMAccountName, distinguishedName -wrap
sAMAccountName                               distinguishedName
--------------                               -----------------
{admin}                                      {CN=admin,CN=Users,DC=scriptinternals,DC=technet}
{Administrator}                              {CN=Administrator,CN=Users,DC=scriptinternals,DC=technet}
{All}                                        {CN=All,CN=Users,DC=scriptinternals,DC=technet}
{ASPNET}                                     {CN=ASPNET,CN=Users,DC=scriptinternals,DC=technet}
{Belle}                                      {CN=Belle,CN=Users,DC=scriptinternals,DC=technet}
{consultation2}                              {CN=consultation2,CN=Users,DC=scriptinternals,DC=technet}
{consultation3}                              {CN=consultation3,CN=Users,DC=scriptinternals,DC=technet}

For example, if you want to access the Guest account directly, specify its distinguishedName. If you’re a domain member, you don’t have to go to the trouble of using the distinguishedName of the domain:

$ldap = "CN=Guest,CN=Users"
$domain = [ADSI]""
$dn = $domain.distinguishedName
$guest = [ADSI]"LDAP://$ldap,$dn"
$guest | Format-List *
objectClass            : {top, person, organizationalPerson, user}
cn                     : {Guest}
description            : {Predefined account for guest access to the computer or domain)
distinguishedName      : {CN=Guest,CN=Users,DC=scriptinternals,DC=technet}
instanceType           : {4}
whenCreated            : {12.11.2005 12:31:31 PM}
whenChanged            : {06.27.2006 09:59:59 AM}
uSNCreated             : {System.__ComObject}
memberOf               : {CN=Guests,CN=Builtin,DC=scriptinternals,DC=technet}
uSNChanged             : {System.__ComObject}
name                   : {Guest}
objectGUID             : {240 255 168 180 1 206 85 73 179 24 192 164 100 28 221 74}
userAccountControl     : {66080}
badPwdCount            : {0}
codePage               : {0}
countryCode            : {0}
badPasswordTime        : {System.__ComObject}
lastLogoff             : {System.__ComObject}
lastLogon              : {System.__ComObject}
logonHours             : {255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255
pwdLastSet             : {System.__ComObject}
primaryGroupID         : {514}
objectSid              : {1 5 0 0 0 0 0 5 21 0 0 0 184 88 34 189 250 183 7 172 165 75 78 29 245 1 0 0}
accountExpires         : {System.__ComObject}
logonCount             : {0}
sAMAccountName         : {Guest}
sAMAccountType         : {805306368}
objectCategory         : {CN=Person,CN=Schema,CN=Configuration,DC=scriptinternals,DC=technet}
isCriticalSystemObject : {True}
nTSecurityDescriptor   : {System.__ComObject}

Using the asterisk as wildcard character, Format-List makes all the properties of an ADSI object visible so that you can easily see which information is contained in it and under which names.

Obtaining Elements from a Container

You already know what to use to read out all the elements in a container: PSBase.Children. However, by using PSBase.Find() you can also retrieve individual elements from a container:

$domain = [ADSI]""
$users = $domain.psbase.Children.Find("CN=Users")
$useraccount = $users.psbase.Children.Find("CN=Administrator")
Predefined account for managing the computer or domain.

Searching for Elements

You’ve had to know exactly where in the hierarchy of domain a particular element is stored to access it. In larger domains, it can be really difficult to relocate a particular user account or group. That’s why a domain can be accessed and searched like a database.

Once you have logged on to a domain that you want to search, you need only the following few lines to find all of the user accounts that match the user name in $UserName. Wildcard characters are allowed:

$UserName = "*mini*"
$searcher = new-object DirectoryServices.DirectorySearcher([ADSI]"")
$searcher.filter = "(&(objectClass=user)(sAMAccountName= $UserName))"

If you haven’t logged onto the domain that you want to search, get the domain object through the log-on:

$domain = new-object DirectoryServices.DirectoryEntry("LDAP://","domainuser","secret")
$UserName = "*mini*"
$searcher = new-object DirectoryServices.DirectorySearcher($domain)
$searcher.filter = "(&(objectClass=user)(sAMAccountName= $UserName))"
$searcher.findall() | Format-Table -wrap

The results of the search are all the objects that contain the string “mini” in their names, no matter where they’re located in the domain:

Path                                                   Properties
----                                                   ----------
LDAP://,CN=Users,DC=scripti {samaccounttype, lastlogon, objectsid, 

nternals,DC=technet whencreated...}

The crucial part takes place in the search filter, which looks a bit strange in this example:

$searcher.filter = "(&(objectClass=user)(sAMAccountName= $UserName))"

The filter merely compares certain properties of elements according to certain requirements. It checks accordingly whether the term user turns up in the objectClass property and whether the sAMAccountName property matches the specified user name. Both criteria are combined by the “&” character, so they both have to be met. This would enable you to assemble a convenient search function.

The search function Get-LDAPUser searches the current log-on domain by default. If you want to log on to another domain, note the appropriate lines in the function and specify your log-on data.

function Get-LDAPUser([string]$UserName, [string]$Start) 
  # Use current logon domain:
  $domain = [ADSI]""
  # OR: log on to another domain:
  #   $domain = new-object DirectoryServices.DirectoryEntry("LDAP://","domainuser",
# "secret")
if ($start -ne "") { $startelement = $domain.psbase.Children.Find($start) } else { $startelement = $domain } $searcher = new-object DirectoryServices.DirectorySearcher($startelement) $searcher.filter = "(&(objectClass=user)(sAMAccountName=$UserName))" $Searcher.CacheResults = $true $Searcher.SearchScope = "Subtree" $Searcher.PageSize = 1000 $searcher.findall() }

Get-LDAPUser can be used very flexibly and locates user accounts everywhere inside the domain. Just specify the name you’re looking for or a part of it:

# Find all users who have an "e" in their names:
Get-LDAPUser *e*

# Find only users with "e" in their names that are in the "main office" OU or come under it.
Get-LDAPUser *e* OU=main office,OU=company

Get-LDAPUser gets the found user objects right back. You can subsequently process them in the PowerShell pipeline—just like the elements that you previously got directly from children. How does Get-LDAPUser manage to search only the part of the domain you want it to? The following snippet of code is the reason:

  if ($start -ne "") 
    $startelement = $domain.psbase.Children.Find($start)
    $startelement = $domain

First, we checked whether the user specified the $start second parameter. If yes, Find() is used to access the specified container in the domain container (of the topmost level) and this is defined as the starting point for the search. If $start is missing, the starting point is the topmost level of the domain, meaning that every location is searched.

The function also specifies some options that are defined by the user:

  $Searcher.CacheResults = $true
  $Searcher.SearchScope = "Subtree"
  $Searcher.PageSize = 1000

SearchScope determines whether all child directories should also be searched recursively beginning from the starting point, or whether the search should be limited to the start directory. PageSize specifies in which “chunk” the results of the domain are to be retrieved. If you reduce the PageSize, your script may respond more freely, but will also require more network traffic. If you request more, the respective “chunk” will still include only 1,000 data records.

You could now freely extend the example function by extending or modifying the search filter. Here are some useful examples:

Search Filter Description
Find only user accounts, not computer accounts
Find only user accounts (much quicker, but harder to read)
Find user accounts with a particular name
Find user with dial-in permission
Find user who has to change password at next logon
Find all computer accounts having no description
Find all user accounts having no description
Find all elements created after March 18, 2005
Find all users whose account never expires (OR condition, where only one condition must be met)
Find all disabled user accounts (bitmask logical AND)
Find all users whose password never expires
Find all users whose password expires (logical NOT using "!")
Finding all distribution groups
Finding all computer accounts that are not domain controllers

Table 19.1: Examples of LDAP queries

Accessing Elements Using GUID

Elements in a domain are subject to change. The only thing that is really constant is the so-called GUID of an account. A GUID is assigned just one single time, namely when the object is created, after which it always remains the same. You can find out the GUID of an element by accessing the account. For example, use the practical Get-LDAPUser function above:

$searchuser = Get-LDAPUser "Guest"
$useraccount = $searchuser.GetDirectoryEntry()

Because the results returned by the search include no “genuine” user objects, but only reduced SearchResult objects, you must first use GetDirectoryEntry() to get the real user object. This step is only necessary if you want to process search results. You can find the GUID of an account in PSBase.NativeGUID.

In the future, you can access precisely this account via its GUID. Then you won’t have to care whether the location, the name, or some other property of the user accounts changes. The GUID will always remain constant:

$acccount = [ADSI]"LDAP://<GUID=f0ffa8b401ce5549b318c0a4641cdd4a>"

Specify the GUID when you log on if you want to log on to the domain:

$guid = "<GUID=f0ffa8b401ce5549b318c0a4641cdd4a>"
$acccount = new-object DirectoryServices.DirectoryEntry("LDAP://$guid","domainuser", `
"secret") distinguishedName ----------------- {CN=Guest,CN=Users,DC=scriptinternals,DC=technet}

Reading and Modifying Properties

In the last section, you learned how to access individual elements inside a domain: either directly through the ADSI path, the GUID, searching through directory contents, or launching a search across domains.

The elements you get this way are full-fledged objects. You use the methods and properties of these elements to control them. Basically, everything applies that you read about in Chapter 6. In the case of ADSI, there are some additional special features:

  • Twin objects: Every ADSI object actually exists twice: first, as an object PowerShell synthesizes and then as a raw ADSI object. You can access the underlying raw object via the PSBase property of the processed object. The processed object contains all Active Directory attributes, including possible schema extensions. The underlying base object contains the .NET properties and methods you need for general management. You already saw how to access these two objects when you used Children to list the contents of a container.
  • Phantom objects: Search results of a cross-domain search look like original objects only at first sight. In reality, these are reduced SearchResult objects. You can get the real ADSI object by using the GetDirectoryEntry() method. You just saw how that happens in the section on GUIDs.
  • Properties: All the changes you made to ADSI properties won’t come into effect until you invoke the SetInfo() method.

In the following examples, we will use the Get-LDAPUser function described above to access user accounts, but you can also get at user accounts with one of the other described approaches.

Just What Properties Are There?

There are theoretical and a practical approaches to establishing which properties any ADSI object contains.

Practical Approach: Look

The practical approach is the simplest one: if you output the object to the console, PowerShell will convert all the properties it contains into text so that you not only see the properties, but also right away which values are assigned to the properties. In the following example, the user object is the result of an ADSI search, to be precise, of the above-mentioned Get-LDAPUser function:

$useraccount = Get-LDAPUser Guest
$useraccount | Format-List *
Path       : LDAP://,CN=Users,DC=scriptinternals,DC=technet
Properties : {samaccounttype, lastlogon, objectsid, whencreated...}

The result is meager but, as you know by now, search queries only return a reduced SearchResult object. You get the real user object from it by calling GetDirectoryEntry(). Then you’ll get more information:

$useraccount = $useraccount.GetDirectoryEntry()
$useraccount | Format-List *
objectClass            : {top, person, organizationalPerson, user}
cn                     : {Guest}
description            : {Predefined account for guest access to the computer or domain)
distinguishedName      : {CN=Guest,CN=Users,DC=scriptinternals,DC=technet}
instanceType           : {4}
whenCreated            : {12.12.2005 12:31:31 PM}
whenChanged            : {06.27.2006 09:59:59 AM}
uSNCreated             : {System.__ComObject}
memberOf               : {CN=Guests,CN=Builtin,DC=scriptinternals,DC=technet}
uSNChanged             : {System.__ComObject}
name                   : {Guest}
objectGUID             : {240 255 168 180 1 206 85 73 179 24 192 164 100 28 221 74}
userAccountControl     : {66080}
badPwdCount            : {0}
codePage               : {0}
countryCode            : {0}
badPasswordTime        : {System.__ComObject}
lastLogoff             : {System.__ComObject}
lastLogon              : {System.__ComObject}
logonHours             : {255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255
pwdLastSet             : {System.__ComObject}
primaryGroupID         : {514}
objectSid              : {1 5 0 0 0 0 0 5 21 0 0 0 184 88 34 189 250 183 7 172 165 75 78 29 245 1 0 0}
accountExpires         : {System.__ComObject}
logonCount             : {0}
sAMAccountName         : {Guest}
sAMAccountType         : {805306368}
objectCategory         : {CN=Person,CN=Schema,CN=Configuration,DC=scriptinternals,DC=technet}
isCriticalSystemObject : {True}
nTSecurityDescriptor   : {System.__ComObject}

In addition, further properties are available in the underlying base object:

$useraccount.PSBase | Format-List *
AuthenticationType : Secure
Children           : {}
Guid               : b4a8fff0-ce01-4955-b318-c0a4641cdd4a
ObjectSecurity     : System.DirectoryServices.ActiveDirectorySecurity
Name               : CN=Guest
NativeGuid         : f0ffa8b401ce5549b318c0a4641cdd4a
NativeObject       : {}
Parent             : System.DirectoryServices.DirectoryEntry
Password           :
Path               : LDAP://,CN=Users,DC=scriptinternals,DC=technet
Properties         : {objectClass, cn, description, distinguishedName...}
SchemaClassName    : user
SchemaEntry        : System.DirectoryServices.DirectoryEntry
UsePropertyCache   : True
Username           : scriptinternalsAdministrator
Options            : System.DirectoryServices.DirectoryEntryConfiguration
Site               :
Container          :

The difference between these two objects: the object that was returned first represents the respective user. The underlying base object is responsible for the ADSI object itself and, for example, reports where it is stored inside a domain or what is its unique GUID. The UserName property, among others, does not state whom the user account represents (which in this case is Guest), but who called it (Administrator).

Theoretical Approach: Much More Thorough

The practical approach we just saw is quick and returns a lot of information, but it is also incomplete. PowerShell shows only those properties in the output that actually do include a value right then (even if it is an empty value). In reality, many more properties are available so the tool you need to list them is Get-Member:

$useraccount | Get-Member -memberType *Property
Name                   MemberType Definition
----                   ---------- ----------
accountExpires         Property   System.DirectoryServices.PropertyValueCollection accountExpires {get;set;}
badPasswordTime        Property   System.DirectoryServices.PropertyValueCollection badPasswordTime {get;set;}
badPwdCount            Property   System.DirectoryServices.PropertyValueCollection badPwdCount {get;set;}
cn                     Property   System.DirectoryServices.PropertyValueCollection cn {get;set;}
codePage               Property   System.DirectoryServices.PropertyValueCollection codePage {get;set;}
countryCode            Property   System.DirectoryServices.PropertyValueCollection countryCode {get;set;}
description            Property   System.DirectoryServices.PropertyValueCollection description {get;set;}
distinguishedName      Property   System.DirectoryServices.PropertyValueCollection distinguishedName {get;...
instanceType           Property   System.DirectoryServices.PropertyValueCollection instanceType {get;set;}
isCriticalSystemObject Property   System.DirectoryServices.PropertyValueCollection isCriticalSystemObject ...
lastLogoff             Property   System.DirectoryServices.PropertyValueCollection lastLogoff {get;set;}
lastLogon              Property   System.DirectoryServices.PropertyValueCollection lastLogon {get;set;}
logonCount             Property   System.DirectoryServices.PropertyValueCollection logonCount {get;set;}
logonHours             Property   System.DirectoryServices.PropertyValueCollection logonHours {get;set;}
memberOf               Property   System.DirectoryServices.PropertyValueCollection memberOf {get;set;}
name                   Property   System.DirectoryServices.PropertyValueCollection name {get;set;}
nTSecurityDescriptor   Property   System.DirectoryServices.PropertyValueCollection nTSecurityDescriptor {g...
objectCategory         Property   System.DirectoryServices.PropertyValueCollection objectCategory {get;set;}
objectClass            Property   System.DirectoryServices.PropertyValueCollection objectClass {get;set;}
objectGUID             Property   System.DirectoryServices.PropertyValueCollection objectGUID {get;set;}
objectSid              Property   System.DirectoryServices.PropertyValueCollection objectSid {get;set;}
primaryGroupID         Property   System.DirectoryServices.PropertyValueCollection primaryGroupID {get;set;}
pwdLastSet             Property   System.DirectoryServices.PropertyValueCollection pwdLastSet {get;set;}
sAMAccountName         Property   System.DirectoryServices.PropertyValueCollection sAMAccountName {get;set;}
sAMAccountType         Property   System.DirectoryServices.PropertyValueCollection sAMAccountType {get;set;}
userAccountControl     Property   System.DirectoryServices.PropertyValueCollection userAccountControl {get...
uSNChanged             Property   System.DirectoryServices.PropertyValueCollection uSNChanged {get;set;}
uSNCreated             Property   System.DirectoryServices.PropertyValueCollection uSNCreated {get;set;}
whenChanged            Property   System.DirectoryServices.PropertyValueCollection whenChanged {get;set;}
whenCreated            Property   System.DirectoryServices.PropertyValueCollection whenCreated {get;set;}

In this list, you will also learn whether properties are only readable or if they can also be modified. Modifiable properties are designated by {get;set;} and read-only by {get;}. If you change a property, the modification won’t come into effect until you subsequently call SetInfo().

$useraccount.Description = guest account

Moreover, Get-Member can supply information about the underlying PSBase object:

$useraccount.PSBase | Get-Member -MemberType *Property
   TypeName: System.Management.Automation.PSMemberSet

Name               MemberType Definition
----               ---------- ----------
AuthenticationType Property   System.DirectoryServices.AuthenticationTypes AuthenticationType {get;set;}
Children           Property   System.DirectoryServices.DirectoryEntries Children {get;}
Container          Property   System.ComponentModel.IContainer Container {get;}
Guid               Property   System.Guid Guid {get;}
Name               Property   System.String Name {get;}
NativeGuid         Property   System.String NativeGuid {get;}
NativeObject       Property   System.Object NativeObject {get;}
ObjectSecurity     Property   System.DirectoryServices.ActiveDirectorySecurity ObjectSecurity {get;set;}
Options            Property   System.DirectoryServices.DirectoryEntryConfiguration Options {get;}
Parent             Property   System.DirectoryServices.DirectoryEntry Parent {get;}
Password           Property   System.String Password {set;}
Path               Property   System.String Path {get;set;}
Properties         Property   System.DirectoryServices.PropertyCollection Properties {get;}
SchemaClassName    Property   System.String SchemaClassName {get;}
SchemaEntry        Property   System.DirectoryServices.DirectoryEntry SchemaEntry {get;}
Site               Property   System.ComponentModel.ISite Site {get;set;}
UsePropertyCache   Property   System.Boolean UsePropertyCache {get;set;}
Username           Property   System.String Username {get;set;}

Reading Properties

The convention is that object properties are read using a dot, just like all other objects (see Chapter 6). So, if you want to find out what is in the Description property of the $useraccount object, formulate:

Predefined account for guest access

But there are also two other options and they look like this:


At first glance, both seem to work identically. However, differences become evident when you query another property: AccountDisabled.

Exception calling "Get" with 1 Argument(s):"The directory property cannot be found in the cache.” 
At line:1 Char:14
+ $useraccount.Get( <<<< "AccountDisabled")

The first variant returns no information at all, the second an error message, and only the third the right result. What happened here?

The object in $useraccount is an object processed by PowerShell. All attributes (directory properties) become visible in this object as properties. However, ADSI objects can contain additional properties, and among these is AccountDisabled. PowerShell doesn’t take these additional properties into consideration. The use of a dot categorically suppresses all errors as only Get() reports the problem: nothing was found for this element in the LDAP directory under the name AccountDisabled.

In fact, AccountDisabled is located in another interface of the element as only the underlying PSBase object, with its InvokeGet() method, does everything correctly and returns the contents of this property.

As long as you want to work on properties that are displayed when you use Format-List * to output the object to the console, you won’t have any difficulty using a dot or Get(). For all other properties, you’ll have to use PSBase.InvokeGet().Use GetEx() iIf you want to have the contents of a property returned as an array.

Modifying Properties

In a rudimentary case, you can modify properties like any other object: use a dot to assign a new value to the property. Don’t forget afterwards to call SetInfo() so that the modification is saved. That’s a special feature of ADSI. For example, the following line adds a standard description for all users in the user directory if there isn’t already one:

$ldap = "CN=Users"
$domain = [ADSI]""
$dn = $domain.distinguishedName
$users = [ADSI]"LDAP://$ldap,$dn"
$users.PSBase.Children | Where-Object { $_.sAMAccountType -eq 805306368 } |
Where-Object { $_.Description.toString() -eq "" } |
ForEach-Object { $_.Description = "Standard description" $_.SetInfo() $_.sAMAccountName + " was changed." }

In fact, there are also a total of three approaches to modifying a property. That will soon become very important as the three ways behave differently in some respects:

$searchuser = Get-LDAPUser Guest
$useraccount = $searchuser.GetDirectoryEntry()

# Method 1:
$useraccount.Description = "A new description"

# Method 2:
$useraccount.Put("Description", "Another new description")

# Method 3:
$useraccount.PSBase.InvokeSet("Description", "A third description")

As long as you change the normal directory attributes of an object, all three methods will work in the same way. Difficulties arise when you modify properties that have special functions. For example among these is the AccountDisabled property, which determines whether an account is disabled or not. The Guest account is normally disabled:


The result is “nothing” because this property is—as you already know from the last section—not one of the directory attributes that PowerShell manages in this object. That’s not good because something very peculiar will occur in PowerShell if you now try to set this property to another value:

$useraccount.AccountDisabled = $false
Exception calling "SetInfo" with 0 Argument(s):  "The specified directory service attribute
or value already exists. (Exception from HRESULT: 0x8007200A)"
At line:1 Char:18 + $useraccount.SetInfo( <<<< ) $useraccount.AccountDisabled False

PowerShell has summarily input to the object a new property called AccountDisabled. If you try to pass this object to the domain, it will resist: the AccountDisabled property added by PowerShell does not match the AccountDisabled domain property. This problem always occurs when you want to set a property of an ADSI object that hadn’t previously been specified.

To eliminate the problem, you have to first return the object to its original state so you basically remove the property that PowerShell added behind your back. You can do that by using GetInfo() to reload the object from the domain. This shows that GetInfo() is the opposite number of SetInfo():


Once PowerShell has added an “illegal” property to the object, all further attempts will fail to store this object in the domain by using SetInfo(). You must call GetInfo() or create the object again:

Finally, use the third above-mentioned variant to set the property, namely not via the normal object processed by PowerShell, but via its underlying raw version:

$useraccount.psbase.InvokeSet("AccountDisabled", $false)

Now the modification works. The lesson: the only method that can reliably and flawlessly modify properties is InvokeSet() from the underlying PSBase object. The other two methods that modify the object processed by PowerShell will only work properly with the properties that the object does display when you output it to the console.

Deleting Properties

If you want to completely delete a property, you don’t have to set its contents to 0 or empty text. If you delete a property, it will be completely removed. PutEx() can delete properties and also supports properties that store arrays. PutEx() requires three arguments. The first specifies what PutEx() is supposed to do and corresponds to the values listed in Table 19.2. . The second argument is the property name that is supposed to be modified. Finally, the third argument is the value that you assign to the property or want to remove from it.

Numerical Value Meaning
1 Delete property value (property remains intact)
2 Replace property value completely
3 Add information to a property
4 Delete parts of a property

Table 19.2: PutEx() operations

To completely remove the Description property, use PutEx() with these parameters:

$useraccount.PutEx(1, "Description", 0)

Then, the Description property will be gone completely when you call all the properties of the object:

$useraccount | Format-List *
objectClass            : {top, person, organizationalPerson, user}
cn                     : {Guest}
distinguishedName      : {CN=Guest,CN=Users,DC=scriptinternals,DC=technet}instanceType           : {4}
whenCreated            : {11.12.2005 12:31:31}
whenChanged            : {17.10.2007 11:59:36}
uSNCreated             : {System.__ComObject}
memberOf               : {CN=Guests,CN=Builtin,DC=scriptinternals,DC=technet}
uSNChanged             : {System.__ComObject}
name                   : {Guest}
objectGUID             : {240 255 168 180 1 206 85 73 179 24 192 164 100 28 221 74}
userAccountControl     : {66080}
badPwdCount            : {0}
codePage               : {0}
countryCode            : {0}
badPasswordTime        : {System.__ComObject}
lastLogoff             : {System.__ComObject}
lastLogon              : {System.__ComObject}
logonHours             : {255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255
pwdLastSet             : {System.__ComObject}
primaryGroupID         : {514}
objectSid              : {1 5 0 0 0 0 0 5 21 0 0 0 184 88 34 189 250 183 7 172 165 75 78 29 245 1 0 0}
accountExpires         : {System.__ComObject}
logonCount             : {0}
sAMAccountName         : {Guest}
sAMAccountType         : {805306368}
objectCategory         : {CN=Person,CN=Schema,CN=Configuration,DC=scriptinternals,DC=technet}
isCriticalSystemObject : {True}
nTSecurityDescriptor   : {System.__ComObject}

ImportantEven Get-Member won’t return to you any more indications of the Description property. That’s a real deficiency as you have no way to recognize what other properties the ADSI object may possibly support as long as you’re using PowerShell’s own resources.. PowerShell always shows only properties that are defined.

However, this doesn’t mean that the Description property is now gone forever. You can create a new one any time:

$useraccount.Description = "New description"

Interesting, isn’t it? This means you could add entirely different properties that the object didn’t have before:

$useraccount.wwwHomePage = ""
$useraccount.favoritefood = "Meatballs"
Cannot set the Value property for PSMemberInfo object of type
"System.Management.Automation.PSMethod". At line:1 Char:11 + $useraccount.L <<<< oritefood = "Meatballs" $useraccount.SetInfo()

It turns out that the user account accepts the wwwHomePage property (and so sets the Web page of the user on user properties), while “favoritefood” was rejected. Only properties allowed by the schema can be set.

The Schema of Domains

The directory service comes equipped with a list of permitted data called a schema to prevent meaningless garbage from getting stored in the directory service. Some information is mandatory and has to be specified for every object of the type, others (like a home page) are optional. The internal list enables you to get to the properties that you may deposit in an ADSI object. The SchemaClass property will tell you which “operating manual” you need for the object:


Take a look under this name in the schema of the domain. The result is the schema object for user objects, which returns the names of all permitted properties in SystemMayContain.

$schema = $domain.PSBase.Children.find("CN=user,CN=Schema,CN=Configuration")
$schema.systemMayContain | Sort-Object

Setting Properties Having Several Values

PutEx() is not only the right tool for deleting properties but also for properties that have more than one value. Among these is otherHomePhone, the list of a user’s supplementary telephone contacts. The property can store just one telephone number or several, which is how you can reset the property telephone numbers:

$useraccount.PutEx(2, "otherHomePhone", @("123", "456", "789"))

But note that this would delete any other previously entered telephone numbers. If you want to add a new telephone number to an existing list, proceed as follows:

$useraccount.PutEx(3, "otherHomePhone", @("555"))

A very similar method allows you to delete selected telephone numbers on the list:

$useraccount.PutEx(4, "otherHomePhone", @("456", "789"))

Invoking Methods

All the objects that you’ve been working with up to now contain not only properties, but also methods. In contrast to properties, methods do not require you to call SetInfo() when you invoke a method that modifies an object. . To find out which methods an object contains, use Get-Member to make them visible (see Chapter 6):

$guest | Get-Member -memberType *Method

Surprisingly, the result is something of a disappointment because the ADSI object PowerShell delivers contains no methods. The true functionality is in the base object, which you get by using PSBase:

$guest.psbase | Get-Member -memberType *Method

   TypeName: System.Management.Automation.PSMemberSet

Name                      MemberType Definition
----                      ---------- ----------
add_Disposed              Method     System.Void add_Disposed(EventHandler value)
Close                     Method     System.Void Close()
CommitChanges             Method     System.Void CommitChanges()
CopyTo                    Method     System.DirectoryServices.DirectoryEntry CopyTo(DirectoryEntry newPare...
CreateObjRef              Method     System.Runtime.Remoting.ObjRef CreateObjRef(Type requestedType)
DeleteTree                Method     System.Void DeleteTree()
Dispose                   Method     System.Void Dispose()
Equals                    Method     System.Boolean Equals(Object obj)
GetHashCode               Method     System.Int32 GetHashCode()
GetLifetimeService        Method     System.Object GetLifetimeService()
GetType                   Method     System.Type GetType()
get_AuthenticationType    Method     System.DirectoryServices.AuthenticationTypes get_AuthenticationType()
get_Children              Method     System.DirectoryServices.DirectoryEntries get_Children()
get_Container             Method     System.ComponentModel.IContainer get_Container()
get_Guid                  Method     System.Guid get_Guid()
get_Name                  Method     System.String get_Name()
get_NativeGuid            Method     System.String get_NativeGuid()
get_ObjectSecurity        Method     System.DirectoryServices.ActiveDirectorySecurity get_ObjectSecurity()
get_Options               Method     System.DirectoryServices.DirectoryEntryConfiguration get_Options()
get_Parent                Method     System.DirectoryServices.DirectoryEntry get_Parent()
get_Path                  Method     System.String get_Path()
get_Properties            Method     System.DirectoryServices.PropertyCollection get_Properties()
get_SchemaClassName       Method     System.String get_SchemaClassName()
get_SchemaEntry           Method     System.DirectoryServices.DirectoryEntry get_SchemaEntry()
get_Site                  Method     System.ComponentModel.ISite get_Site()
get_UsePropertyCache      Method     System.Boolean get_UsePropertyCache()
get_Username              Method     System.String get_Username()
InitializeLifetimeService Method     System.Object InitializeLifetimeService()
Invoke                    Method     System.Object Invoke(String methodName, Params Object[] args)
InvokeGet                 Method     System.Object InvokeGet(String propertyName)
InvokeSet                 Method     System.Void InvokeSet(String propertyName, Params Object[] args)
MoveTo                    Method     System.Void MoveTo(DirectoryEntry newParent), System.Void MoveTo(Dire...
RefreshCache              Method     System.Void RefreshCache(), System.Void RefreshCache(String[] propert...
remove_Disposed           Method     System.Void remove_Disposed(EventHandler value)
Rename                    Method     System.Void Rename(String newName)
set_AuthenticationType    Method     System.Void set_AuthenticationType(AuthenticationTypes value)
set_ObjectSecurity        Method     System.Void set_ObjectSecurity(ActiveDirectorySecurity value)
set_Password              Method     System.Void set_Password(String value)
set_Path                  Method     System.Void set_Path(String value)
set_Site                  Method     System.Void set_Site(ISite value)
set_UsePropertyCache      Method     System.Void set_UsePropertyCache(Boolean value)
set_Username              Method     System.Void set_Username(String value)
ToString                  Method     System.String ToString()

Changing Passwords

The password of a user account is an example of information that isn’t stored in a property. That’s why you can’t just read out user accounts. Instead, methods ensure the immediate generation of a completely confidential hash value out of the user account and that it is deposited in a secure location. You can use the SetPassword() and ChangePassword() methods to change passwords:

$useraccount.SetPassword("New password")
$useraccount.ChangePassword("Old password", "New password")

Here, too, the deficiencies of Get-Member become evident when it is used with ADSI objects because Get-Member suppresses both methods instead of displaying them. You just have to “know” that they exist.

SetPassword() requires administrator privileges and simply resets the password. That can be risky because in the process you lose access to all your certificates outside a domain, including the crucial certificate for the Encrypting File System (EFS), though it’s necessary when users forget their passwords. ChangePassword doesn’t need any higher level of permission because confirmation requires giving the old password.

When you change a password, be sure that it meets the demands of the domain. Otherwise, you’ll be rewarded with an error message like this one:

Exception calling "SetPassword" with 1 Argument(s): 
"The password does not meet the password policy requirements.
Check the minimum password length, password complexity and password
history requirements. (Exception from HRESULT: 0x800708C5)"
At line:1 Char:22 + $realuser.SetPassword( <<<< "secret")

Controlling Group Memberships

Methods also set group memberships. Of course, the first thing you need is the groups in which a user becomes a member. That basically works just like user accounts as you could specify the ADSI path to a group to access the group. Alternatively, you can use a universal function that helpfully picks out groups for you:

function Get-LDAPGroup([string]$UserName, [string]$Start) 
  # Use current logon domain:
  $domain = [ADSI]""
  # OR: log on to another domain:
  #   $domain = new-object DirectoryServices.DirectoryEntry("LDAP://","domainuser",

# "secret")
if ($start -ne "") { $startelement = $domain.psbase.Children.Find($start) } else { $startelement = $domain } $searcher = new-object DirectoryServices.DirectorySearcher($startelement) $searcher.filter = "(&(objectClass=group)(sAMAccountName=$UserName))" $Searcher.CacheResults = $true $Searcher.SearchScope = "Subtree" $Searcher.PageSize = 1000 $searcher.findall() }

In Which Groups Is a User a Member?

There are two sides to group memberships. Once you get the user account object, the memberOf property will return the groups in which the user is a member:

$guest = (Get-LDAPUser Guest).GetDirectoryEntry()

Which Users Are Members of a Group?

The other way of looking at it starts out from the group: members are in the Member property in group objects:

$admin = (Get-LDAPGroup "Domain Admins").GetDirectoryEntry()
CN=Tobias Weltner,CN=Users,DC=scriptinternals,DC=technet

Groups on their part can also be members in other groups. So, every group object has not only the Member property with its members, but also MemberOf with the groups in which this group is itself a member.

Adding Users to a Group

To add a new user to a group, you need the group object as well as (at least) the ADSI path of the user, who is supposed to become a member. To do this, use Add():

$administrators = (Get-LDAPGroup Domain Admins).GetDirectoryEntry()
$user = (Get-LDAPUser Cofi1).GetDirectoryEntry()

In the example, the user Cofi1 is added to the group of Domain Admins. It would have sufficed to specify the user’s correct ADSI path to the Add() method. But it’s easier to get the user and pass the path property of the PSBase object.

Aside from Add(), there are other ways to add users to groups:

$administrators.Member = $administrators.Member + $user.distinguishedName

$administrators.Member += $user.distinguishedName

Instead of Add() use the Remove() method to remove users from the group again..

Creating New Objects

The containers at the beginning of this chapter also know how to handle properties and methods. So, if you want to create new organizational units, groups, and users, all you have to do is to decide where these elements should be stored inside a domain. Then, use the Create() method of the respective container.

Creating New Organizational Units

Let’s begin experimenting with new organizational units that are supposed to represent the structure of a company. Since the first organizational unit should be created on the topmost domain level, get a domain object:

$domain = [ADSI]""

Next, create a new organizational unit called “company” and under it some additional organizational units:

$company = $domain.Create("organizationalUnit", "OU=Idera")
$sales = $company.Create("organizationalUnit", "OU=Sales")
$marketing = $company.Create("organizationalUnit", "OU=Marketing")
$service = $company.Create("organizationalUnit", "OU=Service")

Create New Groups

Groups can be created as easily as organizational units. You should decide again in which container the group is to be created and specify the name of the group. In addition, define with the groupType property the type of group that you want to create, because in contrast to organizational units there are several different types of groups:

Group Code
Global 2
Local 4
Universal 8
As security group Add -2147483648

Table 19.3: Group Types

Security groups have their own security ID so you can assign permissions to them. Distribution groups organize only members, but have no security function. In the following example, a global security group and a global distribution group are created:

$group_marketing = $marketing.Create("group", "CN=Marketinglights")
$group_marketing.psbase.InvokeSet("groupType", -2147483648 + 2)

$group_newsletter = $company.Create("group", "CN=Newsletter")
$group_newsletter.psbase.InvokeSet("groupType", 2)

Creating New Users

To create a new user, proceed analogously, and first create the new user object in a container of your choice. Then, you can fill out the required properties and set the password using SetPassword(). Using the AccountDisabled property, enable the account. The following lines create a new user account in the previously created organization unit “Sales”:

$user = $sales.Create("User", "CN=MyNewUser")
$user.Description = "My New User"
$user.psbase.InvokeSet('AccountDisabled', $false)

Instead of Create() use the Delete() method to delete objects..