User Termination (Part 2)
In this 2 part series I will show you how to build an automated and audit-friendly user termination process. In Part 1, I showed you how to get the users into a centralized location and execute basic user termination actions. For Part 2, we will fully automate the process of the user being deleted from the organization, while backing up their email, home directories, and providing email logs.
Disclaimer: This script could have a very negative impact on your organization. As such, I am not responsible for any damage that may come from using this script. You should take great care when implementing anything that will be destroying/altering data without interaction.
Addons / Things to know
- buy genuine Seroquel online !Important: You will need DotNetZip to use this script properly. You can find the dll that we’ll use here: Ionic.Zip.dll
- An Exchange account will need to be created for mailflow purposes.
- Create a second OU for termed users who have encountered an error. If errors occur, a person will need to correct them.
Scenario
HR has submitted the termination request for the user Test User. In Part 1 we:
- Removed the user from any groups
- Updated the description field with a note (for us, we will use TERMED: mm/dd/yyyy)
- Moved the user to a termed user OU
- Hid the user from the GAL
In Part 2, our script (not us!) will do the following:
- Move the Mailbox to a terminated user Mailbox Database
- Export the active Mailbox and Personal Archive PST
- Zip up and move the user’s Home Directory
- Check our work, and mark users who are ready for removal
- Remove the users from Active Directory
- Clean up previous processes
We will use the following functions:
- Zip
- ConvertToDate
- MoveMailbox
- ExportMailbox
- ArchiveHomeDirectory
- PendingRemoval
- RemoveObject
- LoadModules
- CleanUp
And we’re off.
Variables, Modules, and Parameters
Before we can get into the meat and potatoes, we need to define some variables and load some modules.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#Import Modules param ([string]$function) [void][System.Reflection.Assembly]::LoadFrom("C:\\scripts\\ScheduledTasks\\bin\\Ionic.Zip.dll") [Reflection.Assembly]::LoadWithPartialName(”System.Web”) | out-null # Modifiable Variables $global:terminated_days = "30" $global:from_email = "HelpDesk@domain.local" $global:to_email = "system.administrators@domain.local" $global:smtp_server = "relayserver.domain.local" $global:zip_password = "ZipPassword!" $global:termed_ou = "All Termed Users" $global:domain = "domain.local" $global:termed_mailbox_db = "TERMED" $global:blockmailsender = "blockmailuser" $global:archivelocation = "\\share\archive\" # Static Variables (do not change) $global:purge_date = (get-date).AddDays(-$terminated_days) $global:date = (get-date).ToString("MM/dd/yyy") $split = $domain.Split('.') $global:shortdom = $split[0] $global:shortdom2 = $split[1] |
To begin, our only parameter is $function. This allows us to run the script and define a specific function. We then load our Ionic.Zip.dll (DotNetZip) and System.Web (we’re going to use it to generate a hashtag). The DLL that you downloaded earlier should be placed in C:\\scripts\\ScheduledTasks\\bin\\.
Variables
- Customiziable variables. Change these to reflect your organization.
- $terminated_days – The amount of days that must pass since this user was terminated before we can being our process
- $from_email – Who the email will come from
- $to_email – Who the email will be sent to
- $smtp_server – The relay server. You can DNS or IP
- $zip_password – The password we’ll use to “secure” our zip files
- $termed_ou – The OU where terminated users are placed
- $domain – Your internal domain name
- $termed_mailbox_db – Termed user mailbox database
- $blockmailsender – This user will be used to stop mail flow to a specific user once their PST has been exported
- $archivelocation – Share location where PST’s and homedirectories will be placed
- Static Variables. Do not change these variables.
- $purge_date – Subtract the $terminated_date from today’s date
- $date – Today’s date
- $split – Split $domain into an array
- $shortdom – The NetBIOS name of your domain (hopefully)
- $shortdom2 – Top level domain (local)
Function: Zip [$Path, $OutputPath, $user]
This function will zip up our HomeDirectories.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#### Desc: Zip directories #### function Zip($Path,$OutputPath,$user) { $hashinfo = $null; for($a = 1; $a -ne 10) { $hashinfo = "$hashinfo`n$([System.Web.Security.Membership]::GeneratePassword(128,0))" $a++ } $directoryToZip = "$Path"; $zipfile = new-object Ionic.Zip.ZipFile; $e= $zipfile.AddEntry("README.txt", "$user archived on $date") $e= $zipfile.AddEntry("hash.txt", "$hashinfo") $e= $zipfile.AddDirectory($directoryToZip, "$user") $zipfile.Encryption = [Ionic.Zip.EncryptionAlgorithm]::WinZipAes256 $zipfile.Password = "$zip_password" $zipfile.UseZip64WhenSaving = [Ionic.Zip.Zip64Option]::Always $zipfile.Save("$OutputPath\$user.zip") $zipfile.Dispose(); } |
First we empty out $hashinfo, then with a foreach loop we create a unique hash for this zip. We then add README.txt with the info $user archived on $date. This allows us to know when this user’s HomeDirectory was specifically archived. Add in hash.txt so that our zip is unique. I implore you to read more about DotNetZip for information about what else is happening within this function. DotNetZip is a good tool to have in your arsenal of powershell scripts.
Variables for this function
- $Path – Path to the file that will be zipped
- $OutputPath – Where our zip will be dumped once it has been processed
- $user – The username of the user who is being terminated
Function: ConvertToDate [$STR]
This function will convert the string date that we get from the user’s description field into something we can use. ** Note: I did not write this function, but I cannot find the source. If you did, or know who did create this, please contact me so I can provide proper credit! **
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#### Desc: Convert AD description to a usable date #### function ConvertToDate($STR) { switch -regex ($STR) { "^\d{2}/\d{2}$" {[DateTime]::ParseExact($STR,"MM/yy",$null)} "^\d{1}/\d{2}$" {[DateTime]::ParseExact($STR,"M/yy",$null)} "^\d{2}/\d{2}/\d{2}$" {[DateTime]::ParseExact($STR,"MM/dd/yy",$null)} "^\d{1}/\d{2}/\d{2}$" {[DateTime]::ParseExact($STR,"M/dd/yy",$null)} "^\d{2}/\d{1}/\d{2}$" {[DateTime]::ParseExact($STR,"MM/d/yy",$null)} "^\d{1}/\d{1}/\d{2}$" {[DateTime]::ParseExact($STR,"M/d/yy",$null)} "^\d{2}/\d{2}/\d{4}$" {[DateTime]::ParseExact($STR,"MM/dd/yyyy",$null)} "^\d{1}/\d{2}/\d{4}$" {[DateTime]::ParseExact($STR,"M/dd/yyyy",$null)} "^\d{2}/\d{1}/\d{4}$" {[DateTime]::ParseExact($STR,"MM/d/yyyy",$null)} "^\d{1}/\d{1}/\d{4}$" {[DateTime]::ParseExact($STR,"M/d/yyyy",$null)} "^\d{2}/\d{4}$" {[DateTime]::ParseExact($STR,"MM/yyyy",$null)} "^\d{1}/\d{4}$" {[DateTime]::ParseExact($STR,"M/yyyy",$null)} default { $STR = "N/A"} } } |
Please read this TechNet article for more information about regex.
Function: MoveMailbox
This function will find any users that are in the Termed Users OU and move them to the Termed users mailbox database.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#### Desc: Move all mailboxes located in "All Termed Users" to the proper termed Mailbox Database #### function MoveMailbox { $results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties * -ea stop) | Sort Name $count = 0; foreach($result in $results) { $sam = ($result.SamAccountName) $database = Get-Mailbox $sam -ea stop if($database.Database -ne "$termed_mailbox_db") { New-MoveRequest -identity $sam -TargetDatabase "$termed_mailbox_db" -BadItemLimit "50" -ea stop $count++ $move_mailboxes = "$move_mailboxes$sam<br>" } } # Email admin information if($count -ne 0) { Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Move Mailboxes" -body "<font face=verdana size=2><b>Mailboxes Moved:</b><br><br>$move_mailboxes</font>" -smtpServer "$smtp_server" -BodyAsHtml -ea stop } } |
First we need to get a list of users in the Termed Users OU. We do this by using -SearchBase. We will do this for nearly every function from here on out, so I will not cover it beyond this point. We need to set our $count variable to 0 in order to know if we should send an email (was anything done).
Within the foreach loop, we are generating the following info and executing the following commands.
- $sam – Get the user’s SamAccountName
- $database – Get the user’s mailbox
- if $database.Database does not equal “$termed_mailbox_db” (the one defined by you in the variables), then do the following
- Execute New-MoveRequest for the user $sam with a Target Database of $termed_mailbox_db. Allow up to 50 items to be corrupted.
- You certainly do not have to allow any bad items; however, in my experience this will only cause issues with your automation. It is acceptable within our organization to lose 1-2 emails for every 10,000 emails.
- $count++ is saying make $count whatever it was, plus 1.
- $move_mailboxes is for our logging purposes. If this is the first time the loop has run through, then $move_mailboxes is empty and only the $sam is captured.
- Execute New-MoveRequest for the user $sam with a Target Database of $termed_mailbox_db. Allow up to 50 items to be corrupted.
Now that our loop is complete, we should send an email to the administrator(s) and let them know who has been moved.
We will reuse many of the same commands / variables (although we will redefine them) for future functions, but I will not continue to explain them.
Function: ExportMailbox
This function will export a user’s active mailbox and personal archive PST.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#### Desc: Export Mailbox and Archive to PST #### function ExportMailbox { $results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name $count = 0 $blockmail = (Get-ADUser $blockmailuser -properties *).CanonicalName foreach($result in $results) { if($result.Description) { $termed = ConvertToDate ($result.Description).Replace("TERMED: ", "") if($termed -le $purge_date) { $sam = ($result.SamAccountName) $database = Get-Mailbox $sam if($database.Database -eq "$termed_mailbox_db") { Set-Mailbox -AcceptMessagesOnlyFromSendersOrMembers "$blockmail" -RequireSenderAuthenticationEnabled $true -Identity $sam New-MailboxExportRequest -Mailbox $sam -FilePath $archivelocation\$sam.pst -BadItemLimit 50 if((Get-Mailbox -identity $sam).ArchiveDatabase -ne $null) { New-MailboxExportRequest -Mailbox $sam -FilePath $archivelocation\$sam.archive.pst -IsArchive -BadItemLimit 50 $archive_mailboxes = "$archive_mailboxes$sam<br>" } $count++ $regular_mailboxes = "$regular_mailboxes$sam<br>" } } } } # Email admin information if($count -ne 0) { Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Export PST's" -body "<font face=verdana size=2><b>Mailboxes Exported:</b><br><br>$regular_mailboxes<br><b>Archives Exported:</b><br><br>$archive_mailboxes</font>" -smtpServer "$smtp_server" -BodyAsHtml } } |
The $blockmail variable is collecting the CanonicalName for the user $blockmailuser. This is needed to change the -AcceptMessagesOnlyFromSendersOrMembers parameter within Set-Mailbox.
This foreach loop is completing the following tasks:
- $termed is executing the ConvertToDate function on the user’s Description field. We must do this so we can compare today’s date with the users “TERMED” date.
- If $termed is less than or equal to $purge_date
- $sam – Collect the user’s SamAccountName
- $database – Get $sam user’s mailbox
- If $database.Database equals our $termed_mailbox_db
- Prevent the user from receiving any new email
- Create a New-MailboxExportRequest which will export the PST to $archivelocation\$sam.pst
- If our mailbox has a personal archive
- Create another New-MailboxExportRequest to export this user’s personal archive to $archivelocation\$sam.archive.pst. We use .archive.pst to differentiate their active mailbox and their archived mailbox. The -IsArchive switch tells PowerShell that we are interested in exporting this user’s Personal Archive.
- $archive_mailboxes is used for our email logs
Once we’re done with our loop, we fire off the email.
Function: ArchiveHomeDirectory
This function will zip up and move the user’s HomeDirectory. The Home Directory is a property within Active Directory. Common Home Directories are U:\ or \\Share\Users\tuser.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#### Desc: Move all data located in a user's HomeDirectory to a encrypted zip #### function ArchiveHomeDirectory { $results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name $count = 0 foreach($result in $results) { if($result.Description) { $termed = ConvertToDate ($result.Description).Replace("TERMED: ", "") if($termed -le $purge_date) { $sam = ($result.SamAccountName) if($result.HomeDirectory) { $hd = $result.HomeDirectory if(test-path $hd) { zip "$hd" "$archivelocation\UserShares" "$sam" $archived_HD = "$archived_HD$sam<br>" $count++; } elseif(!(test-path "$hd" -ea silentlycontinue) -and !(Get-ACL $hd -ea silentlycontinue)) { $aclerror = "$aclerror Cannot access $hd<br>" Move-ADObject -identity $result.distinguishedname -TargetPath "OU=$termed_ou (errors),DC=$shortdom,DC=$shortdom2" $count++; } } } } } # Email admin information if($count -ne 0) { Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Archive Home Directories" -body "<font face=verdana size=2><b>Home Directories Archived:</b><br><br>$archived_HD<br><b>Errors:</b><br><br>$aclerror</font>" -smtpServer "$smtp_server" -BodyAsHtml } } |
The similarities between this function and ExportMailbox are very similar; however, here we are working with NTFS files and not mailboxes. Instead of using New-MailboxExportRequest, we’re going to use our own function Zip. We use Test-Path to test if the HomeDirectory listed actually exists, and Get-ACL to test for access. If the folder doesn’t exist, or we do not have permissions, we’ll create an error to send to the admin. The zip files are placed within the share you defined earlier.
Halfway there…
At this point we are halfway done. The script has moved the user’s mailboxes, exported their production mailbox and personal archive to PST, and zipped up and moved their home directory. The following functions will check all of our work and make sure a user is ready for removal, and if everything checks out, remove the AD object and HomeDirectory from our file share. Then all we need is a little cleanup and it’s rinse and repeat.
*Deep breath* Okay, continue when ready.
Function: PendingRemoval
This function is going to check all of our work.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
#### Desc: Remove Object & cleanup folders #### function PendingRemoval { $results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name $count = 0 foreach($result in $results) { if($result.Description -like "TERMED*") { $termed = ConvertToDate ($result.Description).Replace("TERMED: ", "") if($termed -le $purge_date) { $sam = ($result.SamAccountName) $hd = $result.HomeDirectory #Has a PST been created? if((test-path "$archivelocation\$sam.pst")) { if((Get-Item "$archivelocation\$sam.pst").length/1KB -gt 264) { $pst = $true #$sam PST exists. } else { $pst = $false $pendingremoval_error = "PST does not exist (size)" } } else { $pst = $false $pendingremoval_error = "PST does not exist" } #Has the HomeDirectory been backed up? Does this user even have a HomeDirectory? if((test-path "$archivelocation\UserShares\$sam.zip")) { if((Get-Item "$archivelocation\UserShares\$sam.zip").length/1KB -gt 1) { $share = $true } $share = $true } elseif(!($result.HomeDirectory)) { $pendingremoval_error = "$pendingremoval_error - No Home Directory" $share = $true } else { if(!(test-path "$hd" -erroraction silentlycontinue) -and !(Get-ACL $hd -erroraction silentlycontinue)) { $pendingremoval_error = "$pendingremoval_error - Cannot access $hd" $share = $false } $share = $false $pendingremoval_error = "$pendingremoval_error - Home Directory exists, but no archive has been created" } #Parse results from PST and HomeDirectory. If success, update AD with "REMOVAL PENDING" desc. If failure, write to eventlog what has failed. if($pst -eq $true -and $share -eq $true) { Set-ADUser -identity $sam -description "REMOVAL PENDING" -enabled $false $set_success = "$set_success$sam<br>" } else { $set_failure = "$set_failure$sam - $pendingremoval_error<br>" } $count++ } } } # Email admin information if($count -ne 0) { Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Pending Removal" -body "<font face=verdana size=2><b>User's ready for removal:</b><br><br>$set_success<br><b>Errors:</b><br><br>$set_failure</font>" -smtpServer "$smtp_server" -BodyAsHtml } } |
While this function may appear quite long and may be daunting, it’s all very basic logic. Has a PST been created? Has the HomeDirectory been backed up? If those two things are true, then set the user as ready for deletion.
Function: RemoveObject
Alright… 213 lines of PowerShell just to get to this moment… The moment we delete the user!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#### Desc: Remove Object & cleanup folders #### function RemoveObject { $results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name $count = 0 foreach($result in $results) { if($result.Description -eq "REMOVAL PENDING") { if(PendingRemoval) { $sam = ($result.SamAccountName) $hd = $result.HomeDirectory Remove-Item -Recurse -Force $hd -confirm:$false Remove-Mailbox -identity $sam -Permanent $true -confirm:$false $count++ $removed = "$removed$sam<br>" } } } # Email admin information if($count -ne 0) { Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Remove Object" -body "<font face=verdana size=2><b>User's removed:</b><br><br>$removed</font>" -smtpServer "$smtp_server" -BodyAsHtml } } |
What we’re checking for here is if the users (those in the All Termed Users OU) have “REMOVAL PENDING” as their description. If they do, we’re going to check the users again (functions!) and, if everything pans out, delete the user’s Active Directory account and HomeDirectory from the file share.
Function: LoadModules
This script imports the ActiveDirectory module and adds the Exchange Management Shell snap-in.
1 2 3 4 5 |
#Load modules function LoadModules { Import-Module ActiveDirectory Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010 } |
If you have read any of my other articles, you have noticed that I always use PowerShell sessions rather than loading modules directly. This is because I want my scripts to work anywhere on the network and have no dependencies on the local machine. However, because we are already having to use a local DLL for this script to function properly, it doesn’t make much since to use remote sessions. Now you’re probably wondering why I have this wrapped up in a function… The reason is that our production script performs basic file moving tasks that don’t require these modules. Because of this, I don’t want the script to waste time loading modules it doesn’t need for certain tasks.
Function: CleanUp
This function will run prolately before each of our functions to ensure we keep a nice and clean Exchange environment.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#### Desc: Cleanup previous processes #### function CleanUp { if(Get-MoveRequest) { $mr_failure = Get-MoveRequest -MoveStatus Failed if($mr_failure) { Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - MoveStatus Failures" -body "<font face=verdana size=2><b>Failures</b><br><br>$mr_failure</font>" -smtpServer "$smtp_server" -BodyAsHtml } Get-MoveRequest -MoveStatus Failed | Remove-MoveRequest -confirm:$false Get-MoveRequest -MoveStatus Completed | Remove-MoveRequest -confirm:$false } if(Get-MailboxExportRequest) { $mer_failures = Get-MailboxExportRequest | where {$_.status -like "Failed"} if($mer_failures) { foreach($mer_failure in $mer_failures) { $path = $mer_failure.mailbox Move-ADObject -identity $path.distinguishedname -TargetPath "OU=$termed_ou (errors),DC=$shortdom,DC=$shortdom2" $write_mer_failure = "$write_mer_failure<br>$mer_failure" } Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - ExportRequest Failures" -body "<font face=verdana size=2><b>Failures</b><br><br>$write_mer_failure</font>" -smtpServer "$smtp_server" -BodyAsHtml } Get-MailboxExportRequest | where {$_.status -like "Failed"} | Remove-MailboxExportRequest -confirm:$false Get-MailboxExportRequest | where {$_.status -like "Completed*"} | Remove-MailboxExportRequest -confirm:$false } } |
As you can see, we’re heavy on the Exchange commands here. All we’re really trying to do is report any failures that have happened previously. If there is an issue, the user is moved to our All Termed Users (errors) OU. Here an admin can manually audit these users and see if they are ready to be deleted.
Execute the proper function
Now we can tell the script which function to execute.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#### Desc: Execute proper function #### $errormessage = $null try { if($function -eq "MoveMailbox") { LoadModules CleanUp MoveMailbox } elseif($function -eq "ExportMailbox") { LoadModules CleanUp ExportMailbox } elseif($function -eq "ArchiveHomeDirectory") { LoadModules CleanUp ArchiveHomeDirectory } elseif($function -eq "PendingRemoval") { LoadModules CleanUp PendingRemoval } elseif($function -eq "RemoveObject") { LoadModules CleanUp RemoveObject } elseif($function -eq "List") { LoadModules List } else { $errormessage = "No function was defined." } } catch [system.exception] { $errormessage = $($_.Exception.Message) } finally { if($errormessage) { Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - $function" -body "<font face=verdana size=2><b>Error:</b><br><br>$errormessage</font>" -smtpServer "$smtp_server" -BodyAsHtml } } |
The $function variable is defined by the user when they execute the script (e.g. .\UserTermination.ps1 -function MoveMailbox). We’ve wrapped this within try {} catch {} finally {} so we can catch any errors. And that’s it for the script!
Now we need to set up some scheduled tasks so this truly requires no interaction.
The Scheduled Tasks
These are the scheduled tasks that you’ll need to set up to really benefit from this script.
Program: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
Mailbox-Export
- Runs at 7:00 AM every Saturday of every week
- Add Arguments: -noninteractive -nologo C:\scripts\ScheduledTasks\UserTermination.ps1 “ExportMailbox”
Mailbox-HomeDirectory
- Runs at 6:00 PM every Friday of every week
- Add Arguments: -noninteractive -nologo C:\scripts\ScheduledTasks\UserTermination.ps1 “ArchiveHomeDirectory”
Mailbox-Move
- Runs at 8:00 AM every Wednesday of every week
- Add Arguments: -noninteractive -nologo C:\scripts\ScheduledTasks\UserTermination.ps1 “MoveMailbox”
Mailbox-PendingRemoval
- Runs at 8:00 AM every Monday of every week
- Add Arguments: -noninteractive -nologo C:\scripts\ScheduledTasks\UserTermination.ps1 “PendingRemoval”
Mailbox-RemoveObject
- Runs at 5:00 PM every Tuesday of every week
- Add Arguments: -noninteractive -nologo C:\scripts\ScheduledTasks\UserTermination.ps1 “RemoveObject”
All of these must be set to run with the highest privileges, and set to run whether a user is logged on or not.
Structure
Here are some images of what your Active Directory and Scheduled Tasks should look like.
Active Directory
Scheduled Tasks
Conclusion
Assuming you made it all the way down here after reading that mess above, congratulations! If you’re curious about the viability of this script, it has been working in our production environment for just short of a year now. It has processed north of 600 accounts and we have not yet (knock on wood) had a piece of information we were unable to retrieve.
Since the introduction of this script, we have freed up a little over 1TB in Mailbox database space and file share disk space. Those may not be huge returns, but if you’re working with limited resources, every bit helps.
You will probably have some issues along the way. This script was specifically tailored for our environment, but it should work with minimal modification by you.
I hope this is useful to you, and thank you for reading.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 |
##################################### # UserTermination.ps1 # Version 1.1 # Troy Ward # @AutomaShell / www.AutomaShell.com ##################################### #Import Modules param ([string]$function) [void][System.Reflection.Assembly]::LoadFrom("C:\\scripts\\ScheduledTasks\\bin\\Ionic.Zip.dll") [Reflection.Assembly]::LoadWithPartialName(”System.Web”) | out-null # Modifiable Variables $global:terminated_days = "30" $global:from_email = "HelpDesk@domain.local" $global:to_email = "system.administrators@domain.local" $global:smtp_server = "relayserver.domain.local" $global:zip_password = "ZipPassword!" $global:termed_ou = "All Termed Users" $global:domain = "domain.local" $global:termed_mailbox_db = "TERMED" $global:blockmailsender = "blockmailuser" $global:archivelocation = "\\share\archive\" # Static Variables (do not change) $global:purge_date = (get-date).AddDays(-$terminated_days) $global:date = (get-date).ToString("MM/dd/yyy") $split = $domain.Split('.') $global:shortdom = $split[0] $global:shortdom2 = $split[1] #### Desc: Zip directories #### function Zip($Path,$OutputPath,$user) { $hashinfo = $null; for($a = 1; $a -ne 10) { $hashinfo = "$hashinfo`n$([System.Web.Security.Membership]::GeneratePassword(128,0))" $a++ } $directoryToZip = "$Path"; $zipfile = new-object Ionic.Zip.ZipFile; $e= $zipfile.AddEntry("README.txt", "$user archived on $date") $e= $zipfile.AddEntry("hash.txt", "$hashinfo") $e= $zipfile.AddDirectory($directoryToZip, "$user") $zipfile.Encryption = [Ionic.Zip.EncryptionAlgorithm]::WinZipAes256 $zipfile.Password = "$zip_password" $zipfile.UseZip64WhenSaving = [Ionic.Zip.Zip64Option]::Always $zipfile.Save("$OutputPath\$user.zip") $zipfile.Dispose(); } #### Desc: Convert AD description to a usable date #### function ConvertToDate($STR) { switch -regex ($STR) { "^\d{2}/\d{2}$" {[DateTime]::ParseExact($STR,"MM/yy",$null)} "^\d{1}/\d{2}$" {[DateTime]::ParseExact($STR,"M/yy",$null)} "^\d{2}/\d{2}/\d{2}$" {[DateTime]::ParseExact($STR,"MM/dd/yy",$null)} "^\d{1}/\d{2}/\d{2}$" {[DateTime]::ParseExact($STR,"M/dd/yy",$null)} "^\d{2}/\d{1}/\d{2}$" {[DateTime]::ParseExact($STR,"MM/d/yy",$null)} "^\d{1}/\d{1}/\d{2}$" {[DateTime]::ParseExact($STR,"M/d/yy",$null)} "^\d{2}/\d{2}/\d{4}$" {[DateTime]::ParseExact($STR,"MM/dd/yyyy",$null)} "^\d{1}/\d{2}/\d{4}$" {[DateTime]::ParseExact($STR,"M/dd/yyyy",$null)} "^\d{2}/\d{1}/\d{4}$" {[DateTime]::ParseExact($STR,"MM/d/yyyy",$null)} "^\d{1}/\d{1}/\d{4}$" {[DateTime]::ParseExact($STR,"M/d/yyyy",$null)} "^\d{2}/\d{4}$" {[DateTime]::ParseExact($STR,"MM/yyyy",$null)} "^\d{1}/\d{4}$" {[DateTime]::ParseExact($STR,"M/yyyy",$null)} default { $STR = "N/A"} } } #### Desc: Move all mailboxes located in "All Termed Users" to the proper termed Mailbox Database #### function MoveMailbox { $results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties * -ea stop) | Sort Name $count = 0; foreach($result in $results) { $sam = ($result.SamAccountName) $database = Get-Mailbox $sam -ea stop if($database.Database -ne "$termed_mailbox_db") { New-MoveRequest -identity $sam -TargetDatabase "$termed_mailbox_db" -BadItemLimit "50" -ea stop $count++ $move_mailboxes = "$move_mailboxes$sam<br>" } } # Email admin information if($count -ne 0) { Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Move Mailboxes" -body "<font face=verdana size=2><b>Mailboxes Moved:</b><br><br>$move_mailboxes</font>" -smtpServer "$smtp_server" -BodyAsHtml -ea stop } } #### Desc: Export Mailbox and Archive to PST #### function ExportMailbox { $results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name $count = 0 $blockmail = (Get-ADUser $blockmailuser -properties *).CanonicalName foreach($result in $results) { if($result.Description) { $termed = ConvertToDate ($result.Description).Replace("TERMED: ", "") if($termed -le $purge_date) { $sam = ($result.SamAccountName) $database = Get-Mailbox $sam if($database.Database -eq "$termed_mailbox_db") { Set-Mailbox -AcceptMessagesOnlyFromSendersOrMembers "$blockmail" -RequireSenderAuthenticationEnabled $true -Identity $sam New-MailboxExportRequest -Mailbox $sam -FilePath $archivelocation\$sam.pst -BadItemLimit 50 if((Get-Mailbox -identity $sam).ArchiveDatabase -ne $null) { New-MailboxExportRequest -Mailbox $sam -FilePath $archivelocation\$sam.archive.pst -IsArchive -BadItemLimit 50 $archive_mailboxes = "$archive_mailboxes$sam<br>" } $count++ $regular_mailboxes = "$regular_mailboxes$sam<br>" } } } } # Email admin information if($count -ne 0) { Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Export PST's" -body "<font face=verdana size=2><b>Mailboxes Exported:</b><br><br>$regular_mailboxes<br><b>Archives Exported:</b><br><br>$archive_mailboxes</font>" -smtpServer "$smtp_server" -BodyAsHtml } } #### Desc: Move all data located in a user's HomeDirectory to a encrypted zip #### function ArchiveHomeDirectory { $results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name $count = 0 foreach($result in $results) { if($result.Description) { $termed = ConvertToDate ($result.Description).Replace("TERMED: ", "") if($termed -le $purge_date) { $sam = ($result.SamAccountName) if($result.HomeDirectory) { $hd = $result.HomeDirectory if(test-path $hd) { zip "$hd" "$archivelocation\UserShares" "$sam" $archived_HD = "$archived_HD$sam<br>" $count++; } elseif(!(test-path "$hd" -ea silentlycontinue) -and !(Get-ACL $hd -ea silentlycontinue)) { $aclerror = "$aclerror Cannot access $hd<br>" Move-ADObject -identity $result.distinguishedname -TargetPath "OU=$termed_ou (errors),DC=$shortdom,DC=$shortdom2" $count++; } } } } } # Email admin information if($count -ne 0) { Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Archive Home Directories" -body "<font face=verdana size=2><b>Home Directories Archived:</b><br><br>$archived_HD<br><b>Errors:</b><br><br>$aclerror</font>" -smtpServer "$smtp_server" -BodyAsHtml } } #### Desc: Remove Object & cleanup folders #### function PendingRemoval { $results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name $count = 0 foreach($result in $results) { if($result.Description -like "TERMED*") { $termed = ConvertToDate ($result.Description).Replace("TERMED: ", "") if($termed -le $purge_date) { $sam = ($result.SamAccountName) $hd = $result.HomeDirectory #Has a PST been created? if((test-path "$archivelocation\$sam.pst")) { if((Get-Item "$archivelocation\$sam.pst").length/1KB -gt 264) { $pst = $true #$sam PST exists. } else { $pst = $false $pendingremoval_error = "PST does not exist (size)" } } else { $pst = $false $pendingremoval_error = "PST does not exist" } #Has the HomeDirectory been backed up? Does this user even have a HomeDirectory? if((test-path "$archivelocation\UserShares\$sam.zip")) { if((Get-Item "$archivelocation\UserShares\$sam.zip").length/1KB -gt 1) { $share = $true } $share = $true } elseif(!($result.HomeDirectory)) { $pendingremoval_error = "$pendingremoval_error - No Home Directory" $share = $true } else { if(!(test-path "$hd" -erroraction silentlycontinue) -and !(Get-ACL $hd -erroraction silentlycontinue)) { $pendingremoval_error = "$pendingremoval_error - Cannot access $hd" $share = $false } $share = $false $pendingremoval_error = "$pendingremoval_error - Home Directory exists, but no archive has been created" } #Parse results from PST and HomeDirectory. If success, update AD with "REMOVAL PENDING" desc. If failure, write to eventlog what has failed. if($pst -eq $true -and $share -eq $true) { Set-ADUser -identity $sam -description "REMOVAL PENDING" -enabled $false $set_success = "$set_success$sam<br>" } else { $set_failure = "$set_failure$sam - $pendingremoval_error<br>" } $count++ } } } # Email admin information if($count -ne 0) { Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Pending Removal" -body "<font face=verdana size=2><b>User's ready for removal:</b><br><br>$set_success<br><b>Errors:</b><br><br>$set_failure</font>" -smtpServer "$smtp_server" -BodyAsHtml } } #### Desc: Remove Object & cleanup folders #### function RemoveObject { $results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name $count = 0 foreach($result in $results) { if($result.Description -eq "REMOVAL PENDING") { if(PendingRemoval) { $sam = ($result.SamAccountName) $hd = $result.HomeDirectory Remove-Item -Recurse -Force $hd -confirm:$false Remove-Mailbox -identity $sam -Permanent $true -confirm:$false $count++ $removed = "$removed$sam<br>" } } } # Email admin information if($count -ne 0) { Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Remove Object" -body "<font face=verdana size=2><b>User's removed:</b><br><br>$removed</font>" -smtpServer "$smtp_server" -BodyAsHtml } } #### Desc: Manual function to list next batch of accounts to be termed #### function List { $results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name $count = 1 foreach($result in $results) { if($result.Description) { $termed = ConvertToDate ($result.Description).Replace("TERMED: ", "") if($termed -le $purge_date) { $sam = ($result.SamAccountName) write-host "$count) $sam" $count++; } } } } #Load modules function LoadModules { Import-Module ActiveDirectory Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010 } #### Desc: Cleanup previous processes #### function CleanUp { if(Get-MoveRequest) { $mr_failure = Get-MoveRequest -MoveStatus Failed if($mr_failure) { Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - MoveStatus Failures" -body "<font face=verdana size=2><b>Failures</b><br><br>$mr_failure</font>" -smtpServer "$smtp_server" -BodyAsHtml } Get-MoveRequest -MoveStatus Failed | Remove-MoveRequest -confirm:$false Get-MoveRequest -MoveStatus Completed | Remove-MoveRequest -confirm:$false } if(Get-MailboxExportRequest) { $mer_failures = Get-MailboxExportRequest | where {$_.status -like "Failed"} if($mer_failures) { foreach($mer_failure in $mer_failures) { $path = $mer_failure.mailbox Move-ADObject -identity $path.distinguishedname -TargetPath "OU=$termed_ou (errors),DC=$shortdom,DC=$shortdom2" $write_mer_failure = "$write_mer_failure<br>$mer_failure" } Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - ExportRequest Failures" -body "<font face=verdana size=2><b>Failures</b><br><br>$write_mer_failure</font>" -smtpServer "$smtp_server" -BodyAsHtml } Get-MailboxExportRequest | where {$_.status -like "Failed"} | Remove-MailboxExportRequest -confirm:$false Get-MailboxExportRequest | where {$_.status -like "Completed*"} | Remove-MailboxExportRequest -confirm:$false } } #### Desc: Execute proper function #### $errormessage = $null try { if($function -eq "MoveMailbox") { LoadModules CleanUp MoveMailbox } elseif($function -eq "ExportMailbox") { LoadModules CleanUp ExportMailbox } elseif($function -eq "ArchiveHomeDirectory") { LoadModules CleanUp ArchiveHomeDirectory } elseif($function -eq "PendingRemoval") { LoadModules CleanUp PendingRemoval } elseif($function -eq "RemoveObject") { LoadModules CleanUp RemoveObject } elseif($function -eq "List") { LoadModules List } else { $errormessage = "No function was defined." } } catch [system.exception] { $errormessage = $($_.Exception.Message) } finally { if($errormessage) { Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - $function" -body "<font face=verdana size=2><b>Error:</b><br><br>$errormessage</font>" -smtpServer "$smtp_server" -BodyAsHtml } } |