PowerShell Live - being retired

Please go to http://www.shelltools.net for more information

Welcome to PowerShell Live - being retired Sign in | Join | Help
in
Home Main Site Blogs Forums Videos Chat Customer Support

Mastering PowerShell in your Lunch Break

Day 7: Manage Users

In Day 6, you discovered how to connect to domains (or your local computer) and bind to objects. Today, I’ll focus on user objects and what you can do with them.

Grabbing A User

Even if this header may not be politically correct, it describes the process of accessing or binding to a user. There are many ways how you can pick a user. No matter how you do it, you get back a user object that represents the user.

Binding was covered in Day 6 already. Here’s a quick refresher. Each of these samples returns a user object.

Getting Local Users

Get a local user account by name:

$user = [ADSI]"WinNT://./Administrator,user"

Or (faster):

$computer = [ADSI]"WinNT://.,computer"

$user = $computer.psbase.children.Find("Administrator", "User")

Getting Domain Users

Get a domain user account by name:

$ADsPath = "LDAP://CN=Administrator,CN=Users," + ([ADSI]"").DistinguishedName

$user = [ADSI]$AdsPath

Or (without knowing its exact location):

$UserName = "Administrator"

$searcher = new-object DirectoryServices.DirectorySearcher([ADSI]"")

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

$founduser = $searcher.findOne()

$user = $searcher.getDirectoryEntry()

Note that the search returns a SearchResult which is not the real user object. You do have access to most of the user object properties so the SearchResult may suffice in some cases:

$founduser.Properties

However, to get full access and receive an object of same type as with the direct access samples, you need to call getDirectoryEntry().

Note also that the DirectorySearcher can return more than one result. I have discussed the DirectorySearcher in great detail in Day 6. So if you wanted to retrieve a number of users to do things with them, use the appropriate search filter that fits your need best (Day 6). Here’s a sample:

$UserName = "A*"

$searcher = new-object DirectoryServices.DirectorySearcher([ADSI]"")

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

$searcher.findAll() | % {

$user = $_.getDirectoryEntry()

$user.name

}

Getting A User Based On Its GUID

If you read Day 6 carefully, you now know how to retrieve the special GUID from an ADSI object. The GUID is the only object property that never changes throughout its lifetime and therefore a good (and very fast) way of binding to that object. If you know the GUID of a user account, this is how you bind to it:

$user = [ADSI]"LDAP:<GUID=D83F10601E7111CFB1F302608C9E7553>"

Reading User Properties (And Writing To Them, Too)

Each user object contains a rich set of properties. You can see the available properties with get-member:

$user | get-member

Get-Member does not list all available properties, though. It lists only properties that actually contain data. So if you created a new user with a blank description, the property “description” will not be listed. Once you define that property (as you will in a second), get-member lists “description” as one of the properties.

To get a good overview and see all current properties and their values, use this trick:

$user.psbase.properties

This is an excellent way of finding the property you are after. For example, if you’d like to know how to set the phone number in a user account, open Active Directory Users and Computers and change the phone number manually.

Next, bind to that user and use above line to dump all information contained in the user object. Look for the data you entered in the Windows GUI. Next to it, you read the property name.

Once you know the name of the property to change, read on to find out how to read and write (change) that property with PowerShell!

Reading Stuff

To access one of the properties, you have three choices plus an extra choice:

$user.objectClass

$user.Get("objectClass")

$user.psbase.InvokeGet("objectClass")

All three seem to pretty much work alike but they are different. You’ll find out in a second.

There is one extra choice:

$user.GetEx("objectClass")

GetEx always returns an array. So if the property you are reading is an array in the first place – like objectClass is – then there is no difference, and all four alternatives return an array in which you can index into its individual fields. The following lines read the first element from that array:

$user.objectClass[0]

$user.Get("objectClass")[0]

$user.psbase.InvokeGet("objectClass")[0]

$user.Get("objectClass")[0]

GetEx really starts to behave different when the property you are reading is not an array. In this case, GetEx still returns an array with just one single element:

$user.cn.GetType()

$user.Get("cn").GetType()

$user.psbase.InvokeGet("cn").GetType()

$user.GetEx("cn").GetType()

$user.GetEx("cn")[0]

$user.GetEx("cn").Count

So use GetEx when you want to evaluate a property as array no matter what data the property actually contains.

To get a good picture of the data contained in your user object, you could query all available properties using either one of the four ways. However, there is a much easier way.

$user.psbase.properties

Now PowerShell lists all available properties including their values for you. That’s an excellent way of exploring an object and finding properties that contain needed data.

Writing Stuff

