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
- Accessing a Container
- Accessing Individual Users or Groups
- Reading and Modifying Properties
- Invoking Methods
- Creating New Objects
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]"" $domain distinguishedName ----------------- {DC=scriptinternals,DC=technet}
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]"" $domain distinguishedName ----------------- {DC=scriptinternals,DC=technet}
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://10.10.10.1","domainuser", ` "secret") $domain.name 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://10.10.10.1" # 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:\10.10.10.1" # 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://10.10.10.1",$cred.UserName, $pwd) $domain.name 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://10.10.10.1",$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://10.10.10.1$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" $users
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" $users
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" $users.PSBase.Children distinguishedName ----------------- {CN=admin,CN=Users,DC=scriptinternals,DC=technet} {CN=Administrator,CN=Users,DC=scriptinternals,DC=technet} {CN=All,CN=Users,DC=scriptinternals,DC=technet} {CN=ASPNET,CN=Users,DC=scriptinternals,DC=technet} {CN=Belle,CN=Users,DC=scriptinternals,DC=technet} {CN=Consultation2,CN=Users,DC=scriptinternals,DC=technet} {CN=Consultation3,CN=Users,DC=scriptinternals,DC=technet} {CN=ceimler,CN=Users,DC=scriptinternals,DC=technet} (...)
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" } distinguishedName ----------------- {CN=admin,CN=Users,DC=scriptinternals,DC=technet} {CN=Administrator,CN=Users,DC=scriptinternals,DC=technet} {CN=ASPNET,CN=Users,DC=scriptinternals,DC=technet} {CN=Belle,CN=Users,DC=scriptinternals,DC=technet} (...)
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") $useraccount.Description 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))" $searcher.findall()
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://10.10.10.1","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://10.10.10.1/CN=Administrator,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://10.10.10.1","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) } else { $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 |
(&(objectCategory=person)(objectClass=User))
|
Find only user accounts, not computer accounts |
(sAMAccountType=805306368)
|
Find only user accounts (much quicker, but harder to read) |
(&(objectClass=user)(sn=Weltner)
(givenName=Tobias)) |
Find user accounts with a particular name |
(&(objectCategory=person)(objectClass=user)
(msNPAllowDialin=TRUE)) |
Find user with dial-in permission |
(&(objectCategory=person)(objectClass=user)
(pwdLastSet=0)) |
Find user who has to change password at next logon |
(&(objectCategory=computer)(!description=*))
|
Find all computer accounts having no description |
(&(objectCategory=person)(description=*))
|
Find all user accounts having no description |
(&(objectCategory=person)(objectClass=user)
(whenCreated>=20050318000000.0Z)) |
Find all elements created after March 18, 2005 |
(&(objectCategory=person)(objectClass=user)
(|(accountExpires=9223372036854775807) (accountExpires=0))) |
Find all users whose account never expires (OR condition, where only one condition must be met) |
(&(objectClass=user)(userAccountControl:
1.2.840.113556.1.4.803:=2)) |
Find all disabled user accounts (bitmask logical AND) |
(&(objectCategory=person)(objectClass=user)
(userAccountControl:1.2.840.113556.1.4.803:=32)) |
Find all users whose password never expires |
(&(objectClass=user)(!userAccountControl:
1.2.840.113556.1.4.803:=65536)) |
Find all users whose password expires (logical NOT using “!”) |
(&(objectCategory=group)(!groupType:
1.2.840.113556.1.4.803:=2147483648)) |
Finding all distribution groups |
(&(objectCategory=Computer)(!userAccountControl
:1.2.840.113556.1.4.803:=8192)) |
Finding all computer accounts that are not domain controllers |
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() $useraccount.psbase.NativeGUID f0ffa8b401ce5549b318c0a4641cdd4a
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>" $acccount distinguishedName ----------------- {CN=Guest,CN=Users,DC=scriptinternals,DC=technet}
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://10.10.10.1/$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://10.10.10.1/CN=Guest,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://10.10.10.1/CN=Guest,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” $useraccount.SetInfo()
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:
$useraccount.Description Predefined account for guest access
But there are also two other options and they look like this:
$useraccount.Get("Description") $useraccount.psbase.InvokeGet("Description")
At first glance, both seem to work identically. However, differences become evident when you query another property: AccountDisabled.
$useraccount.AccountDisabled $useraccount.Get("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") $useraccount.psbase.InvokeGet("AccountDisabled") False
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" $useraccount.SetInfo() # Method 2: $useraccount.Put("Description", "Another new description") $useraccount.SetInfo() # Method 3: $useraccount.PSBase.InvokeSet("Description", "A third description") $useraccount.SetInfo()
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:
$useraccount.AccountDisabled
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 $useraccount.SetInfo() 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():
$useraccount.GetInfo()
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) $useraccount.SetInfo()
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 |
To completely remove the Description property, use PutEx() with these parameters:
$useraccount.PutEx(1, "Description", 0) $useraccount.SetInfo()
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" $useraccount.SetInfo()
Interesting, isn’t it? This means you could add entirely different properties that the object didn’t have before:
$useraccount.wwwHomePage = "http://www.powershell.com" $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:
$useraccount.psbase.SchemaClassName user
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 accountExpires aCSPolicyName adminCount badPasswordTime badPwdCount businessCategory codepage controlAccessRights dBCSPwd defaultClassStore desktopProfile dynamicLDAPServer groupMembershipSAM groupPriority groupsToIgnore homeDirectory homeDrive homePhone initials lastLogoff lastLogon lastLogonTimestamp lmPwdHistory localeID lockoutTime logonCount logonHours logonWorkstation mail manager maxStorage mobile msCOM-UserPartitionSetLink msDRM-IdentityCertificate msDS-Cached-Membership msDS-Cached-Membership-Time-Stamp mS-DS-CreatorSID msDS-Site-Affinity msDS-User-Account-Control-Computed msIIS-FTPDir msIIS-FTPRoot mSMQDigests mSMQDigestsMig mSMQSignCertificates mSMQSignCertificatesMig msNPAllowDialin msNPCallingStationID msNPSavedCallingStationID msRADIUSCallbackNumber msRADIUSFramedIPAddress msRADIUSFramedRoute msRADIUSServiceType msRASSavedCallbackNumber msRASSavedFramedIPAddress msRASSavedFramedRoute networkAddress ntPwdHistory o operatorCount otherLoginWorkstations pager preferredOU primaryGroupID profilePath pwdLastSet scriptPath servicePrincipalName terminalServer unicodePwd userAccountControl userCertificate userParameters userPrincipalName userSharedFolder userSharedFolderOther userWorkstations
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")) $useraccount.SetInfo()
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")) $useraccount.SetInfo()
A very similar method allows you to delete selected telephone numbers on the list:
$useraccount.PutEx(4, "otherHomePhone", @("456", "789")) $useraccount.SetInfo()
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://10.10.10.1","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() $guest.memberOf CN=Guests,CN=Builtin,DC=scriptinternals,DC=technet
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() $admin.member CN=Tobias Weltner,CN=Users,DC=scriptinternals,DC=technet CN=Markus2,CN=Users,DC=scriptinternals,DC=technet CN=Belle,CN=Users,DC=scriptinternals,DC=technet CN=Administrator,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() $administrators.Add($user.psbase.Path) $administrators.SetInfo()
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.SetInfo() $administrators.Member += $user.distinguishedName $administrators.SetInfo()
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") $company.SetInfo() $sales = $company.Create("organizationalUnit", "OU=Sales") $sales.SetInfo() $marketing = $company.Create("organizationalUnit", "OU=Marketing") $marketing.SetInfo() $service = $company.Create("organizationalUnit", "OU=Service") $service.SetInfo()
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 |
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_marketing.SetInfo() # $group_newsletter = $company.Create("group", "CN=Newsletter") $group_newsletter.psbase.InvokeSet("groupType", 2) $group_newsletter.SetInfo()
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.SetInfo() $user.Description = "My New User" $user.SetPassword("TopSecret99") $user.psbase.InvokeSet('AccountDisabled', $false) $user.SetInfo()
Instead of Create() use the Delete() method to delete objects..