Posts RSS Comments RSS 253 Posts and 411 Comments till now

Blog Archives

Tracing LDAP calls with Powershell

Spat had an eerily coincidental blog post the other day (HERE). The reason I say eerily is because the night before I was fighting trying to get a LDAP trace, this trace was to help figure out EXACTLY how SDS.ActiveDirectory got replication cursors from a Domain Controller (another joe Richards discussion.) Anyway, I digress, I found the blog entry EXTREMELY useful as it allowed me to get what I needed. I proceeded to leave a comment suggesting that this looked like a good job for Powershell as the resulting file from the tool is a CSV. This has led to a “challenge” from Spat and this is my response. I hope I did it justice.

Useful Links about Tracelog.exe
———–
Details about TraceLog.exe
LDAP tracing with TraceLog
ADSI tracing with TraceLog

Details about Script

Here are the functions in the script

Trace-Log
-flag: Hex value for the flags you want to pass (Default Value = “0x1FFFDFF3”)
-guid: GUID or File for tracing (Default Value = “LDAP”)
-SessionName: Unique Name for the actual trace (Default Value = “mytrace”)
-exe: The full name with extension of the EXE to add to registry to enable tracing. This only has to be done the first time you want to trace for an EXE.
[switch]Start: If set it enables logging. If not set, logging is disabled.
[switch]ADSI: If set it passes the ADSI GUID for tracing
[switch]LDAP: If set it passes the ADSI GUID for tracing

Convert-TraceLog
-Source: Trace (etl) file to convert to csv (Default Value = “.\ldap.etl”)
-file: File to set the results to (Default Value = “TraceResults.csv”)
[switch]$import: If set it will return a custom object with results

Below is a video that shows a demo of the script in use. I hope to do another one of these showing how to trace ADSI as well as LDAP. Make sure to read the Comments in Green. I tried to allow enough time. You can click to pause.

Download Tracelog Transcript (right click | Save Target As…)
Best Viewed Full Screen

Get the Flash Player to see this content.

Code
Download Trace Log Functions (right click | Save Target As…)