Next, check out how you change the value of a property. Again, you have three different ways for doing that plus a special extra way. Let us change the description for a user:

$user.Description = "My User"

$user.Put("Description", "My User")

$user.psbase.InvokeSet("Description", "My User")

Whenever you change a property, the change occurs in your local copy of the object. To become effective, you need to write back your changes to the original object. So to make your changes permanent, you use SetInfo():

$user.SetInfo()

Be aware of the fact that only once you call SetInfo(), your property values take effect. Without SetInfo(), the property assignments are valid only in your local copy of the object you are messing with.

Also, when you use SetInfo() to write back your changes, your arguments will be evaluated for validity. So if you specified invalid property values, you get away with it at first. Once you call SetInfo() though, you get the bill.

The exact opposite would be undoing your changes and reloading the original object properties back into your cache, thereby overwriting all the changes you made previously:

$user.GetInfo()

Deleting Properties

There is one extra method for setting property values: PutEx. With PutEx, you manage properties that contain array data. PutEx also is able to completely delete (reset) a value.

To delete a property value, you use PutEx like this:

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

$user.SetInfo()

This line completely deletes the description property. So when you now try and read the property, you get an error message complaining that there is no value in the cache:

$user.Get("Description")

Side note: When you check the description property using the “.”-notation, you will be surprised:

$user.Description

Although PutEx has blanked the property, this line will still return the old description. That’s a bug in PowerShell. The “.”-notation does not get updated when you change a property through any of the other methods. Check this out:

$user.Description = "Test 1"

$user.Description

$user.Put("Description", "Test 2")

$user.Description

$user.psbase.InvokeSet("Description", "Test 3")

$user.Description

It really is a bit messy because every now and then, the “.”-notation does get updated, most of the time it does not.

Dealing With Properties That Contain Arrays (Multiple Entries)

PutEx takes three arguments. The first one tells PutEx what you want:

Argument

Operation

1

Clear property (delete content)

2

Update property (replace content)

3

Append property (add content)

4

Delete part of the property content

The second argument is the name of the property you want to change. The third argument is your value.

In the previous example where we deleted a property value, the third argument was a dummy argument. If you plan to use the other operation modes to update, append or delete a value, you need to specify a correct value as third argument.

To update a property, you’d use this line:

$user.PutEx(2, "Description", @("New Description"))

$user.SetInfo()

Surprised? Note that PutEx deals with arrays. If you work with properties that contain a single value, keep it simple and use Put instead – or any of the other simple ways. If you still want to use PutEx, you must supply the value as array. That’s what @(…) does. It creates an array. In the previous example, the array contained only one member.

If you assigned more than one value to the Description property, you’d get an error the moment you are trying to update the object with SetInfo() because Description cannot hold an array:

$user.PutEx(2, "Description", @("New Description", "Another one"))

$user.SetInfo()

So PutEx really only makes sense with properties that are arrays. One would be the property otherHomePhone which stores any number of phone numbers:

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

$user.SetInfo()

When you open the user dialog in Active Directory Users and Computers, click on the tab for phone numbers and then click on Others, you see your three numbers in a list.

To add a new number to the existing list, you use append mode:

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

$user.SetInfo()

If you’d like to take out a number and leave the others in the list, use delete mode:

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

$user.SetInfo()

Creating New Users

Creating a completely new user is of course another way of getting a user object. We did not cover this in Day 6, so here’s how you create new users.

First, create a new local user:

$computer = [ADSI]"WinNT://."

$user = $computer.Create("user", "MyNewUser")

$user.SetPassword("topsecret")

$user.SetInfo()

$user.Description = "My New User"

$user.SetInfo()

Next, create a new user account in your domain (provided you are joined to a domain and have the permissions):

$ADsPath = "LDAP://CN=Users," + ([ADSI]"").distinguishedName

$container = [ADSI]$ADsPath

$user = $container.Create("User", "CN=MyNewUser")

$user.SetInfo()

$user.Description = "My New User"

$user.SetPassword("TopSecret99")

$user.psbase.InvokeSet('AccountDisabled', $false)

$user.SetInfo()

An Indepth Look At Imperfection

When you look at both scripts, you may be slightly confused, and that’s partially because of certain requirements ADSI imposes but mostly because of the ugly side of PowerShell:

  • You need to resort to the underlying base objects using psbase all the time because the “processed” friendly objects returned by PowerShell are not so friendly after all. They are crippled to the degree of uselessness
  • You need to know the exact order in which new user objects need to be created. This “protocol” is required by ADSI, but PowerShell changed that also and made it even more complex.
  • You need to know lots of methods that are there but for some reason were hidden by the PowerShell team.
  • You need to know which properties are handled by which entity to guess how to access them in the correct way. So you end up with a confusing mix of techniques caused by PowerShells immature tries to actually simplify ADSI access.

