Posts RSS Comments RSS 117 Posts and 170 Comments till now

userAccountControl and “User cannot change password”

Someone asked me a question about setting the “User cannot change password” check box in ADUC. They were creating the user account and setting PASSWD_CANT_CHANGE along with other settings (see my post about HERE about userAccountControl and values) and they couldn’t figure out why the check box wasn’t being applied.

I thought about this, and my first impression was they had the wrong bit value. So, I posted the “correct” one. They came back and said “That didn’t work.”

Hmmm, that’s curious. I turned to Dean and asked him if I was missing something obvious (I had a nagging feeling I had been here before) and he informed me that it has to be set with an “Extended Right” (control access right) via an ACE.

DOH! Now the whole scenario I had THAT feeling about came back to me. I recall having this discussion with someone and providing them a Script that would set the ACE. I searched for the script and I couldn’t find it. This happens to me a lot so I decided a while ago… when I run across this again, BLOG IT!

Technical Info:

In the past, permissions on the ‘userAccountControl’ attribute could be edited oftentimes making the effective password policy moot, i.e. you could end up with accounts that don’t comply with the domain’s password policy.

In Windows 2000, you can’t easily prevent this except by using third party front-end/provisioning tools to manage user objects. In Windows 2003 and later, you can use three newly-added extended rights (Control Access Rights) to prevent these bits from being edited even when the caller has permission to do so. The three (new) ‘Extended Rights’ are -

• Update password not required bit [controls 'password not required' and maps to ACE in footnote below]
• Enable per user reversible encryption [controls whether password is stored reversibly encrypted or not]
• Unexpire password [controls 'password never expires']

Each of these extended rights MUST be configured on the domain head and scoped as “This Object only” with ALLOW or DENY for the security principals you designate. By default, ‘Authenticated Users’ is granted an ALLOW ACE for each of the three extended rights. This doesn’t mean any old authenticated user can alter the password related bits in the ‘userAccountControl’ attribute; they still require the permission to modify ‘userAccountControl’.

USAGE SCENARIO - create a single group representing ALL three extended rights (or perhaps ONE group for EACH extended right). Then ACL the group(s) accordingly on the domain head with a DENY ACE. Finally, place the account-administrator users and groups that have management permissions to user objects (i.e. they have write permissions to the ‘userAccountControl’ property) in the group(s) you just created thereby preventing those account-administrators from altering the password related bits on the ‘userAccountControl’ attribute resulting in an enforced password policy.

IMPORTANT NOTE [Observed Behavior] - When viewing or changing a user’s ability to change their own password (User Cannot Change Password) through the GUI, it no longer appears to touch ‘userAccountControl’s bit 0×40 (64) — rather, it simply grant’s the ‘SELF’ security principal ‘ALLOW’ or ‘DENY’ to ‘Change Password’ — this can be easily verified by viewing the DACL.

Links:
Modifying User Cannot Change Password (LDAP Provider)