function Trace-Log {
    Param($file = ".\ldap.etl",
        $flag = 0x1FFFDFF3,
        $guid = "LDAP",
        $SessionName = "mytrace",
        $exe,
        [switch]$start,
        [switch]$ADSI,
        [switch]$LDAP
    )
    if($ADSI){$guid = "ADSI"}
    switch -exact ($guid)
    {
        "LDAP"  {$myguid = ‘#099614a5-5dd7-4788-8bc9-e29f43db28fc’}
        "ADSI"  {$myguid = ‘#7288c9f8-d63c-4932-a345-89d6b060174d’}
        Default {$myguid = "’$_’"}
    }
   
    Write-Host
   
    if($start)
    {
        Write-Host " Action: Start" -fore Yellow
        Write-Host " GUID:   $GUID" -fore Yellow
        Write-Host " File:   $file" -fore Yellow
        Write-Host " Flag:   $flag" -fore Yellow
        if($exe){Write-Host " Exe:    $exe" -fore Yellow}
       
    }
    else
    {
        Write-Host " State: Disabled" -fore Red
    }
   
    Write-Host
   
    if(!(test-Path "HKLM:\System\CurrentControlSet\Services\ldap\tracing\$exe") -and $exe)
    {
        new-Item -path "HKLM:\System\CurrentControlSet\Services\ldap\tracing" -name $exe | out-Null
    }
   
    if($start)
    {
        $cmd = "Tracelog.exe -start ‘$SessionName’ -f ‘$file’ -flag ‘$flag’ -guid ‘$myguid’"
    }
    else
    {
        $cmd = "tracelog -stop $SessionName"
    }
   
    Write-Host
    Write-Host "==========================" -fore White -back black
    write-Host "Running Command:" -fore White
    Write-Host " ==> $cmd" -fore Yellow
    invoke-Expression $cmd
    Write-Host "==========================" -fore White -back black
    Write-Host
}
function Convert-TraceFile{
    Param($Source=".\ldap.etl",$file="TraceResults.csv",[switch]$import)
   
    $cmd = "tracerpt.exe $Source -o $file -of CSV -y"
   
    invoke-Expression $cmd
   
    if($import)
    {
        import-Csv $file
    }
}

Using rootDSE mods to transfer role ownership

A while back I had conversation with Richard siddaway about a blog entry he made HERE.

The basic gist is there seems to be a bug in the TransferRoleOwnership method of .NET Class DomainController. There is clearly a problem, but I say “seems” because strictly speaking the DomainController class does not include Windows 2008.

Thus we fall back on our old trusty rootDSE mods and for those of you that have never heard of rootDSE mods (you may have heard them referred to as Operational Attributes) I definately recommend reading up on them HERE.

Effectively what we do is get the rootDSE for the DC we want to transfer the role to and set one of the following mods to 1:

  • becomeInfrastructureMaster
  • becomePDC
  • becomeRidMaster
  • becomeSchemaMaster
  • becomeDomainMaster
  • You, of course, need to have the valid rights to perform the operation.

    Here is the Script I use

    Param($Server = "127.0.0.1",$role)
    if(!$role){return "Please enter a valid Role: IM,PDC,RID,Schema,DM"}
    $rootDSE = [ADSI]"LDAP://$Server/rootDSE"
    Write-Host
    Write-Host " Moving FSMO Role"
    Write-Host " – Using Server: [$Server]"
    Write-Host " – Using Role:   [$Role]"
    switch -exact ($role)
    {
        "IM"        {$myrole = ‘becomeInfrastructureMaster’}
        "PDC"       {$myrole = ‘becomePDC’}
        "RID"       {$myrole = ‘becomeRidMaster’}
        "Schema"    {$myrole = ‘becomeSchemaMaster’}
        "DM"        {$myrole = ‘becomeDomainMaster’}
        Default     {return "Please provide Valid Role: IM,PDC,RID,Schema,DM"}
    }
    Write-Host " – Performing $MyRole on $Server"
    $rootDSE.put($myRole,1)
    $rootDSE.SetInfo()
    Write-Host

    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 0x40 (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.

    Param($User = $(throw ‘$User is Required’,[switch]$CheckBox)
    Write-Host

    $Searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]"","(&(objectcategory=User)(sAMAccountName=$user))")
    $MyUser = $Searcher.FindOne().GetDirectoryEntry()

    if(!$?){" !! Failed to Get User !!";Return}

    if($CheckBox)
    {
        Write-Host " – Checking Box for User [$($MyUser.distinguishedName)]"
        $self = [System.Security.Principal.SecurityIdentifier]‘S-1-5-10’
        $ExtendedRight = [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight
        $deny = [System.Security.AccessControl.AccessControlType]::Deny
        $selfDeny = new-object System.DirectoryServices.ActiveDirectoryAccessRule($self,$ExtendedRight,$deny,‘ab721a53-1e2f-11d0-9819-00aa0040529b’)
        $MyUser.psbase.get_ObjectSecurity().AddAccessRule($selfDeny)
        $MyUser.psbase.CommitChanges()
    }
    else
    {
        Write-Host " – Removing Check Box for User [$($MyUser.distinguishedName)]"
        $ACL = $MyUser.psbase.get_ObjectSecurity().GetAccessRules($true,$false, [System.Security.Principal.NTAccount])
        $ACEs = $ACL | ?{($_.ObjectType -eq ‘ab721a53-1e2f-11d0-9819-00aa0040529b’) -and ($_.AccessControlType -eq ‘Deny’)}
        foreach($ACE in $ACEs){if($ACE){[void]$MyUser.psbase.get_ObjectSecurity().RemoveAccessRule($ACE)}}
        $MyUser.psbase.CommitChanges()
    }

    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]

    # Get the Current Domain
    $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain()
    # Get the User Container for the Domain. We use this for the Metadata
    $ConfigContainer = "CN=Configuration,{0}" -f ([adsi]"").distinguishedName[0]
    # 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.
    $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:

    ([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:

    Param(
            $filter = "(objectclass=*)",
            $base,
            $Server,
            [int]$pageSize = 1000,
            [string[]]$props = @("1.1"),
            [switch]$StatsOnly,
            [switch]$Verbose
        )
    function CreateStatsObject2008{
        Param($StatsArray)
        $DecodedArray = [System.DirectoryServices.Protocols.BerConverter]::Decode("{iiiiiiiiiaiaiiiiiiiiiiiiii}",$StatsArray) # Win2008
        $myStatsObject = New-Object System.Object
        $myStatsObject | Add-Member -Name "ThreadCount"     -Value $DecodedArray[1]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "CallTime"        -Value $DecodedArray[3]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "EntriesReturned" -Value $DecodedArray[5]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "EntriesVisited"  -Value $DecodedArray[7]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "Filter"          -Value $DecodedArray[9]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "Index"           -Value $DecodedArray[11] -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "PagesReferenced" -Value $DecodedArray[13] -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "PagesRead"       -Value $DecodedArray[15] -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "PagesPreread"    -Value $DecodedArray[17] -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "PagesDirtied"    -Value $DecodedArray[19] -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "PagesRedirtied"  -Value $DecodedArray[21] -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "LogRecordCount"  -Value $DecodedArray[23] -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "LogRecordBytes"  -Value $DecodedArray[25] -MemberType "NoteProperty"
        $myStatsObject
    }
    function CreateStatsObject2003{
        Param($StatsArray)
        $DecodedArray = [System.DirectoryServices.Protocols.BerConverter]::Decode("{iiiiiiiiiaia}",$StatsArray) # Win2003
        $myStatsObject = New-Object System.Object
        $myStatsObject | Add-Member -Name "ThreadCount"     -Value $DecodedArray[1]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "CallTime"        -Value $DecodedArray[3]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "EntriesReturned" -Value $DecodedArray[5]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "EntriesVisited"  -Value $DecodedArray[7]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "Filter"          -Value $DecodedArray[9]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "Index"           -Value $DecodedArray[11] -MemberType "NoteProperty"
        $myStatsObject
    }
    function CreateStatsObject2000{
        Param($StatsArray)
        $DecodedArray = [System.DirectoryServices.Protocols.BerConverter]::Decode("{iiiiiiii}",$StatsArray) # Win2000
        $myStatsObject = New-Object System.Object
        $myStatsObject | Add-Member -Name "ThreadCount"          -Value $DecodedArray[1]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "CoreTime"             -Value $DecodedArray[3]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "CallTime"             -Value $DecodedArray[5]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "searchSubOperations"  -Value $DecodedArray[7]  -MemberType "NoteProperty"
        $myStatsObject
    }

    if($Verbose){$VerbosePreference = "Continue"}

    Write-Verbose " – Loading System.DirectoryServices.Protocols"
    [VOID][System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols")
       
    [int]$pageCount = 0
    [int]$objcount = 0

    if(!$Server)
    {
        $rootDSE = [ADSI]"LDAP://rootDSE"
        $Server = $rootDSE.dnsHostName
        if(!$base){$base = $rootDSE.defaultNamingContext}
        switch ($rootDSE.domainControllerFunctionality)
        {
            0 {$expression = ‘CreateStatsObject2000 $stats’}
            2 {$expression = ‘CreateStatsObject2003 $stats’}
            3 {$expression = ‘CreateStatsObject2008 $stats’}
        }
    }

    Write-Verbose " – Using Server:  [$Server]"
    Write-Verbose " – Using Base:    [$base]"
    Write-Verbose " – Using Filter:  [$filter]"
    Write-Verbose " – Page Size:     [$PageSize]"
    Write-Verbose " – Returning:     [$props]"
    Write-Verbose " – CSV:           [$csv]"
    Write-Verbose " – NoHeaders:     [$noHeader]"
    Write-Verbose " – Count:         [$Count]"
    Write-Verbose " – StatsOnly:     [$StatsOnly]"
    Write-Verbose " – Expression:    [$expression]"

    Write-Verbose " – Creating LDAP connection Object"
    $connection = New-Object System.DirectoryServices.Protocols.LdapConnection($Server)  
    $Subtree = [System.DirectoryServices.Protocols.SearchScope]"Subtree"

    Write-Verbose " + Creating SearchRequest Object"
    $SearchRequest = New-Object System.DirectoryServices.Protocols.SearchRequest($base,$filter,$Subtree,$props)

    Write-Verbose "   – Creating System.DirectoryServices.Protocols.PageResultRequestControl Object"
    $PagedRequest  = New-Object System.DirectoryServices.Protocols.PageResultRequestControl($pageSize)

    Write-Verbose "   – Creating System.DirectoryServices.Protocols.SearchOptionsControl Object"
    $SearchOptions = New-Object System.DirectoryServices.Protocols.SearchOptionsControl([System.DirectoryServices.Protocols.SearchOption]::DomainScope)

    Write-Verbose "   – Creating System.DirectoryServices.Protocols.DirectoryControl Control for OID: [1.2.840.113556.1.4.970]"
    $oid = "1.2.840.113556.1.4.970"
    $StatsControl = New-Object System.DirectoryServices.Protocols.DirectoryControl($oid,$null,$false,$true)

    Write-Verbose "   – Adding Controls"
    [void]$SearchRequest.Controls.add($pagedRequest)
    [void]$SearchRequest.Controls.Add($searchOptions)
    [void]$SearchRequest.Controls.Add($StatsControl)

    $start = Get-Date
    while ($True)
    {
        # Increment the pageCount by 1
        $pageCount++

        # Cast the directory response into a SearchResponse object
        Write-Verbose " – Cast the directory response into a SearchResponse object"
        $searchResponse = $connection.SendRequest($searchRequest)

        # Display the retrieved page number and the number of directory entries in the retrieved page
        Write-Verbose (" – Page:{0} Contains {1} response entries" -f $pageCount,$searchResponse.entries.count)

        Write-Verbose " – Returning Stats for Page:$PageCount"
        $stats = $searchResponse.Controls[0].GetValue()
        $ResultStats = invoke-Expression $expression
        if($pageCount -eq 1)
        {
            $StatsFilter = $ResultStats.Filter
            $StatsIndex = $ResultStats.Index
            Write-Verbose "   + Setting Filter to [$StatsFilter]"
            Write-Verbose "   + Setting Index  to [$StatsIndex]"
        }
       
        # If Cookie Length is 0, there are no more pages to request"
        if ($searchResponse.Controls[1].Cookie.Length -eq 0)
        {
            if($count){$objcount}
            "`nStatistics"
            "================================="
            "Elapsed Time: {0} (ms)" -f ((Get-Date).Subtract($start).TotalMilliseconds)
            "Returned {0} entries of {1} visited – ({2})`n" -f $ResultStats.EntriesReturned,$ResultStats.EntriesVisited,($ResultStats.EntriesReturned/$ResultStats.EntriesVisited).ToString(‘p’)
            "Used Filter:"
            "- {0}`n" -f $StatsFilter
            "Used Indices:"
            "- {0}`n" -f $StatsIndex
            break
        }

        # Set the cookie of the pageRequest equal to the cookie of the pageResponse to request the next
        # page of data in the send request and cast the directory control into a PageResultResponseControl object
        Write-Verbose " – Setting Cookie on SearchResponse to the PageReQuest"
        $pagedRequest.Cookie = $searchResponse.Controls[1].Cookie
    }

    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

    function Get-DSPObject {
        Param(
                $filter = "(objectclass=user)",
                $base = ([ADSI]"").distinguishedName,
                $Server,
                [int]$pageSize = 1000,
                [string[]]$props = @("1.1"),
                [switch]$noHeader,
                [switch]$csv,
                [switch]$count
            )
           
        [VOID][System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols")
           
        [int]$pageCount = 0
        [int]$objcount = 0
       
        if(!$server){$server = ([ADSI]"").distinguishedName -replace  ",","." -replace "dc=","" }
       
        $connection = New-Object System.DirectoryServices.Protocols.LdapConnection($Server)  
        $subtree = [System.DirectoryServices.Protocols.SearchScope]"Subtree"
       
        $searchRequest = New-Object System.DirectoryServices.Protocols.SearchRequest($base,$filter,$subtree,$props)  
        $pagedRequest = New-Object System.DirectoryServices.Protocols.PageResultRequestControl($pageSize)
        $searchOptions = New-Object System.DirectoryServices.Protocols.SearchOptionsControl([System.DirectoryServices.Protocols.SearchOption]::DomainScope)
        $searchRequest.Controls.add($pagedRequest) | out-null
        $searchRequest.Controls.Add($searchOptions) | out-null
       
        # Output Prep
        if($props -notcontains "1.1")
        {
            $MyProps = @()
            foreach($prop in $props){$MyProps += $prop.ToLower()}
            if($csv)
            {
                if(!$noHeader)
                {
                    $header = "distinguishedName"
                    foreach($prop in $props){$header += ",$prop"}
                    $header
                }
            }
            else
            {
                $MyUserObj = New-Object System.Object
                $MyUserObj | Add-Member -name distinguishedName -MemberType "NoteProperty" -value $null
                foreach($prop in $props){$MyUserObj | Add-Member -name $prop -MemberType "NoteProperty" -value $null}
            }
        }
       
        # Process Pages
        while ($True)
        {
            # Increment the pageCount by 1
            $pageCount++
       
            # Cast the directory response into a SearchResponse object
            $searchResponse = $connection.SendRequest($searchRequest)
       
            # Display the retrieved page number and the number of directory entries in the retrieved page
            # "Page:{0} Contains {1} response entries" -f $pageCount,$searchResponse.entries.count
            $objcount += ($searchResponse.entries).count
           
            # Display the entries within this page
            if(!$count)
            {
                if($props -notcontains "1.1")
                {
                    foreach($entry in $searchResponse.Entries)
                    {
                        if($csv)
                        {
                            $results = "`"{0}`"" -f $entry.distinguishedName
                            foreach($prop in $MyProps)
                            {
                                $results += ",`"{0}`"" -f ($entry.Attributes[$prop][0])
                            }
                            $results
                        }
                        else
                        {
                            $MyUserObj.distinguishedName = $entry.distinguishedName
                            foreach($prop in $MyProps)
                            {
                                $MyUserObj."$prop" = $null
                                $MyUserObj."$prop" = $entry.Attributes[$prop][0]
                            }
                            $MyUserObj
                        }
                    }
                }
                else{$searchResponse.Entries | select distinguishedName}
            }
           
            # if this is true, there are no more pages to request
            if ($searchResponse.Controls[0].Cookie.Length -eq 0){if($count){$objcount};break}
       
            # Set the cookie of the pageRequest equal to the cookie of the pageResponse to request the next
            # page of data in the send request and cast the directory control into a PageResultResponseControl object
            $pagedRequest.Cookie = $searchResponse.Controls[0].Cookie
        }
    }

    foreach($i in (1..10))
    {
        $time = Measure-Command { Get-DSPObject -prop "name","sAMAccountName" -csv }
        "Execution time: {0:n3} s" -f $time.TotalSeconds
    }

    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.

    $SearcherExpression = @’
    $searcher = new-object System.DirectoryServices.DirectorySearcher([ADSI]"","(objectclass=user)",@("distinguishedName"))
    $searcher.pagesize = 1000
    $searcher.findall()
    ‘@

    Write-Host "Test 1"
    Write-Host ("-"*40)
    $myresults1 = "" | select @{n="DirectorySearcher";e={(Measure-command {invoke-expression $SearcherExpression}).TotalSeconds}},
                             @{n="ADFind";e={(Measure-Command { .\adfind -b "dc=corp,dc=lab" -c -f "(objectclass=user)" }).TotalSeconds}},
                             @{n="DSP Using DN";e={(Measure-command { .\Test-DSProtocals.ps1 }).TotalSeconds}},
                             @{n="DSP Using 1.1";e={(Measure-command { .\Test-DSProtocalsSP.ps1 }).TotalSeconds}}
    $myresults1 | fl

    Write-Host "Test 2"
    Write-Host ("-"*40)
    $myresults2 = "" | select @{n="ADFind";e={(Measure-Command { .\adfind -b "dc=corp,dc=lab" -c -f "(objectclass=user)" }).TotalSeconds}},
                             @{n="DSP Using 1.1";e={(Measure-command { .\Test-DSProtocalsSP.ps1 }).TotalSeconds}},
                             @{n="DSP Using DN";e={(Measure-command { .\Test-DSProtocals.ps1 }).TotalSeconds}},
                             @{n="DirectorySearcher";e={(Measure-command {invoke-expression $SearcherExpression}).TotalSeconds}}

    $myresults2 | fl

    Write-Host "Test 3"
    Write-Host ("-"*40)
    $myresults3 = "" | select @{n="DSP Using DN";e={(Measure-command { .\Test-DSProtocals.ps1 }).TotalSeconds}},
                             @{n="DSP Using 1.1";e={(Measure-command { .\Test-DSProtocalsSP.ps1 }).TotalSeconds}},
                             @{n="DirectorySearcher";e={(Measure-command {invoke-expression $SearcherExpression}).TotalSeconds}},
                             @{n="ADFind";e={(Measure-Command { .\adfind -b "dc=corp,dc=lab" -c -f "(objectclass=user)" }).TotalSeconds}}
    $myresults3 | fl

    $myresults1,$myresults2,$myresults3

    Here is what the output of that Script looks like

    S.DS.P : MyTest.ps1 Output

    Here is the System.DirectoryServices.Protocols Code

    [System.Reflection.assembly]::LoadWithPartialName("system.directoryservices.protocols") | Out-Null
    $domain = ([ADSI]"").distinguishedName -replace  ",","." -replace "dc=",""
    $DomainDN = "DC=" + $Domain -replace "\.",",DC="
    [int]$pageCount = 0
    [int]$pageSize = 1000
    [int]$count = 0
    $connection = New-Object System.DirectoryServices.Protocols.LdapConnection($domain)  
    $subtree = [System.DirectoryServices.Protocols.SearchScope]"Subtree"
    $filter = "(objectclass=user)"
    $searchRequest = New-Object System.DirectoryServices.Protocols.SearchRequest($DomainDN,$filter,$subtree,@("1.1"))  
    $pagedRequest = New-Object System.DirectoryServices.Protocols.PageResultRequestControl($pageSize)
    $searchRequest.Controls.add($pagedRequest) | out-null
    $searchOptions = new-object System.DirectoryServices.Protocols.SearchOptionsControl([System.DirectoryServices.Protocols.SearchOption]::DomainScope)
    $searchRequest.Controls.Add($searchOptions) | out-null

    while ($true)
    {
        ## increment the pageCount by 1
        $pageCount++
        ## cast the directory response into a
        ## SearchResponse object
        $searchResponse = $connection.SendRequest($searchRequest)
        ## verify support for this advanced search operation
        if (($searchResponse.Controls.Length -lt 1) -or
            !($searchResponse.Controls[0] -is [System.DirectoryServices.Protocols.PageResultResponseControl]))
        {
            Write-Host "The server cannot page the result set"
            return;
        }
        ## cast the diretory control into
        ## a PageResultResponseControl object.
        $pageResponse = $searchResponse.Controls[0]
        ## display the retrieved page number and the number of
        ## directory entries in the retrieved page                    
        #"Page:{0} Contains {1} response entries" -f $pageCount,$searchResponse.entries.count
        $count += $searchResponse.entries.count
        ## display the entries within this page
        ## foreach($entry in $searchResponse.entries){$entry.DistinguishedName}
        ## if this is true, there
        ## are no more pages to request
        if ($pageResponse.Cookie.Length -eq 0){write-Host $count;break}
        ## set the cookie of the pageRequest equal to the cookie
        ## of the pageResponse to request the next page of data
        ## in the send request
        $pagedRequest.Cookie = $pageResponse.Cookie
    }

    Build Lab w/ Quest AD CMDLets

    Earlier I wrote a post about a script that I used to build my AD Lab Build Lab (v1 w/out Quest Tools) and I mentioned I
    would post a Quest version. I had some time run it (took about 6hrs.) So without further ado:

    Whats it do Again?
    # Creates A TestOU OU
    # Creates A TestComputers OU
    # Creates A TestUsers OU
    # Creates A TestGroups OU
    # Creates 10K OU’s Under TestOU
    ## Each of the 10k OUs will have 4 Child OUs
    ### Each OU should have 5 users Accounts and 5 Machines Accounts
    # Create 500 Group Policies.
    # Link 100 policies on the 10k Base OUs
    # Create 2000 Users in the TestUser OU
    # Create 2000 Computers in the TestComputer OU
    # Create 2K Groups

    Note: Added Write-Progress for OU/User Creation

    # Adding Required Snapins
    Add-PSSnapin SDMSoftware.PowerShell.GPMC -ea 0
    Add-PSSnapin Quest.ActiveRoles.ADManagement -ea 0

    $DomainDN = (([ADSI]"").distinguishedName[0])
    $DomainDNS = (([ADSI]"").distinguishedName[0]) -replace "DC=","" -replace ",","."
    $users = @()

    # A TestOU OU
    $BaseOU = New-QADObject -Type OrganizationalUnit -ParentContainer $DomainDN  -Name TestOU

    # A TestComputers OU
    $TestComps = New-QADObject -Type OrganizationalUnit -ParentContainer $DomainDN -Name TestComputers

    # A TestUsers OU
    $TestUsers = New-QADObject -Type OrganizationalUnit -ParentContainer $DomainDN -Name TestUsers

    # A TestGroups OU
    $TestGrps = New-QADObject -Type OrganizationalUnit -ParentContainer $DomainDN -Name TestGroups

    # 10K OUs Under TestOU
    foreach($i in 1..10000)
    {
        $lvl1Child = New-QADObject -Type OrganizationalUnit -ParentContainer $BaseOU.dn -Name "LvL1ChildOU$i"
        Write-Progress "Creating OUs LvL1ChildOU$i" -status "Updating" -perc ($i/10000*100)
        ## Each of the 10k OUs will have 4 Child OUs
        foreach($x in 1..4)
        {
            $lvl2Child = New-QADObject -Type OrganizationalUnit -ParentContainer $lvl1Child.dn -Name "LvL2Child${i}${x}"
            Write-Progress "Creating Child OUs LvL2Child${i}${x}" -status "Updating" -perc ($x/4*100) -id 1  
            foreach($y in 1..5)
            {
                ## Each OU should have 5 users Accounts and 5 Machines Accounts
                Write-Progress "Creating Child Users/Computers" -status "Updating" -perc ($y/5*100) -id 2
                New-QADUser -ParentContainer $lvl2Child.dn -Name "usr${i}${x}${y}" -SamAccountName "usr${i}${x}${y}" -UserPrincipalName "usr${i}${x}${y}@$DomainDNS" -UserPass "!P@ssw0rd22!" | Out-Null
                New-QADObject -ParentContainer $lvl2Child.dn -name "srv${i}${x}${y}" -objectAttributes @{"sAMAccountName"="srv${i}${x}${y}`$"} -type "Computer" | out-Null
            }
        }
    }

    # Create 500 Group Policies.
    1..500 | %{New-SDMgpo "TestGPO$_"}

    # Link 100 policies on the 10k Base OUs
    1..100 | %{Add-SDMgpLink -name "TestGPO$_" -scope "OU=LvL1ChildOU$i,$($BaseOU.DN)"}

    # Create 2000 Users in the TestUser OU
    1..2000 | %{New-QADUser -ParentContainer $TestUsers.dn -Name "Testusr$_" -SamAccountName "Testusr$_" -UserPrincipalName "Testusr$($_)@$DomainDNS" -UserPass "!P@ssw0rd22!"}

    # Create 2000 Computers in the TestComputer OU
    1..2000 | %{New-QADObject -ParentContainer $TestComps.dn -name "TestComp$($_)" -objectAttributes @{"sAMAccountName"="TestComp$($_)`$"}}

    # Create 2K Groups
    1..2000 | %{New-QADGroup -ParentContainer $TestGrps.dn -name "TestGrp$_" -sAMAccountName "TestGrp$_"}

    Fun with Active Directory (Playing Around Series)

    This is the first in a series of posts call “Playing Around Series.” This series will basically be demo Videos of different Snapins/.NET Classes and their use.

    In this entry I run through creating a DirectoryEntry, DirectorySearcher, and using the System.DirectoryServices.ActiveDirectory.Domain Class.

    Note: This is a fast run through. I STRONGLY recommend pausing and reading the Comments. Best Viewed Full Screen

    Get the Flash Player to see this content.


    Special Shout-out to JayKul and Jeffrey Snover for the Start-Demo script.
    http://www.powershellcentral.com/scripts/302

    Demo Text

    #
    # Lets start off by looking at DirectoryEntry
    #
    $DE = New-Object System.DirectoryServices.DirectoryEntry("LDAP://CN=tstUsr101,OU=MyUsers,DC=corp,DC=lab")
    #
    # First lets see what we have access to
    #
    $DE | Get-Member
    #’
    # Hmmm.. doesn’t seem like much. OH WAIT! Remember Powershell abstracts the class… Lets add psbase
    #
    $DE.psbase | Get-Member
    #
    # Lets look at what properties are available.
    #
    $DE.psbase.Properties
    #
    # Thats more like it. You may also note that some AD properties are still missing.
    # That is because that LDAP doesnt return all the properties. For these you need to "GET" them.
    $DE.psbase.InvokeGet(‘msExchUMFaxID’)
    #
    # Using DirectoryEntry is fine if you know the DN of the object, but what if you need to search?
    # Lets look at System.DirectoryServices.DirectorySearcher
    #
    # The Searcher needs some info so put that in variables first
    #
    $root = [ADSI]""  ## This is using the Type Accelerator we spoke about earlier… This is Gets the base
    $filter = "(&(objectcategory=user))"
    #
    # Now Lets create the searcher
    #
    $searcher = New-Object System.DirectoryServices.DirectorySearcher($root,$filter)
    #
    # That gets the searcher ready, but to execute we need to call findall() or findone()
    #
    $users = $searcher.findAll()
    #
    # Lets see what we got. We have alot so lets only pick the first 10
    #
    $users | select -first 10
    #
    # Tons of info, but notice that this is NOT the same as DirectoryEntry
    #
    $users | get-Member
    #
    # It still has the properties property, Lets look (but only the first 3)
    #
    $users | select -first 3 | %{$_.Properties}
    #
    # Finally Lets look at System.DirectoryDervices.ActiveDirectory.Domain
    #
    # We can use this to interactively browse around
    #
    [system.directoryservices.activedirectory.domain]::GetCurrentDomain()
    #
    # Lets assign that to variable to play with
    #
    $domain = [system.directoryservices.activedirectory.domain]::GetCurrentDomain()
    $domain
    #
    # Lets see what this has to offer
    #
    $domain | get-member
    #
    # Tons of cool stuff here.
    #
    # We can find all domain controllers
    $domain.FindAllDomainControllers()
    #
    # We Can look at our Domain FSMO
    #
    $domain | ft PdcRoleOwner,RidRoleOwner,InfrastructureRoleOwner
    #
    # I can even step the tree and get my forest root
    #
    $forest = $domain.Forest
    $forest
    #
    # With our new found $forest object… what can do we do?
    #
    $forest | Get-Member
    #
    # WE can find all our GCs
    #
    $forest.FindAllGlobalCatalogs()
    #
    # We can look at the Forest Mode
    #
    $forest.ForestMode
    #
    # Look at the Forest FSMO
    #
    $forest | ft SchemaRoleOwner,NamingRoleOwner
    #
    # Even look at sites
    $forest.Sites
    #
    # We can go on forever and ever. If you would like we can revisit this later.
    #

    Get/Set-ADACL (ACL and SDDLs for Active Directory!)

    A friend had a need to get/set Active Directory ACLs. So I wrote these.

    They will use [System.DirectoryServices.ActiveDirectoryAccessRule] objects or SDDLs strings.

    Note: I put the .NET classes and MS Spec for SDDLs at the bottom. Dont miss it!

    Get-ADACL.ps1

    # Get-ADACL.ps1
    Param($DNPath,[switch]$SDDL,[switch]$help,[switch]$verbose)
    function HelpMe{
        Write-Host
        Write-Host " Get-ADACL.ps1:" -fore Green
        Write-Host "   Gets ACL object or SDDL for AD Object"
        Write-Host
        Write-Host " Parameters:" -fore Green
        Write-Host "   -DNPath                : Parameter: DN of Object"
        Write-Host "   -sddl                  : [SWITCH]:  Output SDDL instead of ACL Object"
        Write-Host "   -Verbose               : [SWITCH]:  Enables Verbose Output"
        Write-Host "   -Help                  : [SWITCH]:  Displays This"
        Write-Host
        Write-Host " Examples:" -fore Green
        Write-Host "   Get ACL for ‘cn=users,dc=corp,dc=lab’" -fore White
        Write-Host "     .\Get-ADACL.ps1 ‘cn=users,dc=corp,dc=lab’" -fore Yellow
        Write-Host "   Get SDDL for ‘cn=users,dc=corp,dc=lab’" -fore White
        Write-Host "     .\Get-ADACL.ps1 ‘cn=users,dc=corp,dc=lab’ -sddl " -fore Yellow
        Write-Host
    }

    if(!$DNPath -or $help){HelpMe;return}

    Write-Host
    if($verbose){$verbosepreference="continue"}

    Write-Verbose " + Processing Object [$DNPath]"
    $DE = [ADSI]"LDAP://$DNPath"

    Write-Verbose "   – Getting ACL"
    $acl = $DE.psbase.ObjectSecurity
    if($SDDL)
    {
        Write-Verbose "   – Returning SDDL"
        $acl.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::All)
    }
    else
    {
        Write-Verbose "   – Returning ACL Object [System.DirectoryServices.ActiveDirectoryAccessRule]"
        $acl.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier])
    }

    Set-ADACL.ps1

    # Set-ADACL.ps1
    Param($DNPath,$acl,$sddl,[switch]$verbose,[switch]$help)
    function HelpMe{
        Write-Host
        Write-Host " Set-ADACL.ps1:" -fore Green
        Write-Host "   Sets the AD Object ACL to ‘ACL Object’ or ‘SDDL’ String"
        Write-Host
        Write-Host " Parameters:" -fore Green
        Write-Host "   -DNPath                : Parameter: DN of Object"
        Write-Host "   -ACL                   : Parameter: ACL Object"
        Write-Host "   -sddl                  : Parameter: SDDL String"
        Write-Host "   -Verbose               : [SWITCH]:  Enables Verbose Output"
        Write-Host "   -Help                  : [SWITCH]:  Displays This"
        Write-Host
        Write-Host " Examples:" -fore Green
        Write-Host "   Set ACL on ‘cn=users,dc=corp,dc=lab’ using ACL Object" -fore White
        Write-Host "     .\Set-ADACL.ps1 ‘cn=users,dc=corp,dc=lab’ -ACL $acl" -fore Yellow
        Write-Host "   Set ACL on ‘cn=users,dc=corp,dc=lab’ using SDDL" -fore White
        Write-Host "     .\Set-ADACL.ps1 ‘cn=users,dc=corp,dc=lab’ -sddl `$mysddl" -fore Yellow
        Write-Host
    }

    if(!$DNPath -or (!$acl -and !$sddl) -or $help){HelpMe;Return}

    Write-Host
    if($verbose){$verbosepreference="continue"}
    Write-Verbose " + Processing Object [$DNPath]"

    $DE = [ADSI]"LDAP://$DNPath"
    if($sddl)
    {
        Write-Verbose "   – Setting ACL using SDDL [$sddl]"
        $DE.psbase.ObjectSecurity.SetSecurityDescriptorSddlForm($sddl)
    }
    else
    {
        foreach($ace in $acl)
        {
            Write-Verbose "   – Adding Permission [$($ace.ActiveDirectoryRights)] to [$($ace.IdentityReference)]"
            $DE.psbase.ObjectSecurity.SetAccessRule($ace)
        }
    }
    $DE.psbase.commitchanges()
    Write-Host

    More Info
    I used the following .NET Classes
    System.DirectoryServices.DirectoryEntry
    http://msdn2.microsoft.com/en-us/library/system.directoryservices.directoryentry.aspx
    System.DirectoryServices.ActiveDirectoryAccessRule
    http://msdn2.microsoft.com/en-us/library/system.directoryservices.activedirectoryaccessrule.aspx
    System.DirectoryServices.ActiveDirectorySecurity
    http://msdn2.microsoft.com/en-us/library/system.directoryservices.activedirectorysecurity.aspx
    System.Security.AccessControl.AccessControlSections
    http://msdn2.microsoft.com/en-us/library/system.security.accesscontrol.accesscontrolsections(vs.80).aspx

    SDDL Info
    MS: http://msdn2.microsoft.com/en-us/library/aa379567.aspx

    « Prev - Next »