Populating A Test Lab with AD Objects Using Windows PowerShell

This script name is: Create-ADTestLabContent

The commenting is pretty self-explanatory. I ran across Dmitry Sotnikov’s blog post on creating demo Active Directory environments last fall. You can view that blog post here: http://dmitrysotnikov.wordpress.com/2007/12/14/setting-demo-ad-environments/

I liked his script and used it often, but found I needed something a bit more substatial to save me time while doing tests. So, I used his script as a “base” and have highly modified it since last fall. In January I began to make much more substantial changes to it with the goal of adding it to poshcode.org. You can view and copy the whole script here: http://poshcode.org/1666

In a local VM in VMware Player, I’m able to create all the empty containers, 50 users, accompanying groups and computers in about 2-3 minutes.

Many thanks to Dmitry on his encouragement on the improvement of this script.

Below is the full script minus the comments:

# ---------------------------------------------------------------------------
### Derived From='Dmitry Sotnikov - http://dmitrysotnikov.wordpress.com/2007/12/14/setting-demo-ad-environments/
### This script design uses the original script (base script) written by Dmitry Sotnikov. The script's
### original comments are included below. I am referring to Dmitry's script as "version 1.0"
###
### My goal is to standardize variables, functions and libraries such that the script is portable.
### This is so that I can place all files for PowerShell on an ISO file and re-use the content
### with as little modification as possible from test scenario to test scenario.
###
### My scripts folder is a directory copied from the ISO file. When I build a virtual environment,
### I bring up a completely configured and empty AD domain. I then attach the ISO to the VM and
### copy the "scripts" folder to the root of C:. I then drop in a default profile.ps1 into the
### WindowsPowerShell directory (the default All Users profile) and run this script.
###
### There is more work, yet to do; I want to "pare down" the functions so that the functions could be added to
### a functions.ps1 "library" file.
###
### The labs I set up for testing use an OU structure similar to the following:
###
### OU=DeptName -|
### |- Computers
### |- Groups
### |- Users
###
### The profile.ps1 sets up the PSDrive and then creates a variable to the provider. The profile.ps1
### script is in the root of the scripts directoy which is copied from the ISO file.
###
### Contents of the profile.ps1 file:
###
### New-PSDrive -name ScriptLib -psProvider FileSystem -root "C:\Scripts"
### $ScriptLib = 'ScriptLib:'
###
### The Scripts folder contains a subfolder named "LabSetup". The LabSetup folder contains this script,
### titled "Create-ADTestLabContent.ps1" and all of the text files necessary for creating the user
### objects, OU's, etc. You can create your own files and/or edit this script to match your file names.
### I've listed the contents of each file below.
###
### I deviated from the original text files from Dmitry's script.
### My goal was to have a "true" list of random names by utilizing the "select-random" written by
### Joel Bennett. This can be downloaded from poshcode.org. I found that the combination of the
### select-ramdom on the census files and parsing the extra data was extremely time consuming.
### I went to the census.org page for year 2000 and downloaded the top 1000 names spreadsheet.
### Then, I simply stripped off ALL of the extra data (first row and all columns after column A)
### and saved it as an ascii file called "surnames.txt". The link to that page is:
### http://www.census.gov/genealogy/www/data/2000surnames/index.html
###
### Additionally, I did something similar with the first names.
### I downloaded common male and female names from http://infochimps.org/collections/moby-project-word-lists
### Those files are named fgivennames.txt and mgivennames.txt. You can alternately download a text file
### of 21,000+ common given names from the same site instead of using the surnames from census.gov.
### However, for my testing, a sample of 1000 last names was sufficient for my needs.
###
### departments.txt - Name of each Department which will be both an OU, group, and the department
### property on user objects.
### ous.txt - Name of child-containers for each Department OU (Computers, Groups, Users).
### cities.txt - Names of cities I will use on user properties
### dist.all.last.txt - ASCII file of last names downloaded from the Census.gov website
### dist.male.first.txt - ASCII file of male first names downloaded from the Census.gov website
### dist.female.first.txt - ASCII file of female first names downloaded from the Census.gov website
###
### The descriptions of the deparments match the OU name. This differentiates them from the default
### containers created when AD is set up from those added by this script. This allows for easily removing
### containers and all child items quickly during testing.
###
### Requires ActiveRoles Management Shell for Active Directory. This script will check
### for the snapin and add the snapin at runtime.
###
### History
### changes 01/08/2010 - version 2.0
### - Change Display name and full name properties to format of Lastname, Firstname
### - Change password to p@ssw0rd
### Changes 01/11/2010 - version 2.1
### - Assume base config of empty domain. Create variable for root domain name
### - make sure not attempt is made to duplicate usernames
### - Create containers
### Changes 02/19/2010 - version 2.2
### - added function to create empty departmental OUs and child containers for users, groups and computers
### Changes 02/22/2010 - version 2.3
### - added computer account creation to occur when the user is added
### - dot source functions.ps1
### - added Joel Bennett's select-random v2.2 script to functions.ps1. functions.ps1 in root of scripts folder
### Changes 02/23/2010
### - Made script more readible by using word-wrap
### - Cleaned up description and commenting
### Changes 02/24/2010 - Version 2.4
### - Using new ascii files for first and given names (see notes)
### - Removed original lines for parsing census.gov files
### Changes 02/25/2010
### - added better description for containers added via script to differentiate them to account for
### manually added containers
### - fixed issue with computer object creation - computer objects weren't always getting created
###
### Original Script name: demoprovision.ps1
##################################################
### Script to provision demo AD labs
### (c) Dmitry Sotnikov, xaegr
### Requires AD cmdlets
##################################################
###
### set folder in which the data files are located
### this folder should contain files from
### http://www.census.gov/genealogy/names/names_files.html
### as well as cities.txt and departments.txt with the
### lists of cities and departments for the lab
# ---------------------------------------------------------------------------</code>