Let me explain.

Pick A Container

When you create new objects – any ADSI objects for that matter, but let us focus on user objects for the moment – you first access the ADSI container you want to put the objects in. For local user accounts, that is easy because the WinNT: world that handles all local accounts is a single namespace:

$computer = [ADSI]"WinNT://."

When you create new objects in your domain, it’s a different ball game because Active Directory is a hierarchical store, and you need to bind to the container that you want your new user to be kept in. This would bind to the Users container in the domain scriptinternals.technet:

$container = [ADSI]"CN=Users,DC=scriptinternals,DC=technet"

Using the special X500-related syntax used by LDAP, you could bind to any container this way, let’s say an organizationalUnit:

$container = [ADSI]"OU=Marketing,OU=Company,DC=scriptinternals,DC=technet"

Since it is bad habit to hard-code domain names, you can ask ADSI for the domain you are currently logged on to, and that’s what we did in the example:

$ADsPath = "LDAP://CN=Users," + ([ADSI]"").distinguishedName

$container = [ADSI]$ADsPath

Create A New Object In Your Container

Next, you need to create a new object in the container you picked. But how?

The container object you got from PowerShell does not seem to contain a way of creating new objects, at least get-member won’t show a method that would create new objects. In the past, that was really true and you needed to resort to the underlying raw base object:

$user = $container.psbase.children.psbase.Add("CN=NewUser", "user")

With the final release of PowerShell V1.0, the team added the Create method to their “friendly” object so you no longer need the psbase object:

$user = $container.Create("user", "CN=NewUser")

However, Create is hidden, as are all the other methods silently added to the “friendly” user object. So you need to know them at heart, no TAB completion, no discoverability.

Cook It In The Right Order

Once you have your new object, you need to be very careful about the right order in which you do things. And now, you’ll also discover some bugs in PowerShell.

If you try and set default properties of your new object, you fail:

$computer = [ADSI]"WinNT://."

$user = $computer.Create("user", "MyNewUser")

$user.Description = "My New User"

You first have to instantiate the object using SetInfo() or CommitChanges() which is the .NET version provided by the raw base object. Either one works:

$user.psbase.CommitChanges()

$user.SetInfo()

So to set default properties on new objects, you would need to do this:

$computer = [ADSI]"WinNT://."

$user = $computer.Create("user", "MyNewUser")

$user.SetInfo()                                # frankensteinize the base user account

$user.Description = "My New User"

$user.SetInfo()                                # save any default property values

This causes a dilemma, though. Some objects have so called mandatory properties that need to be set before you can instantiate an object. With Windows 2000 Servers, user objects required to have a valid sAMAccountname, and although not required, it’s still a good idea to provide that.

So to set a property before a new object is instantiated, you must use one of the other ways of setting property values, for example:

$computer = [ADSI]"WinNT://."

$user = $computer.Create("user", "MyNewUser")

$user.Put("Description", "My New User")

$user.SetInfo()                                # save any default property values

Or:

$computer = [ADSI]"WinNT://."

$user = $computer.Create("user", "MyNewUser")

$user.psbase.InvokeSet("Description", "My New User")

$user.SetInfo()                                # save any default property values

The “.”-notation of accessing object properties is the most natural and convenient one. However, it is also the most buggy.

You just noticed that with “.”-notation, you cannot set properties in new objects until you instantiated them. Also, you have previously discovered that “.”-notation yields wrong data in certain circumstances. So our recommendation for the time being would be either not to use “.”-notation at all or to not mix it with other ways of setting property values.

Now, what about a password? That’s a Catch22 because you need a password to instantiate the user account and you need to instantiate the user account before you can change it.

That’s why you can set the password before you actually instantiate the user account:

$computer = [ADSI]"WinNT://."

$user = $computer.Create("user", "MyNewUser")

$user.SetPassword("topSecret99")        # you MAY set the password this early

#$user.Description = "My New User"             # you MAY NOT set props at this time using

                                        # "."-Notation, but you *could* this way:

#$user.Put("Description", "My New User")

 

$user.SetInfo()                                # frankensteinize the base user account

$user.Description = "My New User"

$user.SetInfo()                                # save any default property values

With LDAP: objects, it’s yet another ball game. First of all, you run into serious problems if you mess with a new object before you have instantiated it:

$ADsPath = "LDAP://CN=Users," + ([ADSI]"").distinguishedName

$container = [ADSI]$ADsPath

