SUPPORT@PowerShell.com

Shell Tools Support for Powershell Analyzer and Powershell Plus
Welcome to SUPPORT@PowerShell.com Sign in | Join | Help
in
Home Main Site Blogs Forums Videos Chat Customer Support

Mastering PowerShell in your Lunch Break

Day 6: ADSI Connecting to Domains/Computers and Binding to Objects

In Day 5, we looked at WMI. Today, we’ll look at just another important service used by many advanced scripts: ADSI. It’s also one of the areas where PowerShell is highly “underdocumented” so we are about to change that a bit.

ADSI is responsible for users, groups, computers and domains. So with ADSI, you can manage your users, create new accounts, list disabled users (and enable them), and much much more.In fact, with ADSI you can do so much more that it’s way to much for a single lunch break. That’s why today we cover the basics.

Connecting to a Domain (Or Local Computer)

Connecting to your domain is pretty simple using PowerShell. Take a look:

$domain = [ADSI]""

$domain

[ADSI] is one of those strange type accelerators that you met in Day 5 already.

You’re not currently logged on to a domain? Too bad, then you get an error. There are a number of good reasons not to be joined to a domain:

  • There’s a domain but you are not joined: Maybe you walked in a company as an external consultant, and your notebook is no domain member. Still, you have a network cable and could easily contact the companies domain. So we’ll have to look at ways to connect and authenticate with a domain without the need to be joined to that domain.
  • It’s the wrong domain: Maybe there are more than one domains, and you don’t want to connect to the one you are currently logged on to. Again the need to specify which domain you actually want to connect to.
  • There is no domain: At home or in a small network, there may not be a domain, so obviously your computer is not joined to a domain. You’ll see in a second how you connect to your local user accounts.

Authenticating With User Credentials

The [ADSI] type accelerator has no authentication built in. However, that’s ok because .NET does. In fact, when you look at the type of object returned by [ADSI], you will see that it actually is a DirectoryServices.DirectoryEntry .NET object:

$domain = [ADSI]""

$domain | get-member

So you could bypass the type accelerator and get your DirectoryEntry object from .NET in the first place:

$domain = new-object DirectoryServices.DirectoryEntry("","domain\user", "pwd")

Selecting (Different) Domains

This takes care of the authentication part. However, if you are currently not joined to the (right) domain, you also need to specify where the domain actually can be found:

$domain = new-object DirectoryServices.DirectoryEntry("LDAP://10.10.10.1","domain\user", "pwd")

$domain.name

$domain.distinguishedName

In this example, I simply used the IP address of a domain controller I knew and connect to it with a user account of that domain that owns the necessary privileges.

Note that TAB completion works here. It may take a couple of seconds to kick in though because PowerShell queries the schema for that.

IMPORTANT: The ADSI developers liked to be an exclusive club of geeks and did not want others to easily play with them. So they invented a very strange syntax. Note that ADSI monikers are case sensitive! Also, use forward backslashes, not backward backslashes. Neither of the following works:

$domain = [ADSI]"ldap://10.10.10.1"                   # WRONG!

$domain = [ADSI]"LDAP:\\10.10.10.1"                   # WRONG!

$user = [ADSI]"Winnt://./Administrator,user"          # WRONG!

$user = [ADSI]"WinNT:\\.\Administrator,user"          # WRONG!

While PowerShell would happily accept those lines, you receive an error the moment you try and access the returned object which all by itself is an interesting phenomenon because it illustrates that PowerShell binds to the object only once you start using it.

If you don’t need special authentication and still want to choose a different domain, you can use this line instead:

$domain = [ADSI]"LDAP://testdomain"

Accessing Local User Accounts

What if there is no domain because you are sitting at home with only one or two PCs? What if you want to manage local user accounts in a domain environment? What if…?

You cannot use the LDAP: moniker then because LDAP: always requires an LDAP-compatible directory service. Stand-alone PCs don’t have fancy stuff like this. They just have a SAM database.

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

Enumerating Content

ADSI is all about containers. When you connect to your local computer using WinNT: as in the last example, you get a container full of objects that live in your computer: user accounts, groups, and – yes – even services.

When you connect to a domain using LDAP:, it’s the same game except that now you have tons of containers: Active Directory is a hierarchical structure. On top, there are the top containers like Users and all organizational units you may have created. From there, you can walk down to other containers until you finally reach the container that you want to work with.

So while WinNT: is pretty much like a little garden shed with everything stuffed in it, LDAP: is more like a ten story basement with zillions of rooms. Things are better organized, but first you have to find the right place to go to.