#Load Function Library
. $ScriptLib\functions.ps1

# function to create empty OUs
function create-LabOUs (){
# Create Each Dept OU
for ($i = 0; $i -le $DeptOUs.Length - 1; $i++){
$OUName = "Test Lab Container - " + $DeptOUs[$i]
$CreateDeptOU += @(new-QADObject -ParentContainer $RootDomain.RootDomainNamingContext `
-type 'organizationalUnit' -NamingProperty 'ou' -name $DeptOUs[$i] -description $OUName )
}

# Create Child OUs for each Dept
foreach ($DeptOU in $CreateDeptOU){
for ($i = 0; $i -le $ChildOUs.Length - 1; $i++){
new-qadObject -ParentContainer $DeptOU.DN -type 'organizationalUnit' -NamingProperty 'ou' `
-name $ChildOUs[$i]
}
}
}

function New-RandomADUser (){
# set up random number generator
$rnd = New-Object System.Random

# pick a male or a female first name
if($rnd.next(2) -eq 1) {
$fn = $firstm[$rnd.next($firstm.length)]
} else {
$fn = $firstf[$rnd.next($firstf.length)]
}
# random last name
$ln = $last[$rnd.next($last.length)]

# Set proper caps
$ln = $ln[0] + $ln.substring(1, $ln.length - 1).ToLower()
$fn = $fn[0] + $fn.substring(1, $fn.length - 1).ToLower()

# random city and department
$city = $cities[$rnd.next($cities.length)]
$dept = $depts[$rnd.next($depts.length)]

$SName = ($fn.substring(0,1) + $ln)

# set user OU variable
switch ($dept){
$DeptContainers[0].name {$UserOU = Get-QADObject -SearchRoot $DeptContainers[0].DN | `
where { $_.DN -match "Users" -and $_.Type -ne "user" }}
$DeptContainers[1].name {$UserOU = Get-QADObject -SearchRoot $DeptContainers[1].DN | `
where { $_.DN -match "Users" -and $_.Type -ne "user" }}
$DeptContainers[2].name {$UserOU = Get-QADObject -SearchRoot $DeptContainers[2].DN | `
where { $_.DN -match "Users" -and $_.Type -ne "user" }}
$DeptContainers[3].name {$UserOU = Get-QADObject -SearchRoot $DeptContainers[3].DN | `
where { $_.DN -match "Users" -and $_.Type -ne "user" }}
$DeptContainers[4].name {$UserOU = Get-QADObject -SearchRoot $DeptContainers[4].DN | `
where { $_.DN -match "Users" -and $_.Type -ne "user" }}
$DeptContainers[5].name {$UserOU = Get-QADObject -SearchRoot $DeptContainers[5].DN | `
where { $_.DN -match "Users" -and $_.Type -ne "user" }}
$DeptContainers[6].name {$UserOU = Get-QADObject -SearchRoot $DeptContainers[6].DN | `
where { $_.DN -match "Users" -and $_.Type -ne "user" }}
$DeptContainers[7].name {$UserOU = Get-QADObject -SearchRoot $DeptContainers[7].DN | `
where { $_.DN -match "Users" -and $_.Type -ne "user" }}
$DeptContainers[8].name {$UserOU = Get-QADObject -SearchRoot $DeptContainers[8].DN | `
where { $_.DN -match "Users" -and $_.Type -ne "user" }}
$DeptContainers[9].name {$UserOU = Get-QADObject -SearchRoot $DeptContainers[9].DN | `
where { $_.DN -match "Users" -and $_.Type -ne "user" }}
}

# Check for account, if not exist, create account
if ((get-qaduser $SName) -eq $null){
# Create and enable a user
New-QADUser -Name "$ln`, $fn" -SamAccountName $SName -ParentContainer $UserOU -City $city `
-Department $dept -UserPassword "p@ssw0rd" -FirstName $fn -LastName $ln -DisplayName "$ln`, $fn" `
-Description "$city $dept" -Office $city | Enable-QADUser
}

# set group OU variable
switch ($dept){
$DeptContainers[0].name {$GroupOU = Get-QADObject -SearchRoot $DeptContainers[0].DN | `
where { $_.DN -match "Groups" -and $_.Type -ne "group" }}
$DeptContainers[1].name {$GroupOU = Get-QADObject -SearchRoot $DeptContainers[1].DN | `
where { $_.DN -match "Groups" -and $_.Type -ne "group" }}
$DeptContainers[2].name {$GroupOU = Get-QADObject -SearchRoot $DeptContainers[2].DN | `
where { $_.DN -match "Groups" -and $_.Type -ne "group" }}
$DeptContainers[3].name {$GroupOU = Get-QADObject -SearchRoot $DeptContainers[3].DN | `
where { $_.DN -match "Groups" -and $_.Type -ne "group" }}
$DeptContainers[4].name {$GroupOU = Get-QADObject -SearchRoot $DeptContainers[4].DN | `
where { $_.DN -match "Groups" -and $_.Type -ne "group" }}
$DeptContainers[5].name {$GroupOU = Get-QADObject -SearchRoot $DeptContainers[5].DN | `
where { $_.DN -match "Groups" -and $_.Type -ne "group" }}
$DeptContainers[6].name {$GroupOU = Get-QADObject -SearchRoot $DeptContainers[6].DN | `
where { $_.DN -match "Groups" -and $_.Type -ne "group" }}
$DeptContainers[7].name {$GroupOU = Get-QADObject -SearchRoot $DeptContainers[7].DN | `
where { $_.DN -match "Groups" -and $_.Type -ne "group" }}
$DeptContainers[8].name {$GroupOU = Get-QADObject -SearchRoot $DeptContainers[8].DN | `
where { $_.DN -match "Groups" -and $_.Type -ne "group" }}
$DeptContainers[9].name {$GroupOU = Get-QADObject -SearchRoot $DeptContainers[9].DN | `
where { $_.DN -match "Groups" -and $_.Type -ne "group" }}
}

# Create groups for each department, create group if it doesn't exist
if ((get-QADGroup $dept) -eq $null){
New-QADGroup -Name $dept -SamAccountName $dept -ParentContainer $GroupOU -Description "$dept Users"
}

# Add user to the group based on their department
Get-QADUser $SName -SearchRoot $UserOU | Add-QADGroupMember -Identity { $_.Department }

# set computer OU variable
switch ($dept){
$DeptContainers[0].name {$ComputerOU = Get-QADObject -SearchRoot $DeptContainers[0].DN | `
where { $_.DN -match "Computers" -and $_.Type -ne "computer" }}
$DeptContainers[1].name {$ComputerOU = Get-QADObject -SearchRoot $DeptContainers[1].DN | `
where { $_.DN -match "Computers" -and $_.Type -ne "computer" }}
$DeptContainers[2].name {$ComputerOU = Get-QADObject -SearchRoot $DeptContainers[2].DN | `
where { $_.DN -match "Computers" -and $_.Type -ne "computer" }}
$DeptContainers[3].name {$ComputerOU = Get-QADObject -SearchRoot $DeptContainers[3].DN | `
where { $_.DN -match "Computers" -and $_.Type -ne "computer" }}
$DeptContainers[4].name {$ComputerOU = Get-QADObject -SearchRoot $DeptContainers[4].DN | `
where { $_.DN -match "Computers" -and $_.Type -ne "computer" }}
$DeptContainers[5].name {$ComputerOU = Get-QADObject -SearchRoot $DeptContainers[5].DN | `
where { $_.DN -match "Computers" -and $_.Type -ne "computer" }}
$DeptContainers[6].name {$ComputerOU = Get-QADObject -SearchRoot $DeptContainers[6].DN | `
where { $_.DN -match "Computers" -and $_.Type -ne "computer" }}
$DeptContainers[7].name {$ComputerOU = Get-QADObject -SearchRoot $DeptContainers[7].DN | `
where { $_.DN -match "Computers" -and $_.Type -ne "computer" }}
$DeptContainers[8].name {$ComputerOU = Get-QADObject -SearchRoot $DeptContainers[8].DN | `
where { $_.DN -match "Computers" -and $_.Type -ne "computer" }}
$DeptContainers[9].name {$ComputerOU = Get-QADObject -SearchRoot $DeptContainers[9].DN | `
where { $_.DN -match "Computers" -and $_.Type -ne "computer" }}
}

# Create a computer account for the user
if ((get-qadcomputer "$SName-Computer") -eq $null){
New-QADComputer -Name "$SName-Computer" -SamAccountName "$SName-Computer" -ParentContainer `
$ComputerOU -Location "$city $dept"
}
}
Start-Transcript c:\ADTestLabContent.txt
$TestQADSnapin = get-pssnapin | where { $_.Name -eq "Quest.ActiveRoles.ADManagement"}
if($TestQADSnapin -eq $null){
add-pssnapin -Name Quest.ActiveRoles.ADManagement -ErrorAction SilentlyContinue
}

# number of accounts to generate - edit
$num = 50

# Read root domain text
$RootDomain = Get-QADRootDSE

# Read all text data
# OU's to create
$DeptOUs = @(Get-Content "$ScriptLib\LabSetup\Departments.txt")
$ChildOUs = @(Get-Content "$ScriptLib\labsetup\ous.txt")
# read department and city info
$cities = Get-Content C:\scripts\LabSetup\Cities.txt
$depts = Get-Content C:\scripts\LabSetup\Departments.txt

# read name files
# randomly select names from census files
# Use Joel Bennet's select-random v 2.2; saved in functions.ps1
1..$num | ForEach-Object {
$last += @(Get-Content C:\scripts\LabSetup\surnames.txt | select-random)
$firstm += @(Get-Content C:\scripts\LabSetup\mgivennames.txt | select-random)
$firstf += @(Get-Content C:\scripts\LabSetup\fgivennames.txt | select-random)
}

# Let's do the work

# Create OUs first - call function
create-LabOUs

# Retrieve all newly created OU DN's for use in next function
$DeptContainers = @(Get-QADObject -Type "organizationalUnit" | where {$_.Name -ne "Computers" -and $_.Name `
-ne "Groups" -and $_.Name -ne "Users" -and $_.Description -match "Test Lab Container"})