$user = $container.Create("User", "CN=MyNewUser")

$user.Description = "My New User"

$user.SetInfo()

Once you call SetInfo(), you get a completely misleading error message. In essence, it is complaining about the fact that you already set the Description property. The only way to recover from that problem is to start over again and this time make sure you instantiate your new user object before you use it:

$ADsPath = "LDAP://CN=Users," + ([ADSI]"").distinguishedName

$container = [ADSI]$ADsPath

$user = $container.Create("User", "CN=MyNewUser")

$user.SetInfo()                         # Instantiate New Object Before Use

$user.Description = "My New User"

$user.SetInfo()

With LDAP, you cannot even set a password before you instantiated your user object. That at least makes a bit sense. LDAP: uses Kerberos to communicate the new password to the domain controller, and for that you need to be a) logged on to the domain and b) have an actual object.

Since this would create a Catch22 with domain requirements for a password, LDAP has a workaround. It creates your new user account without a password but disables it. This way, the password security isn’t triggered. All you need to do is make sure you set a proper password before you enable the account. That’s what we did in our sample code next:

$user.SetPassword("TopSecret99")

A Serious Bug: Special Properties

The final step in creating a user account is to enable the account. That sounds simple, and it is. All you need to do is set the AccountDisabled property to $false. If you try, though, you most likely fail:

$user.AccountDisabled = $false

$user.SetInfo()

An error message appears. There seems to be a conflict. Your only escape is to roll back your changes and reload the object properties from the domain:

$user.GetInfo()

What happened? It looks like some kind of interface conflict. PowerShell happily adds any property you use to the object. Some properties like AccountDisabled are handled differently, though, so when you try and access AccountDisabled the way we just did, you can no longer write back your object to the domain using SetInfo().

The same conflict occurs when you resort to the Put method:

$user.Put("AccountDisabled", $false)

$user.SetInfo()                                # ERROR

$user.GetInfo()                                # UNDO

The only way that works with these special properties is using InvokeSet, provided by the underlying raw base object:

$user.psbase.InvokeSet('AccountDisabled', $false)

$user.SetInfo()

Both the “.”-notation and Put are unable to set some properties that have special meaning to the object. That’s clearly a bug in the PowerShell implementation of the user object.

So when you look at all four ways of setting property values, only InvokeSet() always works. The “.”-notation – although the most convenient one – has the most bugs and inconsistencies. Put falls short when it comes to special properties.

Deleting A User

Deleting users is dangerous but easy. Keep in mind that deleting a user is permanent. So don’t delete users if you may need them again later. Instead, disable users in this case. Above, you’ve seen how you disable users using AccountDisabled.

To delete a user, you again bind to the container first that contains the user. Next, you remove the user object from that container. This is how you delete a local account:

$computer = [ADSI]"WinNT://."

$computer.Delete("user", "MyNewUser")

And this is how you delete a domain account:

$ADsPath = "LDAP://CN=Users," + ([ADSI]"").distinguishedName

$container = [ADSI]$ADsPath

$container.Delete("User", "CN=MyNewUser")

If you don’t know where a user account really lives, use DirectorySearcher again:

$UserName = "MyNewUser"

$searcher = new-object DirectoryServices.DirectorySearcher([ADSI]"")

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

$founduser = $searcher.findOne()

$user = $searcher.getDirectoryEntry()

$user.psbase.Parent.Delete("user", "CN=" + $user.cn)

The search returns the user object. Its “Parent” property returns the container the user lives in. All you do is call again “Delete” and supply the user name.

Setting And Changing A User Password

Setting a new password is easy if you are an Admin. Simply use SetPassword:

$user.SetPassword("New Password")

Just make sure your password meets the security requirements. Otherwise, you may get strange error messages. The same is true when you try and set a password for a domain user account while not joined to the domain. In Day 6, you have seen how you can bind to a domain without being a member of that domain. In this case, though, you may be unable to set passwords, depending on the security settings, because if you are not a domain member, you cannot use Kerberos.

Also note that on stand-alone systems (not domain members), changing the password with brute force like that trashes your security certificates. If you used the encrypting file system (EFS) and reset the password, you will no longer be able to access your EFS data.

If you know the old password, you can change the password without Admin privileges:

$user.ChangePassword("Old Password", "New Password")

Control Group Membership

Group memberships control what a user can and cannot do. By becoming a member in a group, the user receives all the privileges granted to that group.

List Membership Status

You can control this membership from both sides. Let’s start with a user and find out its membership status:

$ADsPath = "LDAP://CN=Administrator,CN=Users," + ([ADSI]"").DistinguishedName