So… Here it is (Set-UserCannotChangePassword.ps1)
Parameters
-User: The sAMAccountName of the User
-CheckBox: If passed it checks the box
-Default: Remove Check box.

  1. Param($User = $(throw ‘$User is Required’,[switch]$CheckBox)
  2. Write-Host
  3.  
  4. $Searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]"","(&(objectcategory=User)(sAMAccountName=$user))")
  5. $MyUser = $Searcher.FindOne().GetDirectoryEntry()
  6.  
  7. if(!$?){" !! Failed to Get User !!";Return}
  8.  
  9. if($CheckBox)
  10. {
  11.     Write-Host " - Checking Box for User [$($MyUser.distinguishedName)]"
  12.     $self = [System.Security.Principal.SecurityIdentifier]‘S-1-5-10′
  13.     $ExtendedRight = [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight
  14.     $deny = [System.Security.AccessControl.AccessControlType]::Deny
  15.     $selfDeny = new-object System.DirectoryServices.ActiveDirectoryAccessRule($self,$ExtendedRight,$deny,‘ab721a53-1e2f-11d0-9819-00aa0040529b’)
  16.     $MyUser.psbase.get_ObjectSecurity().AddAccessRule($selfDeny)
  17.     $MyUser.psbase.CommitChanges()
  18. }
  19. else
  20. {
  21.     Write-Host " - Removing Check Box for User [$($MyUser.distinguishedName)]"
  22.     $ACL = $MyUser.psbase.get_ObjectSecurity().GetAccessRules($true,$false, [System.Security.Principal.NTAccount])
  23.     $ACEs = $ACL | ?{($_.ObjectType -eq ‘ab721a53-1e2f-11d0-9819-00aa0040529b’) -and ($_.AccessControlType -eq ‘Deny’)}
  24.     foreach($ACE in $ACEs){if($ACE){[void]$MyUser.psbase.get_ObjectSecurity().RemoveAccessRule($ACE)}}
  25.     $MyUser.psbase.CommitChanges()
  26. }
  27.  
  28. Write-Host

What Domain Controllers were IFM’d and from whom.

This is to provide Context to joe’s post and a Powershell script I used to get the values to determine if our DC’s were Installed from Media.

I manage a very large Active Directory (~380k users) which causes a predictably large DIT (click here to Learn about DITs.) We recently encountered one specific DC exhibiting odd behavior: (LSASS) was churning an unusually large amount of disk IO.  This problem was quickly resolved and is of little interest but resulted in another good-to-know fact.  As we begun our investigation, one of the first things we looked at was how large the DIT was; roughly 4.8GB for this particular DC.  Dean (who consults for us full-time) and I first tried to determine if the churn was a product of our environment combined with DIT Bloat [1] or some kind of other whacky effect/limitation.  Dean was told that it was a newly-born DC and initially discounted DIT bloat since AD does NOT replicate white space.  Just a few minutes later, I recollected and piped up that I had promoted this particular DC via IFM [2] – this made a huge difference to our earlier and incorrect (albeit brief) conclusion: during an IFM, the DIT of the originating DC (white-space and all) is literally copied bit for bit and serves as the template from which the new database is built (necessary modifications acknowledged.)

This brings us to the moral of my story: how do we determine if a DC was promoted via IFM – a great question and an even better answer.  joe’s® post (HERE) was the result of a lengthy and possibly pointless [3] IM between him and Dean.  The short answer is there are some things that DCs create locally during replication causing the metadata to indicate that the local DC was the originating writer of certain attributes.  As a result, it is likely that a DC that lists itself as the originator of the RDN attribute for an object that existed prior to this DC’s promotion, (RDN attribute equating to CN, OU or DC / CN for say the Users container) was either the first DC to host this partition (or, typically, the DC that created this domain) or was promoted via replication (either way, not IFM’d.)  If, however, the attribute’s metadata indicates that another DC originated the RDN attribute, we can assume (though not yet without question since none of us have sufficient data to confirm that) that this DC was IFM’d and that the metadata indicates from whom the IFM’s backup-data was originally sourced (this is not necessarily the same DC from which this particular IFM was taken since it too may have been promoted from yet another IFM.)  HOW COOL IS THAT?

Here is a little [4] Script I used to get me the info:[5]

  1. # Get the Current Domain
  2. $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain()
  3. # Get the User Container for the Domain. We use this for the Metadata
  4. $ConfigContainer = "CN=Configuration,{0}" -f ([adsi]"").distinguishedName[0]
  5. # The final line enums each DC and check the MetaData for the CN of the Users Container. If IFM’d this should be a remote DC. If local it was replicated.
  6. $domain.DomainControllers | Select Name,@{n="USN";e={$_.GetReplicationMetadata($ConfigContainer).cn | select name,LocalChangeUsn,OriginatingChangeUsn,OriginatingServer}} | ft -auto

Here is what the output of the script looks

Name                          USN
====                         ===
HomeDC1.corp.lab     @{Name=cn; LocalChangeUsn=4100; OriginatingChangeUsn=4100; OriginatingServer=HomeDC1.corp.lab}
HomeDC2.corp.lab     @{Name=cn; LocalChangeUsn=5552; OriginatingChangeUsn=5552; OriginatingServer=HomeDC2.corp.lab}
Lab0DC2.corp.lab       @{Name=cn; LocalChangeUsn=5552; OriginatingChangeUsn=5552; OriginatingServer=Lab0DC2.corp.lab}
Lab0DC1.corp.lab       @{Name=cn; LocalChangeUsn=5552; OriginatingChangeUsn=5552; OriginatingServer=Lab0DC1.corp.lab}
Home1dc1.corp.lab  @{Name=cn; LocalChangeUsn=5691; OriginatingChangeUsn=5691; OriginatingServer=Home1dc1.corp.lab}
Home1dc2.corp.lab    @{Name=cn; LocalChangeUsn=5691; OriginatingChangeUsn=5691; OriginatingServer=Home1dc1.corp.lab}
Lab1dc1.corp.lab       @{Name=cn; LocalChangeUsn=5691; OriginatingChangeUsn=5691; OriginatingServer=Home1dc1.corp.lab}
Lab1dc2.corp.lab       @{Name=cn; LocalChangeUsn=5691; OriginatingChangeUsn=5691; OriginatingServer=Home1dc1.corp.lab}

[1] Here is one kind of DIT bloat scenario. (HERE.)
[2] More info on IFM (Install From Media) (HERE)
[3] Comment from Dean
[4] joe posted a “one liner” perl script, I could have easily posted one as well, but I wanted my script to be clear in intent. You could simply do this:

  1. ([System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain()).DomainControllers | Select Name,@{n="USN";e={$_.GetReplicationMetadata("CN=Configuration,{0}" -f ([adsi]"").distinguishedName[0]).cn | select name,LocalChangeUsn,OriginatingChangeUsn,OriginatingServer}} | ft -auto

[5] I used the configuration container for this. This script assumes you are in the forest root.

Working with LDAP Stats Control in Powershell

What: The stats control is a LDAP control that you can pass that will tell the server to return its internal stats on a query.

Why: The stats control is a great way to see what the Domain Controller does with your filter. Like what indexes it hits, how many entries it had to visit, how much time the DC spent, and entries visited. It is very useful in creating the most efficient LDAP Query possible.

How: I Used System.DirectoryServices.Protocols.DirectoryControl to pass the LDAP control to the Server and I used System.DirectoryServices.Protocols.BERConverter along with the protocol spec here: LDAP_SERVER_GET_STATS_OID: 1.2.840.113556.1.4.970 to decode the Byte Array that was returned.

Here is what is Returned:
For 2000
- threadCount: Number of threads that were processing LDAP requests on the DC at the time the search operation was performed.
- coreTime: The time in milliseconds which core logic in the DC spent processing the request.
- callTime: The overall time in milliseconds that the DC spent processing the request.
- searchSubOperations: The number of individual operations which the DC performed in processing the request.

For 2003/2008
- threadCount: Number of threads that were processing LDAP requests on the DC at the time the search operation was performed.
- callTime: The overall time in milliseconds that the DC spent processing the request
- entriesReturned: The number of objects returned in the search result.
- entriesVisited: The number of objects that the DC considered for inclusion in the search result.
- filter: String which represents the optimized form of the search filter used by the DC to perform a search. This very well may be different than the filter that was passed.
- index: String which indicates which database indexes were used by the DC to perform the search.

For 2008 Only
- pagesReferenced: The number of database pages referenced by the DC in processing the search.
- pagesRead: The number of database pages read from disk.
- pagesPreread: The number of database pages preread from disk by the DC in processing the search.
- pagesDirtied: The number of clean database pages modified by the DC in processing the search.
- pagesRedirtied: The number of previously modified database pages that were modified by the DC in processing the search.
- logRecordCount: The number of database log records generated by the DC in processing the search.
- logRecordBytes: The size in bytes of database log records generated by the DC in processing the search.

Note:
- Must have SE_DEBUG_PRIVILEGE
- I did NOT implement SO_EXTENDED_FMT flag.
- I did NOT test 2000.
- The functions that decodes Byte Array actually return objects, but for this test I just outputed the test to mimic ADFind.exe
- Special thanks to Robin Caron, joe Richards, and Dmitri Gavrilov for help with the decoding.
- Here is GREAT Doc on the Controls (and everything else AD) [MS-ADTS]: Active Directory Technical Specification

Code:

  1. Param(
  2.         $filter = "(objectclass=*)",
  3.         $base,
  4.         $Server,
  5.         [int]$pageSize = 1000,
  6.         [string[]]$props = @("1.1"),
  7.         [switch]$StatsOnly,
  8.         [switch]$Verbose
  9.     )
  10. function CreateStatsObject2008{
  11.     Param($StatsArray)
  12.     $DecodedArray = [System.DirectoryServices.Protocols.BerConverter]::Decode("{iiiiiiiiiaiaiiiiiiiiiiiiii}",$StatsArray) # Win2008
  13.     $myStatsObject = New-Object System.Object
  14.     $myStatsObject | Add-Member -Name "ThreadCount"     -Value $DecodedArray[1]  -MemberType "NoteProperty"
  15.     $myStatsObject | Add-Member -Name "CallTime"        -Value $DecodedArray[3]  -MemberType "NoteProperty"
  16.     $myStatsObject | Add-Member -Name "EntriesReturned" -Value $DecodedArray[5]  -MemberType "NoteProperty"
  17.     $myStatsObject | Add-Member -Name "EntriesVisited"  -Value $DecodedArray[7]  -MemberType "NoteProperty"
  18.     $myStatsObject | Add-Member -Name "Filter"          -Value $DecodedArray[9]  -MemberType "NoteProperty"
  19.     $myStatsObject | Add-Member -Name "Index"           -Value $DecodedArray[11] -MemberType "NoteProperty"
  20.     $myStatsObject | Add-Member -Name "PagesReferenced" -Value $DecodedArray[13] -MemberType "NoteProperty"
  21.     $myStatsObject | Add-Member -Name "PagesRead"       -Value $DecodedArray[15] -MemberType "NoteProperty"
  22.     $myStatsObject | Add-Member -Name "PagesPreread"    -Value $DecodedArray[17] -MemberType "NoteProperty"
  23.     $myStatsObject | Add-Member -Name "PagesDirtied"    -Value $DecodedArray[19] -MemberType "NoteProperty"
  24.     $myStatsObject | Add-Member -Name "PagesRedirtied"  -Value $DecodedArray[21] -MemberType "NoteProperty"
  25.     $myStatsObject | Add-Member -Name "LogRecordCount"  -Value $DecodedArray[23] -MemberType "NoteProperty"
  26.     $myStatsObject | Add-Member -Name "LogRecordBytes"  -Value $DecodedArray[25] -MemberType "NoteProperty"
  27.     $myStatsObject
  28. }
  29. function CreateStatsObject2003{
  30.     Param($StatsArray)
  31.     $DecodedArray = [System.DirectoryServices.Protocols.BerConverter]::Decode("{iiiiiiiiiaia}",$StatsArray) # Win2003
  32.     $myStatsObject = New-Object System.Object
  33.     $myStatsObject | Add-Member -Name "ThreadCount"     -Value $DecodedArray[1]  -MemberType "NoteProperty"
  34.     $myStatsObject | Add-Member -Name "CallTime"        -Value $DecodedArray[3]  -MemberType "NoteProperty"
  35.     $myStatsObject | Add-Member -Name "EntriesReturned" -Value $DecodedArray[5]  -MemberType "NoteProperty"
  36.     $myStatsObject | Add-Member -Name "EntriesVisited"  -Value $DecodedArray[7]  -MemberType "NoteProperty"
  37.     $myStatsObject | Add-Member -Name "Filter"          -Value $DecodedArray[9]  -MemberType "NoteProperty"
  38.     $myStatsObject | Add-Member -Name "Index"           -Value $DecodedArray[11] -MemberType "NoteProperty"
  39.     $myStatsObject
  40. }
  41. function CreateStatsObject2000{
  42.     Param($StatsArray)
  43.     $DecodedArray = [System.DirectoryServices.Protocols.BerConverter]::Decode("{iiiiiiii}",$StatsArray) # Win2000
  44.     $myStatsObject = New-Object System.Object
  45.     $myStatsObject | Add-Member -Name "ThreadCount"          -Value $DecodedArray[1]  -MemberType "NoteProperty"
  46.     $myStatsObject | Add-Member -Name "CoreTime"             -Value $DecodedArray[3]  -MemberType "NoteProperty"
  47.     $myStatsObject | Add-Member -Name "CallTime"             -Value $DecodedArray[5]  -MemberType "NoteProperty"
  48.     $myStatsObject | Add-Member -Name "searchSubOperations"  -Value $DecodedArray[7]  -MemberType "NoteProperty"
  49.     $myStatsObject
  50. }
  51.  
  52. if($Verbose){$VerbosePreference = "Continue"}
  53.  
  54. Write-Verbose " - Loading System.DirectoryServices.Protocols"
  55. [VOID][System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols")
  56.    
  57. [int]$pageCount = 0
  58. [int]$objcount = 0
  59.  
  60. if(!$Server)
  61. {
  62.     $rootDSE = [ADSI]"LDAP://rootDSE"
  63.     $Server = $rootDSE.dnsHostName
  64.     if(!$base){$base = $rootDSE.defaultNamingContext}
  65.     switch ($rootDSE.domainControllerFunctionality)
  66.     {
  67.         0 {$expression = ‘CreateStatsObject2000 $stats’}
  68.         2 {$expression = ‘CreateStatsObject2003 $stats’}
  69.         3 {$expression = ‘CreateStatsObject2008 $stats’}
  70.     }
  71. }
  72.  
  73. Write-Verbose " - Using Server:  [$Server]"
  74. Write-Verbose " - Using Base:    [$base]"
  75. Write-Verbose " - Using Filter:  [$filter]"
  76. Write-Verbose " - Page Size:     [$PageSize]"
  77. Write-Verbose " - Returning:     [$props]"
  78. Write-Verbose " - CSV:           [$csv]"
  79. Write-Verbose " - NoHeaders:     [$noHeader]"
  80. Write-Verbose " - Count:         [$Count]"
  81. Write-Verbose " - StatsOnly:     [$StatsOnly]"
  82. Write-Verbose " - Expression:    [$expression]"
  83.  
  84. Write-Verbose " - Creating LDAP connection Object"
  85. $connection = New-Object System.DirectoryServices.Protocols.LdapConnection($Server)  
  86. $Subtree = [System.DirectoryServices.Protocols.SearchScope]"Subtree"
  87.  
  88. Write-Verbose " + Creating SearchRequest Object"
  89. $SearchRequest = New-Object System.DirectoryServices.Protocols.SearchRequest($base,$filter,$Subtree,$props)
  90.  
  91. Write-Verbose "   - Creating System.DirectoryServices.Protocols.PageResultRequestControl Object"
  92. $PagedRequest  = New-Object System.DirectoryServices.Protocols.PageResultRequestControl($pageSize)
  93.  
  94. Write-Verbose "   - Creating System.DirectoryServices.Protocols.SearchOptionsControl Object"
  95. $SearchOptions = New-Object System.DirectoryServices.Protocols.SearchOptionsControl([System.DirectoryServices.Protocols.SearchOption]::DomainScope)
  96.  
  97. Write-Verbose "   - Creating System.DirectoryServices.Protocols.DirectoryControl Control for OID: [1.2.840.113556.1.4.970]"
  98. $oid = "1.2.840.113556.1.4.970"
  99. $StatsControl = New-Object System.DirectoryServices.Protocols.DirectoryControl($oid,$null,$false,$true)
  100.  
  101. Write-Verbose "   - Adding Controls"
  102. [void]$SearchRequest.Controls.add($pagedRequest)
  103. [void]$SearchRequest.Controls.Add($searchOptions)
  104. [void]$SearchRequest.Controls.Add($StatsControl)
  105.  
  106. $start = Get-Date
  107. while ($True)
  108. {
  109.     # Increment the pageCount by 1
  110.     $pageCount++
  111.  
  112.     # Cast the directory response into a SearchResponse object
  113.     Write-Verbose " - Cast the directory response into a SearchResponse object"
  114.     $searchResponse = $connection.SendRequest($searchRequest)
  115.  
  116.     # Display the retrieved page number and the number of directory entries in the retrieved page
  117.     Write-Verbose (" - Page:{0} Contains {1} response entries" -f $pageCount,$searchResponse.entries.count)
  118.  
  119.  
  120.     Write-Verbose " - Returning Stats for Page:$PageCount"
  121.     $stats = $searchResponse.Controls[0].GetValue()
  122.     $ResultStats = invoke-Expression $expression
  123.     if($pageCount -eq 1)
  124.     {
  125.         $StatsFilter = $ResultStats.Filter
  126.         $StatsIndex = $ResultStats.Index
  127.         Write-Verbose "   + Setting Filter to [$StatsFilter]"
  128.         Write-Verbose "   + Setting Index  to [$StatsIndex]"
  129.     }
  130.    
  131.     # If Cookie Length is 0, there are no more pages to request"
  132.     if ($searchResponse.Controls[1].Cookie.Length -eq 0)
  133.     {
  134.         if($count){$objcount}
  135.         "`nStatistics"
  136.         "================================="
  137.         "Elapsed Time: {0} (ms)" -f ((Get-Date).Subtract($start).TotalMilliseconds)
  138.         "Returned {0} entries of {1} visited - ({2})`n" -f $ResultStats.EntriesReturned,$ResultStats.EntriesVisited,($ResultStats.EntriesReturned/$ResultStats.EntriesVisited).ToString(‘p’)
  139.         "Used Filter:"
  140.         "- {0}`n" -f $StatsFilter
  141.         "Used Indices:"
  142.         "- {0}`n" -f $StatsIndex
  143.         break
  144.     }
  145.  
  146.     # Set the cookie of the pageRequest equal to the cookie of the pageResponse to request the next
  147.     # page of data in the send request and cast the directory control into a PageResultResponseControl object
  148.     Write-Verbose " - Setting Cookie on SearchResponse to the PageReQuest"
  149.     $pagedRequest.Cookie = $searchResponse.Controls[1].Cookie
  150. }

Phase 3 of S.DS.P “CSV Output” (as much as 21 sec faster!)

In the next phase I wanted to actually get some data back and display it. Because I just wanted to test performance I kept the test as close as possible. ADFind has the built in ability to output a csv file so I mimicked his output. I also added two properties to return name and sAMAccountName. For those interested, my next step will be to output objects instead of text (really the whole point.)

My expectation for this test was that ADFind would pull ahead due to the processing of each record, but I was pleasantly surprised when the script was actually faster in all environments except 700k Environment. I will let joe explain why his is slower.

Testing
- I ran each 10x in a Row (as joe Suggested) in its own environement
- ADFind I tested using CMD.exe and used ptime.exe for time measurements.
- Get-DSPObject I used Measure-Command and outputed only TotalSeconds

UPDATED!: I had several people suggest that I should have a consolidated view of the results. So here it is. I took the Average from each test and put into and Excel spreadsheet and then plotted on a chart.

Here is the screenshot (lower is better)
DSP vs ADFind Chart

400k objects in a Prod Quality VM running Win2008 RTM 64bit (local)
Average: ADFind = 90.45
Average: DSP = 68.79
Winner: DSP 21.66 secs faster

- Get-DSPObject -prop “name”,”sAMAccountName” -csv
Execution time: 70.455s
Execution time: 69.551s
Execution time: 68.466s
Execution time: 68.085s
Execution time: 68.611s
Execution time: 68.179s
Execution time: 68.837s
Execution time: 68.670s
Execution time: 69.105s
Execution time: 67.994s
- adfind -b “your dn here” -f “(objectclass=user)” name samaccountname -csv
Execution time: 100.538 s
Execution time: 109.511 s
Execution time: 92.499 s
Execution time: 96.063 s
Execution time: 91.601 s
Execution time: 80.693 s
Execution time: 81.551 s
Execution time: 81.044 s
Execution time: 90.945 s
Execution time: 80.143 s

400k objects in a Prod Quality VM running Win2008 RTM 64bit (remote)
Average: ADFind = 77.18
Average: DSP = 64.23
Winner: DSP 13 secs faster

- Get-DSPObject -prop “name”,”sAMAccountName” -csv
Execution time: 67.065 s
Execution time: 63.871 s
Execution time: 63.330 s
Execution time: 63.027 s
Execution time: 62.630 s
Execution time: 64.692 s
Execution time: 64.451 s
Execution time: 64.450 s
Execution time: 64.594 s
Execution time: 64.219 s
- adfind -b “your dn here” -f “(objectclass=user)” name samaccountname -csv
Execution time: 77.280 s
Execution time: 77.616 s
Execution time: 77.304 s
Execution time: 76.899 s
Execution time: 77.426 s
Execution time: 76.773 s
Execution time: 76.699 s
Execution time: 77.445 s
Execution time: 77.463 s
Execution time: 76.912 s

700k objects on a Physical machine Win2k3 x86
Average: ADFind = 101.383
Average: DSP = 109.600
Winner: ADFind 8.21 secs faster

- Get-DSPObject -prop “name”,”sAMAccountName” -csv
Execution time: 111.088s
Execution time: 109.962s
Execution time: 110.112s
Execution time: 110.832s
Execution time: 110.167s
Execution time: 109.853s
Execution time: 109.757s
Execution time: 110.387s
Execution time: 109.070s
Execution time: 109.065s
- adfind -b “your dn here” -f “(objectclass=user)” name samaccountname -csv
Execution time: 101.571 s
Execution time: 101.313 s
Execution time: 101.386 s
Execution time: 101.593 s
Execution time: 101.539 s
Execution time: 100.917 s
Execution time: 101.020 s
Execution time: 101.286 s
Execution time: 101.544 s
Execution time: 101.667 s

300k Objects on Physical Win2k3 x86 (Remote)
Average: ADFind = 49.90
Average: DSP = 44.44
Winner: DSP 5 secs Faster

- Get-DSPObject -prop “name”,”sAMAccountName” -csv
Execution time: 44.782 s
Execution time: 44.316 s
Execution time: 44.473 s
Execution time: 44.322 s
Execution time: 44.380 s
Execution time: 44.440 s
Execution time: 44.394 s
Execution time: 44.544 s
Execution time: 44.428 s
Execution time: 44.247 s
- adfind -b “your dn here” -f “(objectclass=user)” name samaccountname -csv
Execution time: 51.983 s
Execution time: 51.937 s
Execution time: 51.810 s
Execution time: 49.142 s
Execution time: 48.913 s
Execution time: 49.073 s
Execution time: 48.765 s
Execution time: 49.125 s
Execution time: 49.110 s
Execution time: 49.105 s

200k Objects on my Server at Home (Win2k8 x64)
Average: ADFind = 35.50
Average: DSP = 31.74
Winner: DSP 4 secs Faster

- Get-DSPObject -prop “name”,”sAMAccountName” -csv
Execution time: 31.887 s
Execution time: 31.888 s
Execution time: 31.044 s
Execution time: 30.746 s
Execution time: 31.549 s
Execution time: 31.601 s
Execution time: 31.168 s
Execution time: 31.528 s
Execution time: 31.481 s
Execution time: 31.269 s
- adfind -b “your dn here” -f “(objectclass=user)” name samaccountname -csv
Execution time: 37.305 s
Execution time: 34.815 s
Execution time: 34.275 s
Execution time: 34.193 s
Execution time: 39.116 s
Execution time: 34.634 s
Execution time: 39.265 s
Execution time: 33.686 s
Execution time: 33.793 s
Execution time: 33.917 s

Here is the script I used for the DSP Tests

  1. function Get-DSPObject {
  2.     Param(
  3.             $filter = "(objectclass=user)",
  4.             $base = ([ADSI]"").distinguishedName,
  5.             $Server,
  6.             [int]$pageSize = 1000,
  7.             [string[]]$props = @("1.1"),
  8.             [switch]$noHeader,
  9.             [switch]$csv,
  10.             [switch]$count
  11.         )
  12.        
  13.     [VOID][System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols")
  14.        
  15.     [int]$pageCount = 0
  16.     [int]$objcount = 0
  17.    
  18.     if(!$server){$server = ([ADSI]"").distinguishedName -replace  ",","." -replace "dc=","" }
  19.    
  20.     $connection = New-Object System.DirectoryServices.Protocols.LdapConnection($Server)  
  21.     $subtree = [System.DirectoryServices.Protocols.SearchScope]"Subtree"
  22.    
  23.     $searchRequest = New-Object System.DirectoryServices.Protocols.SearchRequest($base,$filter,$subtree,$props)  
  24.     $pagedRequest = New-Object System.DirectoryServices.Protocols.PageResultRequestControl($pageSize)
  25.     $searchOptions = New-Object System.DirectoryServices.Protocols.SearchOptionsControl([System.DirectoryServices.Protocols.SearchOption]::DomainScope)
  26.     $searchRequest.Controls.add($pagedRequest) | out-null
  27.     $searchRequest.Controls.Add($searchOptions) | out-null
  28.    
  29.     # Output Prep
  30.     if($props -notcontains "1.1")
  31.     {
  32.         $MyProps = @()
  33.         foreach($prop in $props){$MyProps += $prop.ToLower()}
  34.         if($csv)
  35.         {
  36.             if(!$noHeader)
  37.             {
  38.                 $header = "distinguishedName"
  39.                 foreach($prop in $props){$header += ",$prop"}
  40.                 $header
  41.             }
  42.         }
  43.         else
  44.         {
  45.             $MyUserObj = New-Object System.Object
  46.             $MyUserObj | Add-Member -name distinguishedName -MemberType "NoteProperty" -value $null
  47.             foreach($prop in $props){$MyUserObj | Add-Member -name $prop -MemberType "NoteProperty" -value $null}
  48.         }
  49.     }
  50.    
  51.     # Process Pages
  52.     while ($True)
  53.     {
  54.         # Increment the pageCount by 1
  55.         $pageCount++
  56.    
  57.         # Cast the directory response into a SearchResponse object
  58.         $searchResponse = $connection.SendRequest($searchRequest)
  59.    
  60.         # Display the retrieved page number and the number of directory entries in the retrieved page
  61.         # "Page:{0} Contains {1} response entries" -f $pageCount,$searchResponse.entries.count
  62.         $objcount += ($searchResponse.entries).count
  63.        
  64.         # Display the entries within this page
  65.         if(!$count)
  66.         {
  67.             if($props -notcontains "1.1")
  68.             {
  69.                 foreach($entry in $searchResponse.Entries)
  70.                 {
  71.                     if($csv)
  72.                     {
  73.                         $results = "`"{0}`"" -f $entry.distinguishedName
  74.                         foreach($prop in $MyProps)
  75.                         {
  76.                             $results += ",`"{0}`"" -f ($entry.Attributes[$prop][0])
  77.                         }
  78.                         $results
  79.                     }
  80.                     else
  81.                     {
  82.                         $MyUserObj.distinguishedName = $entry.distinguishedName
  83.                         foreach($prop in $MyProps)
  84.                         {
  85.                             $MyUserObj."$prop" = $null
  86.                             $MyUserObj."$prop" = $entry.Attributes[$prop][0]
  87.                         }
  88.                         $MyUserObj
  89.                     }
  90.                 }
  91.             }
  92.             else{$searchResponse.Entries | select distinguishedName}
  93.         }
  94.        
  95.         # if this is true, there are no more pages to request
  96.         if ($searchResponse.Controls[0].Cookie.Length -eq 0){if($count){$objcount};break}
  97.    
  98.         # Set the cookie of the pageRequest equal to the cookie of the pageResponse to request the next
  99.         # page of data in the send request and cast the directory control into a PageResultResponseControl object
  100.         $pagedRequest.Cookie = $searchResponse.Controls[0].Cookie
  101.     }
  102. }
  103.  
  104. foreach($i in (1..10))
  105. {
  106.     $time = Measure-Command { Get-DSPObject -prop "name","sAMAccountName" -csv }
  107.     "Execution time: {0:n3} s" -f $time.TotalSeconds
  108. }

My First Venture into S.DS.P and Powershell

There has be much debate and agony on the the slowness of System.DirectoryServices.DirectorySearcher Class. This has lead me down the path of playing with System.DirectoryServices.Protocols (aka s.ds.p.)

By far the best tool out there right now is ADFind by joe Richards. So I used this for my target (although I didnt expect to get close.)

Here is the Guide I used in my Journey:
Introduction to System.DirectoryServices.Protocols (S.DS.P)

I started by writing the script you will find below and tested in a domain with well over 700k users and one with 200K. I did the test three different times in each Domain changing the order so cache hits wouldn’t be an issue. I also use objectclass on all three so index wouldn’t be a factor. I should NOTE that my script ONLY returns the count ATM. I am going to add properties next.

As you will see by the test results below… I got pretty darn close to adfind.exe in regards to perf. I was quite impressed with S.DS.P. Now to be fair, this was just counting objects. I am sure adfind.exe will start sneaking ahead abit further when we start processing properties and such. Can’t wait to see! Understand these test were just to see what tests I should focus on. I will posting another entry on more extensive count test with just DSP Using 1.1 and Adfind.

Test 1 700K Users done Remote

DirectorySearcher : 125.9123257
ADFind : 46.3763349
DSP Using DN : 69.5628776
DSP Using 1.1 : 49.4458161

Test 2 700K Users done Remote

DirectorySearcher : 125.0230257
ADFind : 46.4486472
DSP Using DN : 68.9255288
DSP Using 1.1 : 49.0780736

Test 3 700K Users done Remote

DirectorySearcher : 125.0230257
ADFind : 47.9162918
DSP Using DN : 79.6885386
DSP Using 1.1 : 54.152966

Test 1 200K done Local

DirectorySearcher : 121.1063569
ADFind : 55.2775406
DSP Using DN : 67.897922
DSP Using 1.1 : 28.5441615

Test 2 200K done Local

DirectorySearcher : 80.0894455
ADFind : 23.558696
DSP Using DN : 54.3111576
DSP Using 1.1 : 42.3485998

Test 3 200K done Local

DirectorySearcher : 99.1125363
ADFind : 80.1497852
DSP Using DN : 64.3716824
DSP Using 1.1 : 64.1940421

Summary: adfind.exe was faster (by bout 4sec on Avg.) remotely and larger domain, but protocals was faster (by bout 8 sec on Avg.) local on smaller domain.
Remote

ADFind: 46.92
DSP Using 1.1: 50.89
DSP Using DN: 72.73
DirectorySearcher: 125.32

Local

DSP Using 1.1: 45.03
ADFind: 52.99
DSP Using DN: 62.19
DirectorySearcher: 100.10

Here the script I ran for the test and how I measured the commands. I am going to play with passing the stats control and see what the server says later.

  1. $SearcherExpression = @’
  2. $searcher = new-object System.DirectoryServices.DirectorySearcher([ADSI]"","(objectclass=user)",@("distinguishedName"))
  3. $searcher.pagesize = 1000
  4. $searcher.findall()
  5. ‘@
  6.  
  7. Write-Host "Test 1"
  8. Write-Host ("-"*40)
  9. $myresults1 = "" | select @{n="DirectorySearcher";e={(Measure-command {invoke-expression $SearcherExpression}).TotalSeconds}},
  10.                          @{n="ADFind";e={(Measure-Command { .\adfind -b "dc=corp,dc=lab" -c -f "(objectclass=user)" }).TotalSeconds}},
  11.                          @{n="DSP Using DN";e={(Measure-command { .\Test-DSProtocals.ps1 }).TotalSeconds}},
  12.                          @{n="DSP Using 1.1";e={(Measure-command { .\Test-DSProtocalsSP.ps1 }).TotalSeconds}}
  13. $myresults1 | fl
  14.  
  15. Write-Host "Test 2"
  16. Write-Host ("-"*40)
  17. $myresults2 = "" | select @{n="ADFind";e={(Measure-Command { .\adfind -b "dc=corp,dc=lab" -c -f "(objectclass=user)" }).TotalSeconds}},
  18.                          @{n="DSP Using 1.1";e={(Measure-command { .\Test-DSProtocalsSP.ps1 }).TotalSeconds}},
  19.                          @{n="DSP Using DN";e={(Measure-command { .\Test-DSProtocals.ps1 }).TotalSeconds}},
  20.                          @{n="DirectorySearcher";e={(Measure-command {invoke-expression $SearcherExpression}).TotalSeconds}}
  21.  
  22. $myresults2 | fl
  23.  
  24. Write-Host "Test 3"
  25. Write-Host ("-"*40)
  26. $myresults3 = "" | select @{n="DSP Using DN";e={(Measure-command { .\Test-DSProtocals.ps1 }).TotalSeconds}},
  27.                          @{n="DSP Using 1.1";e={(Measure-command { .\Test-DSProtocalsSP.ps1 }).TotalSeconds}},
  28.                          @{n="DirectorySearcher";e={(Measure-command {invoke-expression $SearcherExpression}).TotalSeconds}},
  29.                          @{n="ADFind";e={(Measure-Command { .\adfind -b "dc=corp,dc=lab" -c -f "(objectclass=user)" }).TotalSeconds}}
  30. $myresults3 | fl
  31.  
  32. $myresults1,$myresults2,$myresults3
  33.  

Here is what the output of that Script looks like

S.DS.P : MyTest.ps1 Output

Here is the System.DirectoryServices.Protocols Code

  1. [System.Reflection.assembly]::LoadWithPartialName("system.directoryservices.protocols") | Out-Null
  2. $domain = ([ADSI]"").distinguishedName -replace  ",","." -replace "dc=",""
  3. $DomainDN = "DC=" + $Domain -replace "\.",",DC="
  4. [int]$pageCount = 0
  5. [int]$pageSize = 1000
  6. [int]$count = 0
  7. $connection = New-Object System.DirectoryServices.Protocols.LdapConnection($domain)  
  8. $subtree = [System.DirectoryServices.Protocols.SearchScope]"Subtree"
  9. $filter = "(objectclass=user)"
  10. $searchRequest = New-Object System.DirectoryServices.Protocols.SearchRequest($DomainDN,$filter,$subtree,@("1.1"))  
  11. $pagedRequest = New-Object System.DirectoryServices.Protocols.PageResultRequestControl($pageSize)
  12. $searchRequest.Controls.add($pagedRequest) | out-null
  13. $searchOptions = new-object System.DirectoryServices.Protocols.SearchOptionsControl([System.DirectoryServices.Protocols.SearchOption]::DomainScope)
  14. $searchRequest.Controls.Add($searchOptions) | out-null
  15.  
  16. while ($true)
  17. {
  18.     ## increment the pageCount by 1
  19.     $pageCount++
  20.     ## cast the directory response into a
  21.     ## SearchResponse object
  22.     $searchResponse = $connection.SendRequest($searchRequest)
  23.     ## verify support for this advanced search operation
  24.     if (($searchResponse.Controls.Length -lt 1) -or
  25.         !($searchResponse.Controls[0] -is [System.DirectoryServices.Protocols.PageResultResponseControl]))
  26.