foreach ($item in $DeptContainers){
$item.description
}
# Create users, create dept groups
1..$num | ForEach-Object { New-RandomADUser }

Stop-Transcript
trap{
Write-Host "ERROR: script execution was terminated.`n" $_.Exception.Message
break
}
Advertisements

A Case of Humble Pie – Expect the Unexpected! Part 2

I am always open to learning new things. Let me know if there’s a different or better way to do something. What would you do different? What other safeguards would you add? etc.

Ok, now let’s look at some improvements to that previous script.

First, let’s define what changes we want to make to accomplish those improvements.

  1. As a precautionary measure, let’s set the action preference to always stop. Even though we are trapping errors, let’s ensure that we stop when an error occurs. This is important if we are dealing with an admin script that is making changes in a production AD environment. We can improve upon this later, but for now, let’s just make sure that if there is an error, immediately stop. This way there is no chance of any further actions that could make any uinwanted changes. Check out Joel Bennett’s blog post on trapping: http://huddledmasses.org/trap-exception-in-powershell/
  2. This is a bit “extra”, but helps protect against typographical error issues. Let’s ensure that when the script executes, that the input from Read-Host isn’t null, is not a wildcard, doesn’t contain spaces, and is not less than 3 characters. Why not less than 3 characters? This is a bit over-kill, but in my environment, I cannot forsee a username that is 3 characters or less. With a larger environment you potentially could have a username of 3 characters (I can think of many if I included Asian names).
  3. Let’s protect against making any changes against a Domain Admin account. The assumption is, to maintain security and protect your admin access, that changes to admin accounts are always performed manually.
  4. Let’s make sure we’re not making changes from the root of AD. Let’s limit the script to a specific container. This will limit the scope of any changes. This assumes the presence of a single user’s container in a small environment. This is a bit of overkill to a certain extent, but is a simple way to protect your root Users OU and BuiltIn OU from being affected by any changes. Again, let’s assume we want to make changes to these OU’s manually. Obviously, changes would have to be made where there are several user OU’s.
  5. Let’s make sure only 1 user is ever changed via this script. This protects against any unwanted “mass” changes in Active Directory. The intention of the original script was to act on one user at a time, so this requirement is mandatory.
  6. Let’s also ensure that the date entered from Read-Host is a valid date.
  7. Finally, for any issues encountered with the account name entered from Read-Host, let’s create a quick function to inform the user and stop the script. Let’s do the same thing for an incorrect date.