$user = [ADSI]$AdsPath

$user.memberOf

The memberOf property returns each group the user is member of.

Next, let’s take the other perspective and see which users are member in a specific group:

$ADsPath = "LDAP://CN=Domain-Admins,CN=Users," + ([ADSI]"").DistinguishedName

$group = [ADSI]$AdsPath

$group.member

Add User To Group

To get a user into a group, you can use Add which is a hidden method provided by the group:

$ADsPathUser = "LDAP://CN=Administrator,CN=Users," + ([ADSI]"").DistinguishedName

$ADsPathGroup = "LDAP://CN=Domain-Admins,CN=Users," + ([ADSI]"").DistinguishedName

$user = [ADSI]$AdsPathUser

$group = [ADSI]$AdsPathGroup

 

$group.Add($user.psbase.Path)                               # VARIANT 1

$group.Add("LDAP://" + $user.distinguishedName        # VARIANT 2

$group.SetInfo()

Or you use a bit more PowerShell style:

$ADsPathUser = "LDAP://CN=Administrator,CN=Users," + ([ADSI]"").DistinguishedName

$ADsPathGroup = "LDAP://CN=Domain-Admins,CN=Users," + ([ADSI]"").DistinguishedName

$user = [ADSI]$AdsPathUser

$group = [ADSI]$AdsPathGroup

 

$group.Member = $group.Member + $user.distinguishedName     # VARIANT 1

$group.Member += $user.distinguishedName              # VARIANT 2

$group.SetInfo()

Just make sure you add the user only once to the group. And please note that here the same buggy behaviour exists when you use “.”-notation. If you wanted to use PutEx() instead, this is how you do it:

$group.PutEx(3, "member", @($user.distinguishedName))

You do not necessarily need a live $user object. If you know the distinguishedName by heart, you can specify it directly:

$group.PutEx(3, "member", @("CN=MyNewUser,CN=Users,DC=scriptinternals,DC=technet"))

Remove User From Group

To remove a user from a group, you use the Remove method. Any one of the following is valid:

$group.Remove($user.psbase.path)

$group.Remove("LDAP://" +  $user.distinguishedName)

$group.Remove("LDAP://CN=MyNewUser,CN=Users,DC=scriptinternals,DC=technet")

You can also remove a user using PutEx:

$group.PutEx(4, "member", @($user.distinguishedName))

$group.PutEx(4, "member", @("CN=MyNewUser,CN=Users,DC=scriptinternals,DC=technet"))

List Of Hidden Methods

As seen before, user objects returned by PowerShell contain important methods that are there but invisible and not discoverable using TAB completion or get-member.

Method

Description

Put

Writes a new value to a property

PutEx

Deletes a property value or writes array values to a property

Get

Reads a value from a property

GetEx

Reads a value from a property and always returns it as array

SetPassword

Sets a new password. You need Admin privileges for that

ChangePassword

Sets a new password. You must specify the old password first and do not need Admin privileges

GetInfo

Undos any changes you made to the properties and reloads the original property values

SetInfo

Saves your changes to properties and makes them permanent

GetInfoEx

Same as GetInfo, except here you can specify which properties you want to reload: $user.GetInfoEx(@("description", "userflags"), 0)

Also, there are hidden methods in container objects and group objects:

Method

Description

Create

Provided by a container such as the Users container or any organizationalUnit. Creates a new object in the container and returns it.

Delete

Provided by a container such as the Users container or any organizationalUnit. Deletes an existing object present in the container

Add

Provided by a group object. Adds a new user or group to the group. Add actually needs the LDAP path to the user or group to be added

Remove

Provided by a group object. Removes a user or group from the group. Remove actually needs the LDAP path to the user or group to be removed

 

 

 

 

Summary

There are a number of ways to get a user object. You can use its direct ADsPath, you can access a user object with its GUID (if you know it), and you can search for a user.

Once you have a user object, you can examine its properties to find out information about that user. Or you can access the underlying base object using psbase. That can be useful to dump all user properties or to get access to raw ADSI methods not available in the processed PowerShell version of that object.

You can also change properties. There are four ways of doing that, and you have seen that most of them are buggy in certain circumstances. Only the InvokeSet() method provided by the underlying base object works reliably in all situations.

Note that you need to call SetInfo() to make any changes permanent. SetInfo() is necessary only to write back changes you made to properties. You don’t need to call SetInfo() when you made changes using method calls. For example, SetPassword() and ChangePassword() both take effect immediately.

To create new users (or delete existing ones), you bind to the container the user object lives in. Use the container methods Create and Delete to create new users and delete existing ones. To successfully create new users, you need