The initial question is: How do you list the contents of a container? Here’s how:

$domain = [ADSI]" "

$domain.psbase.children | % {

"Name: " + $_.name

"DistinguishedName: " + $_.distinguishedName

"Class: " + $_.objectClass[$_.objectClass.count -1]

"========================================"

}

Similarly, you enumerate the contents of your local computer:

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

$computer.psbase.children | % {

"Name: " + $_.name

"Class: " + $_.psbase.SchemaClassName

"========================================"

}

Wait a minute. Why do both scripts vary so much? What’s this psbase thing? Why would I use different ways to find out the class of an object? Where is my consistency?

First, let’s look at the common things. To enumerate the content of any ADSI container, you use children. Unfortunately, there is no children in your object.

That’s because the PowerShell guys have sent it through their type adapter before they handed it to you, and the type adapter tried to create a “friendly” object. It would take too long to explain their good intentions. It simply didn’t work because the new friendly object lacks properties like children that are necessary to overcome other weaknesses in PowerShell that older script languages did not need to take care of in the first place.

Fortunately, the PowerShell team did allow access to the “raw” ADSI object from the one synthesized by the PowerShell type adaptor. You get the raw version through psbase. Check out the difference:

$domain | get-member

$domain.psbase | get-member

$computer | get-member

$computer.psbase | get-member

Once you get the child objects that live inside a container, you can enumerate them using a foreach-object loop. % { … } is just a shortcut for that.

Inside the loop, $_ represents an individual child. Both scripts now output (some of the vast) information contained in the childs.

Since WinNT: and LDAP: are really two different worlds, they organize information differently, and that’s why both scripts use different properties to find out information.

To better understand what’s going on here, let’s see how you get access to exactly one object, for example one specific user. We then use this object to examine its inner workings more closely.

Accessing Individual Objects

There are plenty ways to access an individual object. One you know already. You used it already. Remember? When you connected to the domain or computer, you already accessed a single unique object. Remember how you did that?

$domain = [ADSI]"LDAP://DC=scriptinternals,DC=technet"

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

You used the type accelerator [ADSI] followed by the so called ADsPath of the object you wanted to access. This way, you can reach any object – as long as you know its ADsPath.

Using ADsPath To Connect Directly

To access your local Administrator user account, you could do this:

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

And this is how you access the user account of the currently logged on user:

[ADSI]"WinNT://$env:userdomain/$env:username"

With WinNT:, you can optionally hint the kind of object you are after to speed up connection times by resolving ambiguities:

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

To access your domain’s Administrator account, you’d do this:

$user = [ADSI]"LDAP://CN=Administrator,CN=Users,DC=scriptinternals,DC=technet"

Note the hard coded domain name in this ADsPath. You most likely will have to change that. In a second, you find ways to eliminate the need for hard coded domain names.

To doublecheck what the ADsPath of a given object is, use this:

$user.psbase.Path

Accessing objects via their built-in unique ADsPath is not always the best way. The last example showed clearly that your domain name is part of the ADsPath. So if you wanted to get access to the domain Admin account without hard-coding a specific domain name, you’d need other ways.

Picking An Object From A Container

Once you have access to the container an object lives in, you can pick the object out of the container. Let’s see how that works with local accounts first:

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

$user = $computer.psbase.Children.Find("Administrator")

$user.Description

Works.

Let’s try and get a service object from the local computer:

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

$service = $computer.psbase.Children.Find("Alerter")

$service | get-member

$service.Dependencies

Works, too. However, to be more precise, there may be occasions where you want to also specify the kind (class) of the object to pick:

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

$service = $computer.psbase.Children.Find("Alerter", "Service")

$service | get-member

$service.Dependencies

Note how Find accepts a second (optional) argument. That’s the kind of object you are after.

Can you play with services that way? Stop and start them? Yes you can:

$service.Start()

$service.Stop()

Unfortunately, though, you need to know that this is possible because get-member does not list the ADSI methods. We’ll look into the supported (but invisible) ADSI methods shortly when I cover the individual classes and their capabilities.

Now the same with the domain account – this time, we’ll try not to hard-code the domain name which of course requires that you are already logged on to a domain. If you are not logged on to a domain, replace the first line by one of the alternatives mentioned above when discussing the connection process.

$domain = [ADSI]""

$users = $domain.psbase.Children.Find("CN=Users")

$user = $users.psbase.Children.Find("CN=Administrator")

$user.Description

Now, as you see you would have to walk down the hierarchy using Find and connect to each container on your way to the container that holds the object you are after. That can be time-consuming. And sometimes, in a large directory, you simply don’t know in which container an object lives.

