I recently posted a PointCast discussing Active Directory snapshots in Windows Server 2008. In it, I point out that one of the limitations of using snapshots is the fact that they can’t be used to recover deleted user objects.
Fortunately, with PowerShell and a little bit of scripting knowledge you can create a convenient way to restore deleted user objects from the proverbial digital grave. Once this is accomplished, you can use a snapshot to recover other important attributes (group membership comes to mind). Before I get into all that, I should mention that any deleted object in Active Directory isn’t truly deleted until the tombstone period expires. To understand better how this process works, I’d like to refer you to Gil Kirkpatrick’s excellent article on TechNet, which provides all the detail you need to know. A great benefit with doing this is the fact that the account’s SID and GUID are restored without having to execute an authoritative restore; a process that most administrators are typically less than comfortable with. I can confirm the process works the same in Windows Server 2008 (RC1) as with Windows Server 2003.
This particular PowerShell script is intended for situations where you need to recover many (more than a few) deleted user objects. First, it asks the administrator for the number of hours ago the users were deleted. It then iterates through all deleted user objects in the current Active Directory domain. If any deleted object has a "last update" timestamp less than the threshold entered, it is recovered back to its original OU. NOTE: It is assumed in this script that the original OU structure is in-place. To accomplish the recovery, the script invokes adrestore.exe and pipes in the "y" character to confirm the operation. I could have coded the recovery process myself directly in the PowerShell script using the .NET LDAP libraries, but I decided on a simpler solution using this well known, free utility. You will need to download adrestore.exe (link) and place the executable in the same directory as the script.
The recovery process does not restore the user’s password. Therefore, the user object is always restored in a disabled state. The script generates a partially random password, sets it on the restored user account, unlocks the account, and saves the information to a tab-delimited text file. You can then use the text file as source data for a mail-merge which you can then print and distribute to end-users.
So here it is. Hopefully, this should help give you an idea of how you can use PowerShell to do some real, meaningful work in your IT environment. In the near future, I have plans to add group recovery options and interfaces to attribute recovery using a snapshot.
I do have a couple of disclaimers, first: For starters, I consider this to be "proof-of-concept" code. Do not implement this in your production environment until you’re comfortable with how it works and have tested the functionality. I make no claims of being a professional PowerShell developer. I’m learning about PowerShell like all the rest of us. I welcome any feedback on the script, but please keep any comments constructive.
$hoursAgo = Read-Host "Within how many hours was the object(s) deleted?"
# CREATE A LOG FILE WITH DYNAMIC FILENAME AND ADD HEADER INFO
$strLogFile = Get-Date -format yyyymmdd-hhmmsss
$strLogFile = $strLogFile + ".txt"
$strLogHeader = "NAME" + "`t" + "DN" + "`t" + "NEW PASSWORD"
Add-Content $strLogFile $strLogHeader
$strFilter = "(&(isDeleted=TRUE)(objectclass=user))"
$objDomain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$objRoot = $objDomain.GetDirectoryEntry()
$objRoot.psbase.AuthenticationType = [System.DirectoryServices.AuthenticationTypes]::FastBind
# DYNAMICALLY DETERMINE THE CURRENT DOMAIN NAME IN DN FORMAT
$strDomainName = $objRoot.psbase.path
$a = $strDomainName.IndexOf("DC")
$strDomainName = $strDomainName.substring($a, $strDomainName.length – $a)
$strDeletedItemPath = "LDAP://cn=Deleted Objects," + $strDomainName
Write-Host "SEARCHING:" $strDeletedItemPath
$objRoot.psbase.path = $strDeletedItemPath
$objSearcher = [System.DirectoryServices.DirectorySearcher]$objRoot
$objSearcher.PageSize = 1000
$objSearcher.Filter = $strFilter
$objSearcher.SearchScope = "OneLevel"
$objSearcher.Tombstone = $true
$colResults = $objSearcher.FindAll()
ForEach ($objResult in $colResults)
[string] $name = $objResult.Properties["name"]
$nameArray = $name.split("`n")
$name = $nameArray
[string] $lastknownparent = $objResult.Properties["lastknownparent"]
[string] $changetime = $objResult.Properties["whenchanged"]
$currentDateTime = get-date
# Get current time zone bias
$timeZoneBias = (gwmi win32_timezone).Bias
$dtChangeTime = [datetime]$changetime
$changeTimeThisTZ = $dtChangetime.addMinutes($timeZoneBias)
$hoursDifference = $currentDateTime.Subtract($changeTimeThisTZ).Hours
if ($hoursDifference -lt $hoursAgo)
Write-Host "Object Name:" $name
Write-Host "Last known parent:" $lastKnownParent
Write-Host "Current Time:" $currentDateTime
Write-Host "Object Updated:" $changeTimeThisTZ
Write-Host "This object was deleted less than" $hoursAgo "hours ago"
echo y | adrestore.exe -r $name
# Connect to recovered user object
$strRecoveredObj = "CN=" + $name + "," + $lastKnownParent
$objUser = New-Object DirectoryServices.DirectoryEntry("LDAP://" + $strRecoveredObj)
# Generate a password and set it.
$rand = New-Object system.random
$strNewPwd = "Adat!" + $rand.next(1000,9999)
# Enable the account
# Save data to text file
$strText = $name + "`t" + $strRecoveredObj + "`t" + $strNewPwd
Add-Content $strLogFile $strText