Wednesday, March 30, 2011

Creating Dynamic User Objects in Active Directory

Account management in Active Directory comes at a cost of time and effort for an Administrator. This is especially burdensome for objects that are only needed for a limited timeframe such as testing objects. Active Directory provides a low cost method for dealing with temporary objects through the dynamic object class which provides automatic garbage collection based on a time-to-live value in seconds. This benefit does come with several limitations:
  • The maximum TTL of a dynamic object is 31,556,926 seconds (1 year).
  • A deleted dynamic object due to its TTL expiring does not leave a tombstone behind.
  • All DCs holding replicas of dynamic objects must run on Windows Server 2003 or greater.
  • Dynamic entries with TTL values are supported in all partitions except the Configuration partition and Schema partition.
  • Active Directory Domain Services do not publish the optional dynamicSubtrees attribute, as described in the RFC 2589, in the root DSE object.
  • Dynamic entries are handled similar to non-dynamic entries when processing search, compare, add, delete, modify, and modifyDN operations.
  • There is no way to change a static entry into a dynamic entry and vice-versa.
  • A non-dynamic entry cannot be added subordinate to a dynamic entry.
In the code sample below, I take the advantage of the dynamic object class to create 50 test accounts that will expire and delete themselves from the forest on May 1, 2011 at 2:30 pm. The accounts created for a hypothetical finance application testing are placed in a specific OU, provided a unique sAMAccountName (tested to ensure this) and a pseudo-random password created for them. All this information is placed in a comma separated values text file so you can pass it over to your Quality Assurance team.

And remember, this script creates objects in Active Directory! Use at your own risk! I might have the best of intentions but my skill may betray you. Test, test and further test before implementing this code in a production environment.
Function Get-LocalDomainController($objectDomain) {
 return ([System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite()).Servers | Where-Object { $_.Domain.Name -eq $objectDomain } | ForEach-Object { $_.Name } | Select-Object -first 1
}
     
Function Get-ObjectADDomain($distinguishedName) {
 return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}
     
Function Get-ActiveDirectoryObject($distinguishedName) {
 return [ADSI]("LDAP://" + (Get-LocalDomainController (Get-ObjectADDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
}

Function Get-DomainDistinguishedName($domain) {
 return ("dc=" + $domain.Replace(".",",dc="))
}

Function Test-sAMAccountName($sAMAccountName,$domain) {
 $objectConnection = New-Object -comObject "ADODB.Connection"
 $objectConnection.Open("Provider=ADsDSOObject;")
 $objectCommand = New-Object -comObject "ADODB.Command"
 $objectCommand.ActiveConnection = $objectConnection

 $ldapBase = ("LDAP://" + (Get-LocalDomainController $domain) + "/" + (Get-DomainDistinguishedName $domain))
 $ldapAttr = "sAMAccountName"
 $ldapScope = "subtree"
 $ldapFilter = "(&(objectClass=user)(sAMAccountName=$sAMAccountName))"
 $ldapQuery= "<$ldapBase>;$ldapFilter;$ldapAttr;$ldapScope"
 $objectCommand.CommandText = $ldapQuery
 $objectRecordSet = $objectCommand.Execute()
 if($objectRecordSet.RecordCount -gt 0) {
  $found = $true
 } else {
  $found = $false
 }
 return $found 
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name destinationOu -option Constant -value "ou=Finance Testing,dc=americas,dc=ad,dc=mycompany,dc=local"
Set-Variable -name totalNumberOfAccounts -option Constant -value 50
Set-Variable -name givenName -option Constant -value "Test"
Set-Variable -name sn -option Constant -value "Account"
Set-Variable -name passwordSize -option Constant -value 12
#--------------------------------------------------------------------------------------------------#
$randomNumber = New-Object System.Random
$accounts = @()
$endDate = "05/01/2011 14:30:00"
$entryTtlSeconds = [int]((Get-Date $endDate) - (Get-Date)).TotalSeconds
#######################################################################
# Or you can do a time span instead for the account time to live i.e.,
# $entryTtlSeconds = [int](New-TimeSpan -days 90).TotalSeconds 
# $entryTtlSeconds = [int](New-TimeSpan -minutes 60).TotalSeconds 
#######################################################################
if($entryTtlSeconds -gt 31556926) {
 Write-Host "Time-to-live greater than 1 year. Exiting!" -foregroundColor Red
 exit
}

$destinationOuObject = Get-ActiveDirectoryObject $destinationOu

if($destinationOuObject.distinguishedName -ne $destinationOu) {
 Write-Host "Unable to connect to $destinationOu. Exiting!" -foregroundColor Red
 exit
}

for($i = 1;$i -le $totalNumberOfAccounts;$i++) {
 $accountName = ($givenName + $sn + "$i")
 Write-Host "Creating $accountName..."
 
 if(Test-sAMAccountName $accountName (Get-ObjectADDomain $destinationOu)) {
  Write-Host "$accountName already exists, skipping..." -foregroundColor Yellow
  continue
 }
 
 $userObject = $destinationOuObject.Create("user","CN=$accountName")
 $userObject.PutEx(2,"objectClass",@("dynamicObject","user"))
 $userObject.Put("entryTTL",$entryTtlSeconds)
 $userObject.Put("sAMAccountName", $accountName) # Mandatory attribute
 $userObject.Put("givenName",$givenName)
 $userObject.Put("sn",($sn + "$i"))
 $userObject.Put("displayName",($givenName + " " + $sn + "$i"))
 $userObject.Put("description","Account Used for Application Testing")
 $userObject.Put("wWWHomePage","http://sharepointsite/projects/newfinancesystem")
 $userObject.Put("userPrincipalName",($accountName + "@" + (Get-ObjectADDomain $destinationOu)))
 $userObject.SetInfo()
 
 $password = ""
 for($x = 1;$x -le $passwordSize;$x++) {
  $password += [char]($randomNumber.Next(33,126))
 }
 
 $userObject.SetPassword($password)
 $userObject.Put("userAccountControl", 512)
 $userObject.SetInfo()

 $account = New-Object -typeName PSObject
 Add-Member -inputObject $account -type NoteProperty -name "domain" -value (Get-ObjectADDomain $destinationOu).Split(".")[0]
 Add-Member -inputObject $account -type NoteProperty -name "sAMAccountName" -value ($userObject.sAMAccountName).ToString()
 Add-Member -inputObject $account -type NoteProperty -name "givenName" -value ($userObject.givenName).ToString()
 Add-Member -inputObject $account -type NoteProperty -name "sn" -value ($userObject.sn).ToString()
 Add-Member -inputObject $account -type NoteProperty -name "userPrincipalName" -value ($userObject.userPrincipalName).ToString() 
 Add-Member -inputObject $account -type NoteProperty -name "password" -value $password
 $accounts += $account
}

$accounts | Export-Csv -path "$givenName $sn List.csv" -noTypeInformation

No comments:

Post a Comment