There are two solutions to that problem. One is cheating. You could connect to your Active Directory using the WinNT: moniker. Since its world is flat and not hierarchical, you would find anyone immediately. All users are organized in one huge container:

$domain = [ADSI]"WinNT://$env:userdomain,domain"

$user = $domain.psbase.children.Find("Administrator")

$user.Description

This can be a good solution, especially if you just want to change user properties that are accessible in the WinNT: world. However, most of the time, WinNT: is too limited and too slow for you.

Picking An Object Via GUID

There is a little-known third way of accessing LDAP objects. Each object is assigned a unique GUID number when it is created, and this GUID never changes. So once you know the GUID for an object, you can easily and quickly access it via GUID. I’ll show you how to find out an  objects’ GUID and how to bind to an object using its GUID a bit later.

Searching For Users (And Other Objects)

In larger directory environments, you need a more efficient way to find objects. One is the ADSI Searcher accessible from .NET.

The following get-ADSIUser function demonstrates how easy it is to find users:

Get-ADSIUser Administrator

Get-ADSIUser Ad*

Get-ADSIUser *mini*

(Get-ADSIUser Administrator).Path

And here’s the function:

function get-LDAPUser ($UserName)

{

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

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

$searcher.findall()

}

The important part takes place in the search filter. This filter determines what the search will look for. It uses the AD query syntax which looks a bit strange at first:

(&(objectClass=user)(sAMAccountName= $UserName))

Here, the search looks for objects of type (class) “User” where the property (or attribute) named “sAMAccountName” equals the name you specified. Note also that you can use “*” as wildcard.

The function looks for the “old” user names and could for example easily translate them into DS Syntax. That’s possible because the function returns all found objects, and you can then choose which object property you’d like to use:

(Get-ADSIUser Administrator).Properties.DistinguishedName

(Get-ADSIUser Administrator).Properties

Note: Just make sure your query results in exactly one match. If you get more than one match, you need to use a foreach-object loop to access the individual users returned. Or you could change your get-LDAPUser function altogether and replace FindAll() by FindOne().

Setting Search Options

As long as you expect only one or a few results from your search, you don’t need to worry about the page size. In its defaults, the search returns a maximum of 1000 results to prevent load on the domain controller executing the search.

If you must overcome this limit, you can change the Page Size like this:

function get-LDAPUser ($UserName)

{

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

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

$Searcher.CacheResults = $true

$Searcher.SearchScope = “Subtree”

$Searcher.PageSize = 1000

$searcher.findall()

}

By setting PageSize to anything, you in effect disable the limitation altogether, and that’s why:

If you don’t set PageSize, the searcher attempts to return all results in one chunk. If it gets larger than 1000 objects, the internal limitation kicks in and cancels the search. You get incomplete results.

If you do set a PageSize, then you get back the results in separate chunks, each with the size defined in PageSize. So, as long as your PageSize is smaller than or equal to 1000, the chunks will not trigger the limitation, and you get all results. You can try and set PageSize = 1. You still get all results. The results now come in chunks of exactly one entry which slows down the search. The best way therefore is to set PageSize to the maximum allowed size of 1000. This limit is set in your AD schema and may have been changed to another value.

Another property you can set is SearchScope. The default is “Subtree” which enables recursive searching from the root you specified. So if the root is the top of your domain, you find any object located anywhere.

Maybe you’d like to find only objects that are located in a specific container (like a specific organizationalUnit). With the search, you set the root to the container you want to use as start for your search:

$ADsPath = "LDAP://OU=Accounting,OU=Company," + ([ADSI]"").distinguishedName

$root = [ADSI]$ADsPath

$searcher = new-object DirectoryServices.DirectorySearcher($root)

$searcher.filter = "(objectClass=user)"

$Searcher.SearchScope = "OneLevel"

$searcher.findall()

This script would find all users located exactly in the OU “Accounting”. If you changed SearchScope back to “SubTree”, you’d also find users located in child OUs.

If you want to see all options available with the DirectorySearcher, simply output its properties:

$searcher

Understanding Search Results (and SearchResultCollections)

There is an  important difference between the result you get from your get-LDAPUser function we used earlier and the above code. Check out what happens when you try and access the properties of a user object returned by your search:

$user.properties

Nothing is returned. That’s strange because if you wrapped the code as function and returned the search results, the properties would show (as in the previous example). Why is that?

Actually, FindAll() returns a SearchResultCollection that can contain many results. Your function was smart enough to detect that the SearchResultCollection contained only one result, so it automagically extracted the one member of the collection and returned that as SearchResult.