Our first requirement is an easy change. Right after the functions, let’s insert the global error action value.

# set the global error action preference
$ErrorActionPreference="stop"

For the next change, we’ll look to our last item, #7. This is quite easy to implement. We’ll create two functions to stop the script if A) there is an issue with the account name and B) there is an issue with the date. These we’ll add as the last two functions.

function Stop-UserIsBad (){
Write-warning "Problem with the username."
exit
}

function Stop-DateIsBad (){
Write-warning "Problem with the date entered."
exit
}

Pretty simple here. We’re keeping the message short and we’re limiting the information displayed back to the user and we exit the script.

Next, let’s harness the power of PowerShell and tackle the typos by combining several of these into one statement.

#Get username input. Make sure username is not null, not wildcard and not less than 3 characters and is void of spaces
$Username = Read-Host "Enter a username"
if (($Username -eq $null) -or ($Username -eq "*") -or ($Username.Length -lt 3) -or ($Username -match [char]32)){
Stop-UserIsBad
}

Here we are making sure the value from Read-Host isn’t null, isn’t a wildcard, isn’t less than 3 characters and doen’t contain spaces. Again, this is pretty simple and applies my KISS principle. If any of the conditions exist, we are using the Stop-UserIsBad function to halt the script.

Next, let’s limit the scope and root of our search of the account in Active Directory:

#Get the user account, filter the query first liming search scope and root and that user is not a domain admin
$ADAccount = Get-QADuser $Username -SearchScope 'base' -SearchRoot $UserOU

Here we’re using the parameters of Get-QADUser to limit the search scope and defining the search root. We’ll include the $UserOU variable in the final script.

The last two items are making sure the date is valid and making sure the account is not a Domain Admin. We’ll implement this around the actions performed by the script.

#If a value was returned after filtering proceed, but make sure the user isn't a DA
if (!($ADAccount.memberOf -contains $DAGroup) -and $ADAccount -ne $null){</code>

$TermDate = Read-Host "Enter Last Date of Employment (MM/DD/YYYY)"
#Make sure date is valid
if (($TermDate -as [DateTime]) -eq $null){
Stop-DateIsBad
}

$NewPassword = Start-PasswordGenerator

#Make sure user exists and not typo

Write-Warning "Contact a CISCO Admin to check AND terminate VPN sessions for $ADAccount before continuing."
Write-Host ""
Start-Pause

#Disable user
Disable-QADUser $ADAccount -Confirm:$TRUE | Out-Null

#Hide From GAL (uncomment for production script)
Set-QADUser $ADAccount -oa @{'msExchHideFromAddressLists'=$True} | Out-Null

#Remove group membership for Remote Access"
$ADAccount.memberOf | where {$_ -eq "$RAGroup"} | Remove-QADGroupMember -member $ADAccount | Out-Null
$ADAccount.memberOf | where {$_ -eq "$RASGroup"} | Remove-QADGroupMember -member $ADAccount | Out-Null

#disable VPN and remote access property
Get-QADUser $ADAccount -IncludedProperties msNPAllowDialin | Set-QADObject -ObjectAttributes @{msNPAllowDialin=$false} | Out-Null

#Generate new random 12 character password and change account password
Get-QADUser $ADAccount | Set-QADUser -UserPassword $NewPassword | Out-Null
}
else {
Stop-UserIsBad
}

