PowerShell – less code, same result

One thing I have seen a lot of since I’ve been using PowerShell is that you can get the same tasks completed with less code.

Now with a bunch of the ADSI scripts we’ve gone through lately this isn’t blatantly obvious. These have been conversions of existing VBScripts that I have in my toolkit. However, this reduction in code has been highlighted really well in a recent script I converted.

When I create a new user account in Active Directory, I prefer to use a random password generator. Not particularly practical for creating one new account, but when you create say a hundred it becomes quite useful.

Ok, so here’s our first function:

—————————————
‘ Function fnRandomPassword
‘ Abstract Function to randomly generate a complex password
‘ Requirements None
‘ Parameters Integer Length of the password required.
‘ Return values random complex password to specified length
‘ Revision 1.2
‘—————————————
Public Function fnRandomPassword(imLength)

Dim smPassword,imRandValue, x
Dim imSecPoint(2), imMark
imMark = CInt(imLength/3)
‘ To meet complex password requirements we need at least 1 lower, 1 numb and 1 upper.
imSecPoint(0) = imMark
imSecPoint(1) = imSecPoint(0) + imMark
imSecPoint(2) = (imSecPoint(1) + imMark) -1
If imSecPoint(2) > imLength Then imSecPoint(2) = imSecPoint(2) -1
smPassword = “”
Randomize
Do while len(smPassword) < imLength
If Len(smPassword) = imSecPoint(0) Then
' Make sure this character is number
imRandValue = Int((128 * Rnd) + 1)
if imRandValue >= 48 and imRandValue <= 57 Then
smPassword = smPassword & Chr(imRandValue)
end if
ElseIf Len(smPassword) = imSecPoint(1) Then
' Make sure this character is an Lower case Alpha
imRandValue = Int((128 * Rnd) + 1)
if imRandValue >= 97 and imRandValue <= 122 Then
smPassword = smPassword & Chr(imRandValue)
end if
ElseIf Len(smPassword) = imSecPoint(2) Then
' Make sure this character is an Upper case Alpha
imRandValue = Int((128 * Rnd) + 1)
if imRandValue >= 65 and imRandValue <= 90 Then
smPassword = smPassword & Chr(imRandValue)
end if
Else
' Make the bugger complex :-)
imRandValue = Int((128 * Rnd) + 1)
' Due to using DCPromo.exe and HTML, we have to avoid: &<"\
if ((imRandValue => 35) And (imRandValue =< 37)) Or _
((imRandValue => 39) And (imRandValue <= 59)) Or _
((imRandValue >= 61) and (imRandValue <= 91)) Or _
((imRandValue >= 93) and (imRandValue <= 123)) then
smPassword = smPassword & Chr(imRandValue)
end if
End If
Loop
fnRandomPassword = smPassword

End Function

If you’ve played with the Randomize() Statement before then there’s nothing particularly difficult here.

We define three markers in our password where we’ll ensure that we meet complex password requirements. This falls over if the password is less then five characters, but as I usually use a minimum of eight it’s not a show stopper!

Most of the hard work in this function is testing that our random number falls within acceptable values. Then we use the Char function to produce the relevant character. If you haven’t played with Char before then Microsoft have a useful table here.

This is performed for each character up until the password length and then our resulting password is returned.

So now let’s compare our PowerShell equivalent:

function create-complexpassword
{
# *** Unable to generate complex password less then 5 chars ***
# ASCII data taken from http://msdn2.microsoft.com/en-us/library/60ecse8t(VS.80).aspx

Param (
[int]$PassLength
)

# Let’s work out where our 3 complex characters will be inserted in the password…
[int]$mark = ($PassLength/3)
$ComplexChar = @(“marker”)
$ComplexChar[0] = $mark
$ComplexChar = $ComplexChar+($ComplexChar[0] + $mark)
$ComplexChar = $ComplexChar+(($ComplexChar[1] + $mark) -1)

$Password = $null
$rnd = new-object random

# “i” is our counter while we make the password, one char at a time.
$i = $Password.length
do
{
switch ($Password.length)
{
$ComplexChar[0]
{
# Make this character a Numeric
$password = $password+([char]($rnd.next(48,57)))

}

$ComplexChar[1]
{
# Make this character a LowerAlpha
$password = $password+([char]($rnd.next(65,90)))
}

$ComplexChar[2]
{
# Make this character a Upper Alpha
$password = $password+([char]($rnd.next(97,122)))
}

default
{
# In case this is used in a DCPromo answer files, theres a few chars to
# avoid: Ampersand, Less than, double quote and back slash
$NextChar = $rnd.next(33,123)

switch ($nextChar)
{
34 {exit}
38 {exit}
60 {exit}
92 {exit}
default
{
$Password = $Password+([char]$nextChar)

}
}
}
}
$i++

}
Until ($Password.length -eq $PassLength)
return $Password

Apart from the code spacing within this function you can see that this is dramatically shorter than the VBScript counterpart. Now I guess this isn’t really a true PowerShell code reduction because we haven’t used a Cmdlet to reduce the code, this has been done using the .Net class Random and the Next method which allows us to specify some boundaries on our random number.

Personally, I think this still counts. What do you think? Do you have any good examples you’d like to share?

Searching Active Directory for Windows 2000 and Windows NT4 Domain Controllers with PowerShell

When I posted the article on raising AD functionality levels recently David Saxon pointed out that more could be done in regards to confirming the Domain Controllers are at the required level, i.e. they are Windows 2003 Domain Controllers.

Since we’ve looked at searching the Directory yesterday, this seems like a good time to take a closer look into this.
Microsoft have a good article here on what features are available depending on you functionality level.

So we’d start with:

$query = new-object system.directoryservices.directorysearcher
$root = [adsi]“”

And presuming our DC’s are located in the default location, we can narrow our search down:

$ou = [adsi](“LDAP://ou=Domain Controllers,”+$root.distinguishedName)
$query.SearchRoot = $ou
$query.SearchScope = “OneLevel”

The key to our search is going to be the filter() that we use. If you look around there’s a few different ways of identifying what we might want to search on.

In KB article 322692 there is a good filter for finding NT4 Domain Controllers:

$query.filter = “(&(objectCategory=computer)(operatingSystem Version=4*)(userAccountControl:1.2.840.113556.1.4.803:=8192))”

Here we’re filtering on the computer object, the OS version, and the UAC value. Let’s break this down a bit more. The first one is pretty obvious, the OS version is easy enough too. Anything with a 4 in the version relates to NT4. Windows 2000 is 5.0, and Windows 2003 is 5.2. So what’s with the UAC filter? The bitwise filter is an Object ID (OID) that means all the flags have to be set for a match. The value of 8192 specifies that the account is a Domain Controller.

We can search for Windows 2000 Domain Controllers using the following filter, as described in KB article 325379

$query.filter = “(&(objectCategory=computer)(primaryGroupID=516)(operatingSystemVersion=5.0*))”

Similar to the above example, we’re filtering on the computer object again. This time rather than use the UAC approach though we’ve used the PrimaryGroupID which we’ve touched on previously. The OS version corresponds to the versions discussed above.

It seems to me that our best approach would be to filter on any computer account, that’s not Windows 2003. As we’re only searching in the Domain Controllers OU we shouldn’t need to try and check the UAC or Primary Group information, but you could just to be sure if you felt that way inclined.

$query.filter = “(&(objectCategory=computer)(!operatingSystem Version=5.2*)(userAccountControl:1.2.840.113556.1.4.803:=8192))”

There’s a subtle difference between this filter and the first example. Basically we’re looking for OS version that’s not (!) equal to Windows 2003. In theory this should give us all the other Domain Controllers ;)

So to finish off then:

$result = $query.FindAll()
if ($result -eq $null)
{
return $null
}
else
{
foreach ($machine in $result)
{
$ADobject = $machine.GetDirectoryEntry()
write-host $ADobject.distinguishedName
}
}

Of course just because we have accounts in AD doesn’t actually mean that there are machines still out there. You could tie this into a function that then calls a ping test against the names that are retrieved. That’s still not gospel, but if it’s a DC, it’s more likely to be up if it’s still out there!

If you’re new to LDAP queries and would like more information Microsoft have a good primer here.

Searching Active Directory with PowerShell

Being quite the lazy admin that I am, whenever I’m scripting for AD I like to use short names when referring to objects. As an example, if I want to add the user BellA into the Domain Admins group I don’t want to provide the distinguishedName for both objects .

The easy solution to this is to have enough information to hand, to allow a quick rummage through the Directory, and let the script
retrieve the details it needs. In fact being able to extract information from an AD search is pretty much the corner stone of any AD scripting tool box, as you’ll see over the next couple of posts.

I’ll show you a couple of ways of searching and then mention a few points to be aware of regarding performance, and your impact on the servers whose workload you’re adding to.

# —————————————————————————————————
function Find-DN
# —————————————————————————————————
{
Param (
$sADobjectName,
$sADobjectType
)
$query = new-object system.directoryservices.directorysearcher
$root = [adsi]“”

$query.SearchRoot = $root
$query.filter = “(&(ObjectClass=$sADobjectType)(cn=$sADobjectName))”
$query.SearchScope = “subtree”
$result = $query.findOne()

if ($result -eq $null)
{
return $null
}
else
{
$ADobject = $result.GetDirectoryEntry()
return $ADobject.distinguishedName
}
}

There isn’t a lot of specific PowerShell code in the function above. Its pretty LDAP orientated. We have a simple function that returns you the distinguishedName of an object when you provide the objectClass and cn.
So to find the DN of my test user account:

Find-DN “BellA” “user”

The $result variable is a System.DirectoryServices.SearchResult which we cast into System.DirectoryServices.DirectoryEntry with the GetDirectoryEntry() method. I like this as it gives us a much nicer way of retrieving the objects properties.

The other method of searching AD I have seen, is similar, but with a PS slant to it:

# —————————————————————————————————
function Find-DN2
# —————————————————————————————————
{
Param (
$sADobjectName,
$sADobjectType
)
$query = new-object system.directoryservices.directorysearcher
$root = [adsi]“”

$query.SearchRoot = $root
$query.SearchScope = “subtree”

$ADobject = $query.findAll() | where-object {$_.properties.objectclass -eq $sADobjectType `
-and $_.properties.cn -eq $sADobjectName}
$props = $ADobject.Properties

if ($ADobject -eq $null)
{
return $null
}
else
{
foreach ($prop in $props)
{
return $prop.distinguishedname
}
}
}

Straight away you can see some PowerShell in the function above. Rather then use the $query.filter, we pipe the results into a where-object and then filter for our search criteria. We also handle the properties returned differently.

This function would be called exactly the same as our previous example:

Find-DN2 “BellA” “user”

Now I’ve had problems with this function. I’ve found it worked fine for “some” objects, but I have had trouble retrieving others. Despite the SearchScope being set to “SubTree” I’ve had it fail to retrieve objects that are a few OU’s deep. I suspect this is a flaw with my function as apposed to anything flawed in PS ;)

Search Tips
So now we’ve seen the basics, I thought it worth mentioning a couple of ways to improve on performance, and speed:

[1] Scope: If you can, narrow your SearchScope. There’s three options: “Base”, “OneLevel”, and “SubTree”. If you’re not sure, there’s a good explanation on MSDN here.

[2] Filter: Always try and provide a filter to reduce how much data you’re trying to retrieve. There’s obviously not much point retrieving every user account in your domain and enumerating through them all just for the one you want. Think about the Filter() method, and use the relevant FindOne() FindAll() methods.

I’m not sure how our second examples works with this. My hunch is that it retrieves ALL the objects and trawls through each with the where-object. I could be wrong here of course ;)

[3] Properties: If you’re only after say, the distinguishedName it would be a good idea to reduce the properties returned by using the PropertiesToLoad method.

The Microsoft Scripting Guy’s, have an example here

Managing group membership in Active Directory with PowerShell (Part 2)

Carrying on from where we left off yesterday; today I want to look at adding and removing users from groups.

To get us started we’ll bind to the two objects we’re going to be working with:

# Serverless bind to the domain, and define the root DN
$root = [adsi]“”
$rootdn = $root.distinguishedName

# Bind to our User and Group objects
$user = [adsi](“LDAP://cn=bella, ou=Test OU,”+$root.distinguishedName)
$group = [adsi](“LDAP://cn=Domain Admins, cn=Users,”+$rootdn)

If you have a VBScript/ADSI background then the example below will be familiar. We’ve basically just called the add method against the group object. The important thing to be aware of using this approach is that add method has to be constructed using the ADspath, which is structured by specifying the provider (“LDAP://”) and the distinguishedName of the object we’re adding (user or group).

# The classical VBS/ADSi approach
$group.add(“LDAP://”+$user.distinguishedName)
$group.SetInfo()

Alternatively we could use a PowerShell approach:

# PowerShell approach
$members = $group.member
$group.member = $members+$user.distinguishedName
$group.setinfo()

Here we basically retrieve the group members into the $members variable, and then add the full DN of the object we want to include. Then it’s just a case of writing the collection back to our group object.

If you’re making a lot of changes to an objects attributes, you’ll want to make sure that you rebind to the object after a SetInfo() call, otherwise your data will not be up to date, and you wont see the changes that have been made.

When it comes to removing members from a group it can become a little more complex.

So using the classic ADSI approach:

# Classic VBS/ADSI
$group.remove(“LDAP://”+$user.distinguishedName)
$group.SetInfo()

That’s not too difficult!

If we look at the PowerShell approach, we would take similar steps as adding to the group, only this time we’ll enumerate through the group membership, and for every membership that isn’t our user account will get added to the $newList variable which we’ll then write back to the attribute when we’ve finished.

# PS – harder
foreach ($member in $group.member)
{
# As long as it’s not our user DN add the existing member
# to the list of users we’re going to add back into the group.
if ($user.distinguishedname -ne $member)
{
$newlist = $newList+$member
}
}
$group.member = $newlist
$group.SetInfo()

On the flipside if you want to be a little more selective you could do:

# PS – Simple
$group.member = $group.member[0..3]
$group.SetInfo()

This would just put back the first 4 members into the group.

The thing that I find so powerful about PowerShell, apart from the simplicity of doing stuff (when you can work out the right syntax!), is that it is so flexible. We can perform the same actions in this example, two different ways, and as Marc (MOW) pointed out recently, you can also use the .Net framework, so that’s three different ways to perform the task!