Without the function wrapper, you get the raw SearchResultCollection and need to extract the SearchResult yourself:

$user[0].Properties

If you expect to get only one result anyway, you should therefore replace FindAll() with FindOne(). FindOne() returns exactly one (the first) match as a SearchResult:

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

$searcher.filter = "(&(objectClass=user)(sn=Weltner)(givenName=Tobias))"

$user = $searcher.FindOne()

$user.properties

Playing with Search Queries

Your search query determines what kind of objects the search returns. So it’s a good idea to better understand the search query. The previous search used this query:

(&(objectClass=user)(sAMAccountName=$UserName))

Each condition that needs to be met is enclosed in parenthesis. To combine more than one condition with a logical “AND” operator (all conditions need to be met), you place a “&” at the beginning and enclose that with another pair of parenthesis.

Selecting The Kind of Objects

The sample query returned all objects where objectClass equals “User” and the property (or attribute) “sAMAccountname” equals the content of your $UserName variable.

objectClass is a very important filter criteria because it tells the search engine what kind of object you are after. Each ADSI object has a objectClass property which really is an array and for user accounts contains something like top, person, organizationalPerson and user. The list goes from unspecific to most specific.

If you know you are looking for a user account (and not a contact), you use “user”. If you want your search to be of wider scope and also include other objects that may not be a user account but still a representation of a person (like a contact), you use “person” instead of “user”.

In fact, when you query for objectClass=User, you will not only get user accounts but also computer accounts. To limit the search to true user accounts, you would have to also include the objectCategory:

(&(objectCategory=person)(objectClass=User))

A much faster but much more unreadable way would be to filter for sAMAccounttype:

(sAMAccountType=805306368)

Adding More Search Criteria

You can then add as many other conditions to your query as you want. For that, you can use any property (or attribute) present in a given object. For example, to find a specific user by his name, you’d use this query:

(&(objectClass=user)(sn=Weltner)(givenName=Tobias))

To actually use this search query, you could change the get-LDAPUser function we used earlier appropriately, or you query directly:

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

$searcher.filter = "(&(objectClass=user)(sn=Weltner)(givenName=Tobias))"

$user = $searcher.findall()

Now that you know how to combine properties in your search query, it’s all just a matter of knowing the right properties and values. If for example you wanted to look up all users with “Allow Access” checked in their “Dial-in” tab, use this:

(&(objectCategory=person)(objectClass=user)(msNPAllowDialin=TRUE))

And this would return all users that need to set their password when they log on the next time:

(&(objectCategory=person)(objectClass=user)(pwdLastSet=0))

Advanced Queries: Looking For Empty Values And Times

In your queries, you can combine wildcards (“*”) and NOT operators (“!”). To find all computer objects with no description set, use this:

(&(objectCategory=computer)(!description=*))

Likewise, to find all users that have a description, use this:

(&(objectCategory=person)(description=*))

You can also compare values with greater and less operators. To list all objects created after a specific date like March 18, 2005, use this:

(&(objectCategory=person)(objectClass=user)(whenCreated>=20050318000000.0Z))

Finally, you can logically OR conditions. To check for all users with non-expiring accounts, accountExpires can either be “0” or 2^63-1, so let’s check for either one:

(&(objectCategory=person)(objectClass=user)(|(accountExpires=9223372036854775807)(accountExpires=0)))

Binary Comparisons: Doing The Math

Some attributes and properties are easy to evaluate. You can for example look for a specific string in the givenName attribute. Others are tricky. Some properties (like userAccountControl) are really bits, and each bit represents a special meaning. How can you check for individual bits in such a property?

Here’s an example why you would care in the first place. Maybe you want to find all user accounts that are currently disabled. Disabled accounts use bit 1 (0x02) in userAccountControl as flag.

Since this property contains many more bits with other meanings, you cannot simply check whether userAccountControl contains the value 2:

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

$searcher.filter = "(&(objectClass=user)(userAccountControl=2))"   # WRONG!

$user = $searcher.FindAll()

Instead, you need to check if the actual bit is set. To do that, you need LDAP matching rules which are pretty easy once you know how they work:

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

$searcher.filter = "(&(objectClass=user)( userAccountControl:1.2.840.113556.1.4.803:=2))"

$user = $searcher.FindAll()

1.2.840.113556.1.4.803 is the rule for a binary “AND”, so you use that if you want to make sure a specific bit is set. In this example, the bit was 0x02. If you changed that to 65536 (which is Bit 15), you would get all users with a non-expiring password. Look up userAccountControl if you want to know what all the other bits represent.