Ok, let’s pull out those individual changes and look at them.

if (!($ADAccount.memberOf -contains $DAGroup) -and $ADAccount -ne $null)

We’re doing a final check to make sure the account is not a member of the domain admins group. The $DAGroup variable will be included in our final script. But, please note, as an improvement you might want to include additional checks to ensure the account isn’t a domain admin.

We’re also making sure that the $ADAccount variable still contains a valid user. This is an extra check. It may or may not be necessary but is a fail-safe to make sure it’s still valid. If it’s null or a member of the Domain Admins group, we use the Stop-UserIsBad function.

Finally, we made sure the date was valid.

$TermDate = Read-Host "Enter Last Date of Employment (MM/DD/YYYY)"
#Make sure date is valid
if (($TermDate -as [DateTime]) -eq $null){
Stop-DateIsBad
}

Once again, this is pretty simple. Make sure the string can be converted into a valid DateTime format. If the conversion fails, no value is returned and we use our Stop-DateIsBad function.

Here is the final script:

#Functions

#Pause Function
function Start-Pause ($Message = "Press any key to continue...")
{
Write-Host -NoNewLine $Message
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
Write-Host ""
}

#Generate password function
#Modified from script written by Derek Mangrum
#http://grinding-it-out.blogspot.com/2008/10/generating-passwords-with-powershell.html
function Start-PasswordGenerator{
$charsToUse = "4"
$lCase = 'abcdefghijklmnopqrstuvwxyz'
$uCase = $lCase.ToUpper()
$nums = '1234567890'
$specChars = '!@#$%^&amp;*()_+-={}[]'

$charsToUse += $lCase
$regexExp += "(?=.*[$lCase])"

$charsToUse += $uCase
$regexExp += "(?=.*[$uCase])"

$charsToUse += $nums
$regexExp += "(?=.*[$nums])"

$charsToUse += $specChars
$regexExp += "(?=.*[\W])"

$test = [regex]$regexExp
$rnd = New-Object System.Random

do
{
$NewPassword = $null
for ($i = 0 ; $i -lt "12" ; $i ++ ){
$NewPassword += $charsToUse[($rnd.Next(0,$charsToUse.Length))]
$seed = ([system.Guid]::NewGuid().GetHashCode())
$rnd = New-Object System.Random ($seed)
}
}
until ($NewPassword -match $test)

return $NewPassword
}

function Stop-UserIsBad (){
Write-warning "Problem with the username."
exit
}

function Stop-DateIsBad (){
Write-warning "Problem with the date entered."
exit
}

#Variables
# set at the global error action preference
$ErrorActionPreference="stop"

#Remove test lab varabiles for production script
$UserOU = "adtest.local/Test/Users"
$DAGroup = 'CN=Domain Admins,CN=Users,DC=adtest,DC=local'
$RAGroup = 'CN=Remote Access,OU=Groups,OU=Test,DC=adtest,DC=local'
$RASGroup = 'CN=RAS Access,OU=Groups,OU=Test,DC=adtest,DC=local'

#Test for QAD Snapin
$TestQADSnapin = $null
$TestQADSnapin = get-pssnapin | where { $_.Name -eq "Quest.ActiveRoles.ADManagement"}
if(-not $TestQADSnapin){add-pssnapin -Name Quest.ActiveRoles.ADManagement}

Clear-Host

#Get username input. Make sure username is not null, not wildcard and not less than 3 characters and is void of spaces
$Username = Read-Host "Enter a username"
if (($Username -eq $null) -or ($Username -eq "*") -or ($Username.Length -lt 3) -or ($Username -match [char]32)){
Stop-UserIsBad
}

#Get the user account, filter the query first liming search scope and root and that user is not a domain admin
$ADAccount = Get-QADuser $Username -SearchScope 'base' -SearchRoot $UserOU

