Tuesday, September 30, 2008

Creating standard AD groups from a query using Powershell

Confusing title there.

We have a messaging application (it shall remain nameless) here which cannot deal with dynamic groups, a.k.a. query-based groups. It is only able to 'see' standard groups, i.e. those which have had their members manually added.

The users of the application want to be able to target messages to particular departments. Luckily enough, we have something like that recorded against each AD user. More to the point, it determines who pays for each user's printing, so, being associated with money, it is generally pretty accurate.

We have a large enough number of users, and a high enough rate of staff turnover to make manually creating (and in particular, manually maintaining) these departmental groups near-on impossible.

I've made a couple of Powershell scripts which use an LDAP query to get the correct members, then update the existing standard AD groups.

I got the method for updating the groups from here. It compares the current group membership to a text file - I simply modified this so the text file is created from an LDAP query.

Here's the script, with my comments interspersed below what I'm commenting on (lines beginning with '#' are comments within the script):

# Adds the Quest AD Management Powershell snap-in (this is REQUIRED
# in order to be able to manipulate AD groups):
#
# *** NOTE ***
# this section can be removed if the PowerShell Console file (*.psc1) being used
# (either as console, or called at command line when running this script from a
# Scheduled Task) includes the snapin below.
# *** **** ***
#Remove-PSSnapIn quest.activeroles.admanagement
#Add-PSSnapIn quest.activeroles.admanagement

These cmdlets, available from Quest, are invaluable for doing AD stuff with Powershell.

# Create a log file to record which users were added or removed
# from which groups:
$date = get-date -format yyyyMMddHHmm
$logfile = "[your log location]\" + $date + "added+removed.txt"

Always useful to have some logging, even if it's very basic like this.

# Function to determine whether an account is disabled. Takes the
# userAccountControl value of the current user as input:
Function isitdisabled
{
$num = $args

# Converts the number to hex:
$hexnum = "{0:X}" -f $num

# Checks if the last digit is a 2; assigns True/False accordingly:
if ($hexnum.substring($hexnum.length - 1,1) -eq 2)
{
$accountdisabled = $True
}
else
{
$accountdisabled = $False
}
return $accountdisabled
}

This is called later on - I don't want disabled accounts in my groups.

# Hash that holds the Department/Groupname key/value pairs:
$departmentshash = @{"[AD attribute value you're searching on](1)" = "[AD group to be modified](1)"; "[AD attribute value you're searching on](2)" = "[AD group to be modified](2)"; etc.}

$departments = $departmentshash.get_keys()

In my environment, I queried users based on their Department attribute. For example, if there are a set of users whose Department begins with "Info Tech", the key would be "Info Tech*" (asterisk for the wildcard in an LDAP search), and the value for that key might be "Info Tech Group".

You can have as many of these pairs as you like; next, the script loops through each pair:

foreach ($department in $departments)
{
# Search filter:
$strFilter = "(&(objectCategory=User)(Department=$department))"

This is where the LDAP query string is set. This is quite a simple one, they can get a lot more complicated, depending what you need. Since I have a couple of groups made up of users with completely disparately-named departments, I have another version of the script which performs an LDAP "OR" ("|()") search on two different department names.

$objDomain = New-Object System.DirectoryServices.DirectoryEntry("LDAP://[your domain here]")

I searched from the root of my domain, but you could restrict it to lower-level OUs.


$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.SearchRoot = $objDomain
$objSearcher.PageSize = 80000
$objSearcher.Filter = $strFilter

# Filters to get the name and department attributes for each user found:
$colProplist = "name", "department", "userAccountControl", "samaccountname"

foreach ($i in $colPropList)
{
$objSearcher.PropertiesToLoad.Add($i)
}

I didn't end up using all these properties - you might, and you might want to retrieve additional properties too.

# Performs the search:
$colResults = $objSearcher.FindAll()

# Loops through each user found:
foreach ($result in $colResults)
{
$user = $result.properties
# Converts the 'userAccountControl' property to Int32 (NOTE THE SQUARE BRACKETS):
[int]$useraccountcontrol = $user.useraccountcontrol[0]

# Checks if the current user is disabled (see 'isitdisabled' function at top):
$disab = isitdisabled $useraccountcontrol
if ($disab -ne $True)
{
$samid = $user.samaccountname[0]
$samids += ,($samid)
}
}

This is where the script uses the function to check whether each account is disabled. If it's not, it gets added to an array.

# Orders the samids alphabetically to save extraneous additions/removals:
$samids = $samids|sort
# Writes the current list of samids to a text file with the department name.
# Overwrites the existing file:
$deptforfile = $department
$deptforfile = $deptforfile.replace("*", "")
$deptforfile = $deptforfile.replace(" ", "")
$filename = "[location of your temp logs]" + $deptforfile + "users.txt"
$samids > $filename

This is where the accounts that are a result of the search are written to a text file. This isn't strictly necessary (could just use the $samids array), but I've left it in as I'm scared to break something that works.

# Clears the current array of samids in preparation for the next lot:
$samids = @()

# Creates an array for putting the members of the AD group into:
$members = @()

# Puts all the usernames from the current filename into an array:
$users = get-content $filename

# Gets the corresponding AD group name from the current department:
$groupname = $departmentshash.get_item($department)

# Populates an array with the members of the current AD group:
get-QADGroupMember $groupname | foreach {$members += ($_.SAMAccountName)}

# Sorts the array alphabetically to avoid extraneous additions/removals:
$members = $members|sort

This grabs the current membership of the AD group and adds it to an array.

# Compares the users in each of the arrays against each other:
$compare = compare-object $users $members

Compare-object goes through both arrays and sees what is in one, but not in the other. It appears to go through from beginning to end, and compare line 1 with line 1, line 2 with line 2 etc. This is why the arrays are sorted first - if not, users get removed and then re-added, which is a bit pointless.

# Loops through each differing item and adds or removes it from the AD group
# as necessary:
foreach ($user in $compare)
{
if ($user.SideIndicator -eq '<=')
{
get-QADUser ($user.InputObject) | Add-QADGroupMember $groupname
"Adding $($user.InputObject) to $groupname." >> $logfile
}
elseif ($user.SideIndicator -eq '=>')
{
Get-QADUser ($user.InputObject) | Remove-QADGroupMember $groupname
"Removing $($user.InputObject) from $groupname." >> $logfile
}
}
}

This last bit looks at each item in the array created by Compare-Object, and removes or adds items (users) from the AD group as necessary. Make sure the arrays are compared the right way round or you'll end up with the wrong users.

That's pretty much it.

I welcome emails and comments - let me know what I could do better (probably most of it!).