This would return all users not required to have a password:

(&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=32))

1.2.840.113556.1.4.804 is the rule for a binary OR. And if you wanted to negate your query, you use “!”. The next example lists all users with expiring passwords:

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

$searcher.filter = "(&(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=65536))"

$user = $searcher.FindAll()

Note that the NOT operator (“!”) can be placed either before the entire expression or before the property name. So you could also use this:

$searcher.filter = "(&(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=65536)))"

Now that you know how to check for bits, you could easily find all distribution groups:

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

Well, if you knew that they are missing a special bit in groupType, that is. But you are getting the idea. This is really very flexible. This search filter returns all computers that are not domain controllers:

(&(objectCategory=Computer)(!userAccountControl:1.2.840.113556.1.4.803:=8192))

Identifying Objects By GUID

When accessing objects, there is only one identifier that is really unique to the object: its GUID. The GUID is the only property that remains constant throughout the lifetime of an ADSI object. Anything else can be changed.

When you are dealing with a user object by name, the person may have married or changed names for other reasons. If you want to still be able to find that user account, you need to know its GUID.

How do you find an object GUID? To illustrate that, I need to explain a bit about objects. Unfortunately, with PowerShell you are dealing with all kinds of (different) objects that seem to be identical. In reality, though, PowerShells internal type adapter creates phantom objects that are all different.

When you access a user directly through its ADsPath, let’s see what you get:

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

$user1 = [ADSI]$ADsPath

$user1 | get-member

$user2 = $user1.psbase

$user2 | get-member

If you get back your user from a search query, you get a third object kind:

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

$searcher.filter = "(&(objectClass=user)(sAMAccountname=Administrator)"

$user3 = $searcher.FindOne()

$user3 | get-member

To sum it up, your user account can be represented by three different objects: a processed DirectoryEntry ($user1), a native .NET DirectoryEntry ($user2) and a SearchResult ($user3).

When you look for a property like the GUID, you may have to examine all three objects to find the one that gives access to that information.

And here’s how you get the GUID:

$user1.objectGUID

$user2.properties.objectGUID

$user3.properties.objectGUID

Now, does that mean that all three objects contain the same information just in different properties? No. objectGUID returns the GUID as individual bytes. We need a string GUID, though. Only one of the objects does contain the string GUID in its nativeGUID property:

$user2.nativeguid

So it really is important to know about all three objects. If you want to find out the GUID of an ADSI object, you can therefore use one of two approaches:

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

$user = [ADSI]$ADsPath

$user.psbase.nativeGUID

Or:

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

$searcher.filter = "(&(objectClass=user)(sAMAccountname=Administrator)"

$user = [ADSI]($searcher.FindOne()).Path

$user.psbase.nativeGUID

Note that the search actually returns a SearchResult object which is not a DirectoryEntry and does not contain the needed nativeGUID property. So in the previous example, I connected to the real user object by reading the Path name from the SearchResult object, then used that to connect to the real object:

$user = [ADSI]($searcher.FindOne()).Path

Here’s another way to do the same:

$user = ($searcher.FindOne()).getDirectoryEntry()

Once you know an objects’ GUID, you can use its GUID to find and access the object very quickly and regardless of any changes to its properties like name changes etc. with this syntax:

$object = [ADSI]"LDAP://GUID=<…>"

Here’s an example:

# find out GUID of user:

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

$user = [ADSI]$ADsPath

$guid = "LDAP://<GUID=" + $user.psbase.nativeGUID + ">"

# anytime later, use GUID to quickly bind to object

$userViaGuid = [ADSI]$guid

Note that in this example, the first part is only needed to find out the GUID for the user. Once you know that GUID, you can use it anytime later to access that user object because the GUID will never change.

Summary

Today, you have learned how to use the [ADSI] type accelerator to connect to a domain or to your local computer. You have also seen how you can authenticate yourself if your computer is not joined to a domain or if you’d like to connect to another domain/use different credentials.

You have discovered how you can access individual objects like user accounts:

  • Direct access via an objects’ path name
  • Direct access via an object GUID
  • Search for an object using DirectorySearcher

We also have covered in great detail how the ADSI query language really works. So now you can search for objects, use wildcards, logical operators and even check for dates or bits.

And you have seen the different object representations present in PowerShell. When you get an object, it has been processed by the PowerShell type adapter. Much of the original functionality has been disabled, and to use the full power of ADSI, you often have to access the underlying raw ADSI object using psbase.

In the following days, we take a much closer look at what you can actually do with the objects you have bound to today. We’ll look at changing properties, creating new objects like ne