#If a value was returned after filtering proceed, but make sure the user isn't a DA
if (!($ADAccount.memberOf -contains $DAGroup) -and $ADAccount -ne $null){

$TermDate = Read-Host "Enter Last Date of Employment (MM/DD/YYYY)"
#Make sure date is valid
if (($TermDate -as [DateTime]) -eq $null){
Stop-DateIsBad
}

$NewPassword = Start-PasswordGenerator

#Make sure user exists and not typo

Write-Warning "Contact a CISCO Admin to check AND terminate VPN sessions for $ADAccount before continuing."
Write-Host ""
Start-Pause

#Disable user
Disable-QADUser $ADAccount -Confirm:$TRUE | Out-Null

#Hide From GAL (uncomment for production script)
Set-QADUser $ADAccount -oa @{'msExchHideFromAddressLists'=$True} | Out-Null

#Remove group membership for Remote Access"
$ADAccount.memberOf | where {$_ -eq "$RAGroup"} | Remove-QADGroupMember -member $ADAccount | Out-Null
$ADAccount.memberOf | where {$_ -eq "$RASGroup"} | Remove-QADGroupMember -member $ADAccount | Out-Null

#disable VPN and remote access property
Get-QADUser $ADAccount -IncludedProperties msNPAllowDialin | Set-QADObject -ObjectAttributes @{msNPAllowDialin=$false} | Out-Null

#Generate new random 12 character password and change account password
Get-QADUser $ADAccount | Set-QADUser -UserPassword $NewPassword | Out-Null
}
else {
Stop-UserIsBad
}

trap{
Write-Host "ERROR: script execution was terminated.`n" $_.Exception.Message
break
}

A Case of Humble Pie – Expect the Unexpected! Part 1

First, the title of this post needs some explanation. Sometimes you have to eat humble pie. There are times when the unexpected things will happen. This post is about learning from that unexpected event in my life. Mistakes happen, we’re all human. Sometimes the best knowledge we gain is from cleaning up the occasional mess.

I developed a script to perform several actions we take to disable users quickly in the event their departure from the company is immediate.

The purpose of the script is to:

A) disable the user account quickly
B) disable user accounts in a consistent and repeatable process
C) hide the account from the Exchange Global Address List
D) Remove the account from all remote access privilege groups
E) create a random password and set the new password on the account
F) Set an extended property which records the employee’s last date of employment (functions not shown in examples)

Here’s this script in it’s basic format:

function Start-Pause ($Message = "Press any key to continue...")
{
Write-Host -NoNewLine $Message
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
Write-Host ""
}

#Generate password function
#Modified from script written by Derek Mangrum
#http://grinding-it-out.blogspot.com/2008/10/generating-passwords-with-powershell.html
function Start-PasswordGenerator{
$charsToUse = "4"
$lCase = 'abcdefghijklmnopqrstuvwxyz'
$uCase = $lCase.ToUpper()
$nums = '1234567890'
$specChars = '!@#$%^&amp;*()_+-={}[]'

$charsToUse += $lCase
$regexExp += "(?=.*[$lCase])"

$charsToUse += $uCase
$regexExp += "(?=.*[$uCase])"

$charsToUse += $nums
$regexExp += "(?=.*[$nums])"

$charsToUse += $specChars
$regexExp += "(?=.*[\W])"

$test = [regex]$regexExp
$rnd = New-Object System.Random

do
{
$NewPassword = $null
for ($i = 0 ; $i -lt "12" ; $i ++ ){
$NewPassword += $charsToUse[($rnd.Next(0,$charsToUse.Length))]
$seed = ([system.Guid]::NewGuid().GetHashCode())
$rnd = New-Object System.Random ($seed)
}
}
until ($NewPassword -match $test)

return $NewPassword
}

$UserOU = "adtest.local/Test/Users"
$DAGroup = 'CN=Domain Admins,CN=Users,DC=adtest,DC=local'
$RAGroup = 'CN=Remote Access,OU=Groups,OU=Test,DC=adtest,DC=local'
$RASGroup = 'CN=RAS Access,OU=Groups,OU=Test,DC=adtest,DC=local'

