Posts RSS Comments RSS 117 Posts and 170 Comments till now

Archive for the 'All' Category

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

Using Get-Credential to Store Passwords “securely” in a file.

Lee Holmes of PowerShell Cookbook fame has a post here Importing and Exporting Credentials in PowerShell. After fielding a few questions on EE regarding this post.. I wrote these two scripts to make it a little simpler.

Set-myCredential: This will prompt you for credentials and store them in the file specified.

  1. #####################
  2. #Set-myCredential.ps1
  3. Param($File)
  4. $Credential = Get-Credential
  5. $credential.Password | ConvertFrom-SecureString | Set-Content $File
  6. #####################

Get-myCredential: This will get you credentials from a file specified. It require you know the user name.

  1. #####################
  2. #Get-myCredential.ps1
  3. Param($User,$File)
  4. $password = Get-Content $File | ConvertTo-SecureString
  5. $credential = New-Object System.Management.Automation.PsCredential($user,$password)
  6. $credential
  7. #####################

With these two script you can do something like this (using VMware Toolkit for example.)

  1. c:\scripts\Set-myCredential.ps1 c:\tools\mp.txt
  2. $creds = c:\scripts\Get-myCredential.ps1 MyUserName c:\tools\mp.txt
  3. Get-ViServer MyVirtualCenter -cred $creds

What Are “Parameterized Properties?”

I recently had someone ask me a question about property types. They kept referring to Parameterized Properties and while I knew they were confused, I only had vague understanding of this concept and I knew I definitely could not define it.

I had tried google and all I came up with was this ETS Parameterized Properties. I shot this description over to a friend at MS and he said that was a bad definition and one didn’t really exist. I asked him to define it the best he can and here is the result. I would consider this source pretty authoritative :)

Note: These are very common with COM Objects

Parameterized Property

Properties are members that are accessed like fields but are actually methods. There is a setter method for setting the value of the property and a getter method for getting the current value of the property. Normally, the setter method takes only one parameter (the value to set) and the getter does not take any.

However, there are situations where additional parameters are needed. Consider the case where you want to use the property to get and set a member of a collection. To do this, the setter method requires 2 parameters—the index (location) in the collection and the value to set the member to; the getter method requires 1 parameter—the index to retrieve the value from. When a property requires additional parameters to operate, it is called a parameterized property or an indexer.

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.

Powershell Usability Study

Microsoft Usability is conducting a study focusing on the remoting feature of PowerShell version 2 from July 22 to July 29, 2008. This is a great opportunity for IT administrators who have experience working with PowerShell to provide feedback and help improve the user experience of PowerShell version 2.

The study will be conducted in a lab setting on Microsoft’s main campus in Redmond, Washington. It is important for you to know that you do not need to prepare anything for this. We want to learn from you, the experts, to determine what needs to be improved in our software.

If you are interested or know someone who could be interested in participating, please email us at uccoord@microsoft.com with Powershell in subject line. For information on other studies and to learn more about Microsoft’s User Research program email us.

Scripting/SysAdmin Meme

Was called out by GPOGuy

What was your first machine?
Atari 800 or a TRS 80. Can’t remember.

What was the first real script you wrote?
Wow… The one that forced me to learn vbscript. I always knew I should, but when I got stuck in an environment with 2800 DCs in 2700+ sites. That was a lot of clicking… I had to script. I wrote a script that created the AD replication topology (scripted KCC if you like.)

What scripting languages have you used?
Used extensively: DOS batch, VBScript, PIC, PowerShell. dabbled with PHP, Perl, and Python.

What was your first professional sysadmin gig?
My first “computer” job was in high school working at a local technology repair firm. The first “sysadmin” job was for a small collection agency in Pcola Fl.

If you knew then what you know now, would have started in IT?
Absolutely. If I didnt like it I would leave. I think the most important thing about work is enjoying it.

If there is one thing you learned along the way that you would tell new sysadmins, what would it be?
Google, Google, GOOGLE! Think out of the box. Always ask why, the REAL problem isnt always clear.

What’s the most fun you’ve ever had scripting?
Decoding the UpToDateness Vector Table or my first 1000 line vbscript.

Who am I calling out?
No one… the buck stops here :)

joe’s Response to my perf testing.

I really appreciate him taking the time to post. He didn’t have to. It can be found HERE

A couple of things here:

1) I don’t want to pretend to be a software developer. I have not the first clue when it comes to writing apps. I am just now learning how data types affect performance. It is quite a leap from scripter to developer. My point here is that adfind is an app that has grown over time and has what is called “feature creep.” Most of the features were not intended and joe has just squeezed them in. I have no doubt if joe wanted to he could re-write adfind in a way that would absolutely blow my little feature lacking script away.

2) My point in this exercise is that when it comes to AD and Perf is concern there is a potential option out there if you do not mind the extra work.

3) I respect joe not only for his developer skills but as a person with a ton of knowledge about AD and a willingness to share that knowledge. joe does not advertise on his site and he gives all his tools away for free. He doesnt ask for anything in return.

4) My testing is not over. IMO for this to be a real success I need to be able to achieve similar performance returning objects… I mean that is the whole point right?

5) I will be working with Darren
(GPOGuy) from SDMSoftware to produce a CMDLet version of my script with a ton more features so watch his site.

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. }

DSP vs ADFind Chart

In an earlier blog entry Phase 3 of S.DS.P “CSV Output” (as much as 21 sec faster!) I posted some statics that proved that you could achieve similar performance using a Powershell Script and System.DirectoryServices.Protocols as you could with Raw LDAP API from a C/C++ app.

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

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. }

Next »