#Test presense of Quest commandlets snapin.
$TestQADSnapin = $null
$TestQADSnapin = get-pssnapin | where { $_.Name -eq "Quest.ActiveRoles.ADManagement"}
if(-not $TestQADSnapin){add-pssnapin -Name Quest.ActiveRoles.ADManagement}

Clear-Host

$Username = Read-Host "Enter a username"
$TermDate = Read-Host "Enter Last Date of Employment (MM/DD/YYYY)"
$ADAccount = Get-QADUser $Username
$NewPassword = Start-PasswordGenerator

#Make sure user exists and not typo
if ($ADAccount){
Write-Warning "Check AND terminate VPN sessions for $ADAccount before continuing."
Write-Host ""
Start-Pause

#Disable user
Disable-QADUser $ADAccount -Confirm:$TRUE | Out-Null

#Hide From Exchange global address list
Set-QADUser $ADAccount -oa @{'msExchHideFromAddressLists'=$True} | Out-Null

#Remove group membership for Remote Access"
$ADAccount.memberOf | where {$_ -eq $RAGroup} | Remove-QADGroupMember -member $ADAccount | Out-Null
$ADAccount.memberOf | where {$_ -eq $RASGroup} | Remove-QADGroupMember -member $ADAccount | Out-Null

#disable VPN and remote access property
Get-QADUser $ADAccount -IncludedProperties msNPAllowDialin | Set-QADObject -ObjectAttributes @{msNPAllowDialin=$false} | Out-Null

#Generate new random 12 character password and change account password
Get-QADUser $ADAccount | Set-QADUser -UserPassword $NewPassword | Out-Null
}

trap{
Write-Host "ERROR: script execution was terminated.`n" $_.Exception.Message
break
}

First, a few things. I enjoy reading examples of how other people do things. I typically will take snippets of code and use PowerShell Plus to break things down and use the debugger to engineer what I want to accomplish. The script was tested in a test Active Directory lab which is a single 2003 AD VM I use on my machine. I always extensively use -WhatIf during development even in a test environment.

Once in production, the script ran fine. It had been used for a couple of months with no issues.

Then, one day, when the script was run it started resetting passwords on ALL users in the domain. Definitely NOT good! The good news was the issue was caught and stopped relatively quickly. In the end, there were several hours of humble pie diet on my part and extra time I spent researching the problem and making the changes to prevent the issue in the future.

From this event, I will say that it pays to have a password vault for your test and service accounts. I will post on that subject and a recommendation on a product at a later time. I’ll also have a post later about a modified script taken from Dmitry Sotnikov’s blog on creating an AD Lab.

In a test environment in VMware, I was never able to reproduce the issue/circumstances of how this happened. I’m continuing to work on that to find out why. For now, the “how” remains a mystery.

However, with this event fresh in my memory and still leaving that bad taste of humble pie in my mouth, I began to see several design flaws when using this in a production environment. It only took a few minutes of analyzing how the script worked to come to that conclusion! Lesson learned!

So, on with the show…

Where are the safeguards? As you look at the sample script, you’ll realize there’s only one. That is the confirm parameter on the Disable-QADUser commandlet. Ouch!

There are other problems as well:

  • There’s no control. What prevents the user from entering a wildcard? Nothing. The confirm parameter will return the entire list of users from the Get-QADUser commandlet. That should be enough to alert someone. But, why not try and prevent it?
  • There should also be some sort of control over what containers are queried and acted upon; i.e., protect your builtin and root Users OU container.
  • There should be control over how many accounts are returned from the Get-QADUser commandlet to make sure that one and ONLY one user is ever modified
  • There should be limitations on accidentally modifying any accounts which are members of the Domain Admins group.
  • There should be some way to validate the date entered. In my example, the individual executing the script is entering the date as a string. We should still validate that date to make sure it’s properly formatted and not a typo.
  • There should be some sort of control to ultimately STOP the script… bring it to a screeching halt if there are any exceptions or issues with the information the script was provided. For security reasons, the reason displayed for the termination of the script should be brief.

From a rudimentary standpoint, this would be the basic set of requirements we should focus on. There are probably additional things we could add later (such as returning the state of the account once the action has been completed to display the results of the action), but those types of improvements can be added later. My primary concern was to add those items which would be “safeguards” against unintentional actions, typos, etc.

Part 2 will show the new version of the script with these safeguards implemented.