Posts RSS Comments RSS 253 Posts and 407 Comments till now

Testing AD LDS (ADAM) replication with Powershell

Earlier this month I had a discussion with Laura (of AD Cookbook fame) regarding ADLDS and how to test convergence. After a few minutes I remembered I had a AD convergence script I wrote a while back found HERE. With a little tweaking (specifically discoverability) we converted it to test ADLDS as well. Below you will fine the result.

Parameters
- Server: The ADLDS/ADAM server that hosts the application partition you want to test
- DN: The distinguished name of the application partition you want to test (will try to discover)
- Port: Port ADLDS/ADAM list on (Default 389)
- Table [switch]: A switch that outputs an object with the results.

Note: Please feel free to provide any feedback you have regarding this. I do not use ADLDS or ADAM so other than my test environment I really cannot play with this.

The Code
Test-LDSReplication.ps1


 

Param($Server = $Env:ComputerName,
      $DN,
      $Port = "389",
      [switch]$table
      )

function Ping-Server {
   Param([string]$srv)
   $pingresult = Get-WmiObject win32_pingstatus -f "address=’$srv’ and Timeout=1000"
   if($pingresult.statuscode -eq 0) {$true} else {$false}
}

$DirectoryServer = "{0}:{1}" -f $Server,$Port

$Context = new-object System.DirectoryServices.ActiveDirectory.DirectoryContext("DirectoryServer",$DirectoryServer)
$ADAM = [System.DirectoryServices.ActiveDirectory.AdamInstance]::GetAdamInstance($context)

if(!$DN)
{
    $AppPartition = $ADAM.ConfigurationSet | %{$_.ApplicationPartitions} | Select-Object -first 1
    $DN = $AppPartition.Name
    $dclist = $AppPartition.DirectoryServers | ?{$_.HostName -notmatch $Server}
}
else
{
    $dclist = $ADAM.ConfigurationSet.AdamInstances | ?{($_.Partitions -contains $DN) -and ($_.HostName -notmatch $Server)}
}

if($table)
{
    $DCTable = @()
    $myobj = "" | select Name,Time
    $myobj.Name = ("$Server [SOURCE]").ToUpper()
    $myobj.Time = 0.00
    $DCTable += $myobj
}

$timestamp = [datetime]::Now.ToFileTime().ToString()
Write-Host "`n  Modifying wwwHomePage Attribute on Object [$DN] on [$DirectoryServer] with value [$timestamp]"
$object = ([ADSI]"LDAP://$DirectoryServer/$DN")
$object.wWWHomePage = $timeStamp
$object.SetInfo()
$objectDN = $object.distinguishedname
Write-Host "  Object [$objectdn] Modified! `n"

$start = Get-Date

$i = 0

Write-Host "  Found [$($dclist.count)] LDS replicas"
$cont = $true

While($cont)
{
    $i++
    $oldpos = $host.UI.RawUI.CursorPosition
    Write-Host "  =========== Check $i ===========" -fore white
    start-Sleep 1
    $replicated = $true
    foreach($dc in $dclist)
    {
        if($server -match $dc.HostName){continue}
        if(ping-server $dc.HostName)
        {
            $DCServer = "{0}:{1}" -f $dc.HostName,$dc.LdapPort
            $object = [ADSI]"LDAP://$DCServer/$dn"
            if($object.wwwHomePage -eq $timeStamp)
            {
                Write-Host "  - $DCServer Has Object Description [$dn]" (" "*5) -fore Green
                if($table -and !($dctable | ?{$_.Name -match $dc.HostName}))
                {
                    $myobj = "" | Select-Object Name,Time
                    $myobj.Name = $dc.HostName.ToUpper()
                    $myobj.Time = ("{0:n2}" -f ((Get-Date)-$start).TotalSeconds)
                    $dctable += $myobj
                }
            }
            else{Write-Host "  ! $($dc.HostName.ToUpper()) Missing Object [$dn]" -fore Red;$replicated  = $false}
        }
        else
        {
            Write-Host "  ! $($dc.HostName.ToUpper()) Failed PING" -fore Red
            if($table -and !($dctable | ?{$_.Name -match $dc}))
            {
                $myobj = "" | Select-Object Name,Time
                $myobj.Name = $dc.HostName.ToUpper()
                $myobj.Time = "N/A"
                $dctable += $myobj
            }
        }
    }
    if($replicated){$cont = $false}else{$host.UI.RawUI.CursorPosition = $oldpos}
}

$end = Get-Date
$duration = "{0:n2}" -f ($end.Subtract($start).TotalSeconds)
Write-Host "`n    Took $duration Seconds `n" -fore Yellow

if($table){$dctable | Sort-Object Time}

Getting users group membership (tokengroups)

Around the same time as I wrote my Linked-Value Attribute script I also came up with this little gem. It also uses a constructed attribute provided with Windows 2003 called “tokengroups.” (Did I mention this includes recursive groups?)

Effectively it gets the attribute which returns an array of SIDs (in byte array form) for each group. I then use a function I posted about eariler called ConvertTo-Name to convert that BYTE array into a friendly group name we humans like.

Parameters:

  • - Account: Can be User samAccountName or DN of the user
  • - Verbose: Enables verbose output

More Info:
You may notice the GetInfoEx call I make on the user object. This is because tokengroups is not an actual attribute and does not get “populated” until you specifically request it. The GetInfoEx does exactly that.

Links:
MSDN: GetInfoEx
MSDN: tokenGroups

Get-TokenGroups.ps1

Param($Account,[switch]$Verbose)

if($verbose){$verbosepreference="Continue"}

Write-Host
Write-Verbose " – Account: $Account"
Write-Verbose " – Verbose: $Verbose"

function ConvertTo-Name($sid,[switch]$FromByte) {
    if($FromByte)
    {
        $ID = New-Object System.Security.Principal.SecurityIdentifier($sid,0)
    }
    else
    {
        $ID = New-Object System.Security.Principal.SecurityIdentifier($sid)
    }
    if($ID)
    {
        $User = $ID.Translate([System.Security.Principal.NTAccount])
        $User.Value
    }
}
function GetDNfromName{
    Param($name)
    $root = [ADSI]""
    $filter = "(sAMAccountName=$name)"
    $props = @("distinguishedName")
    $Searcher = new-Object System.DirectoryServices.DirectorySearcher($root,$filter,$props)
    $Searcher.FindOne().properties.distinguishedname
}

if($Account -notmatch "CN=(.*),((OU|DC)=\w*)*")
{
    Write-Verbose " + Getting User DN for [$Account]"
    $Account = GetDNfromName $Account
    Write-Verbose "   – GetDNfromName returned [$Account]"
}

Write-Verbose " – Getting User Object"
$UserAccount = [ADSI]"LDAP://$Account"

Write-Verbose " – Calling GetInfoEx"
$UserAccount.GetInfoEx(@("tokengroups"),0)

Write-Verbose " – Getting tokengroups"
$groups = $UserAccount.Get("tokengroups")

Write-Verbose " + Processing Groups"
foreach($group in $groups)
{
    $GroupName = ConvertTo-Name $group -FromByte
    Write-Verbose "   – Found group [$GroupName]"
    $GroupName
}

Write-Host

Constructed Properties and LVR (Linked-Value Replication)

There was an interesting question that came up on the news group that discussed getting Active Directory replication metadata and while the question really wasn’t directly a Powershell question I found it intriguing. I knew I had the answer to the question back in my brain, but I couldn’t retrieve it.

Here is the Question:

“I’m working on a script where I need to compare the last time the member
attribute of a distribution group was modified (not the AD group object
itself) with the time stamp on a file (I’m exporting distribution group
memberships to a file, but only ones that have changed). The problem
I’m running into is that some Active Directory distribution groups
aren’t returning the member attribute when I look at the replication
meta data.

Any one have any thoughts on why some distribution groups return the
member attribute when I run GetReplicationMetadata and some don’t? This
returns all kinds of other attributes and their metadata, but naturally
not the one I’m interested in (it does some times, on some groups, but
not all). I could run my script by the actual AD object WhenChanged
attribute, but I’ll be processing a large number of lists and I want it
to run as fast as possible and since other attributes can change on a
group object, I don’t want to have to export a 8,000 member group if the
displayName changes, for example (I’m only interested in the member
attribute).
As always, any insight is appreciated.”

I thought about this for a bit and also consulted some AD friends and we determined the issue was LVR (Link Value Replication.) This was introduced in Windows 2003 (specifically 2003 Native Mode.)

Basically, LVR changed the unit of replication for some attributes to be the actual values. Prior to LVR if you changed group membership the entire attribute member would have to replicate. With LVR, just the link you added for user replicates.

As one can imagine this changed the Metadata and therefore GetReplicationMetadata() didnt get retrieve the data for you. Where does this leave us?

There were also a few constructed attributes that were added with 2003. One of which is called “msds-replvaluemetadata.” This attribute provided the metadata for these links in wonderful XML format. You will find code using this XML below.

Some Useful Links regarding LVR and Constructed Attributes:

Below is the script that resulted from the investigation


 

Get-GroupMemberMetadata.ps1

Param($GroupName)

$GroupMembers = @()

$root = [ADSI]""
$filter = "(&(objectclass=group)(name=$GroupName))"
$props = @("msDS-ReplValueMetaData")
$Searcher = new-Object System.DirectoryServices.DirectorySearcher($root,$filter,$props)
foreach($Group in $Searcher.findall())
{
    foreach($childMember in $Group.Properties."msds-replvaluemetadata")
    {
        $GroupMembers += [xml+site:msdn.microsoft.com”>XML]$ChildMember
    }
}

foreach($Member in $GroupMembers)
{
    $Member.ChildNodes
}

Getting the UpToDateness Vector (UTDV) in Powershell

A year or so ago I needed to get the UTDV Table (defined in detail HERE by Laura Hunter of ShutUpLaura.) At the time, the only way I knew how to get this information was to get the replUpToDateVector attribute from the NC on the target DC. I could have used Repadmin shown below, but that wouldn’t have been any fun. I did however end up with the massive script at the bottom of this post. It required decoding the UTDV table and resolving the Invocation ID to a name or “DeletedDSA.”

Fast forward to the present.

During the course of a long conversation on an ActiveDir thread (“Domain Controller Version”,) joe (aka joeware) and I got into side conversation about getting the UTDV table (ironically due to a statement/question in the thread by the same Laura, mentioned earlier.)

This led me to a wonderful little .NET method (GetReplicationCursors) on DirectoryServices.ActiveDirectory.DomainController class. This was so much easier I wanted to kick myself for not finding it sooner.

In any case, it was a great learning experience so I wanted to share it you. Enjoy my pain and victory.

Using Repadmin

repadmin /showutdvec [dc]  "[ DN for Naming Context ]"

The easy way.
Using System.DirectoryServices.ActiveDirectory.DomainController

Write-Host
$domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$NC = "DC={0}" -f ($domain.Name -replace "\.",",DC=")
foreach($dc in $domain.DomainControllers)
{
    Write-Host "$($DC.Name)"
    Write-Host "================"
    $UDTV =  $dc.GetReplicationCursors($NC)
    $GUID = @{n=‘DSA’;e={if($_.SourceServer){$_.SourceServer}else{$_.SourceInvocationId}}}
    $UDTV | Select-Object $GUID,
                          UpToDatenessUsn,
                          LastSuccessfulSyncTime | Sort DSA -desc | Format-Table -auto
    Write-Host
}

The Hard way.
I initially did all the work myself using this function. This gets the Up To Dateness vector and decodes it. It also translates the Invocation ID (if possible.)

param([string]$server)

Begin{
    function Get-InvocationIDFromBytes{
        Param([byte[]]$GuidArray)
        $GUIDSection1 = @(1..4)
        $GUIDSection2 = @(1..2)
        $GUIDSection3 = @(1..2)
        $GUIDSection4 = @(1..2)
        $GUIDSection5 = @(1..6)
   
        $start = 0
       
        $InvocationIDString = ""
       
        foreach($byte in $GuidArray)
        {
            $InvocationIDString = $InvocationIDString + "\" + ([string]::Format("{0:X2}",$byte))
        }
       
        [system.Array]::Copy($GuidArray,$start,$GUIDSection1,0,4)
        [system.Array]::Copy($GuidArray,($start+4),$GUIDSection2,0,2)
        [system.Array]::Copy($GuidArray,($start+6),$GUIDSection3,0,2)
        [system.Array]::Copy($GuidArray,($start+8),$GUIDSection4,0,2)
        [system.Array]::Copy($GuidArray,($start+10),$GUIDSection5,0,6)
        [system.Array]::Reverse($GUIDSection1)
        [system.Array]::Reverse($GUIDSection2)
        [system.Array]::Reverse($GUIDSection3)
       
        [string]$GuidString1 = $GUIDSection1 | %{[string]::Format("{0:X2}",$_)}
        [string]$GuidString2 = $GUIDSection2 | %{[string]::Format("{0:X2}",$_)}
        [string]$GuidString3 = $GUIDSection3 | %{[string]::Format("{0:X2}",$_)}
        [string]$GuidString4 = $GUIDSection4 | %{[string]::Format("{0:X2}",$_)}
        [string]$GuidString5 = $GUIDSection5 | %{[string]::Format("{0:X2}",$_)}
       
        $GuidString1 = $GuidString1.replace(" ","")
        $GuidString2 = $GuidString2.replace(" ","")
        $GuidString3 = $GuidString3.replace(" ","")
        $GuidString4 = $GuidString4.replace(" ","")
        $GuidString5 = $GuidString5.replace(" ","")
       
        $InvocationGUID = "{0}-{1}-{2}-{3}-{4}" -f $GuidString1,$GuidString2,$GuidString3,$GuidString4,$GuidString5
       
        $name = Get-NameFromInvocationID $InvocationIDString
        if(!$DCInvocationID.$InvocationGUID)
        {
            if($name)
            {
                $DCInvocationID.add($InvocationGUID,($InvocationIDString,$name))
            }
            else
            {
                $DCInvocationID.add($InvocationGUID,$InvocationIDString)
            }
        }
        return $InvocationGUID
    }
    function Get-USNFromBytes{
        Param([byte[]]$USNArray)
        [system.Array]::Reverse($USNArray)
        [string]$USN = $USNArray | %{[string]::Format("{0:X2}",$_)}
        $usn = $usn.replace(" ","")
        $usn = [int]"0x$usn"
        $usn.PadRight(12)
    }
    function Get-DateFromBytes{
        Param([byte[]]$dateArray)
        [system.Array]::Reverse($dateArray)
        $temp = @(1..5)
        [system.Array]::Copy($dateArray,3,$temp,0,5)
        [string]$date = $temp | %{[string]::Format("{0:X2}",$_)}
        $date = $date.replace(" ","")
        $date = [int64]"0x$date"
        $date = $date + "0000000"
        [system.DateTime]::FromFileTime($date)
    }
    function Get-NameFromInvocationID{
        Param($InvocationID)
        [string]$config = ([adsi]"LDAP://RootDSE").ConfigurationNamingContext
        $de = new-Object System.DirectoryServices.DirectoryEntry("LDAP://$Config")
        $filter = "(&(invocationID=$InvocationID)(objectcategory=ntdsdsa))"
        $ds = new-Object System.DirectoryServices.DirectorySearcher($de,$filter)
        $hresult = $ds.findone()
        if($hresult.Path)
        {
            $ResultDE = $hresult.GetDirectoryEntry()
            $name = ($ResultDE.psbase.parent).DNSHostName
        }
        $name
    }
    function Decode-UpToDateVectorTable{
        Param([byte[]]$table)
       
        $guid = @(1..16)
        $USN = @(1..8)
        $Date = @(1..8)
        $UTDTable = @()
        $name = $null
       
        for($i=16;$i -lt $table.count;$i+=32)
        {
            [system.Array]::Copy($table,$i,$Guid,0,16)
            [system.Array]::Copy($table,$i+16,$USN,0,8)
            [system.Array]::Copy($table,$i+16+8,$Date,0,8)
           
            $GUIDString = Get-InvocationIDFromBytes $GUID
            $USNString  = Get-USNFromBytes $usn
            $dateString = Get-DateFromBytes $Date
            $UTDEntry = "" | Select-Object Name,USN,Date
            [system.Array]$dcname = $DCInvocationID.$GUIDString
            if($dcname[1])
            {
                $UTDEntry.Name = $dcname[1]
                $UTDEntry.USN =  $USNString
                $UTDEntry.Date = $DateString
                $UTDTable += $UTDEntry
            }
            else
            {
                $UTDEntry.Name = $GUIDString
                $UTDEntry.USN =  $USNString
                $UTDEntry.Date = $DateString
                $UTDTable += $UTDEntry
            }
        }  
        $UTDTable
    }
    $DCInvocationID = @{}
    $utdTables = @()
    $process = @()
}
Process{
    if($_)
    {
        $process += $_
    }
}
End{
    if($server){$process += $server}
    foreach($srv in $process)
    {
        $table = ([adsi]"LDAP://$srv").replUpToDateVector[0]
        if($process.count -gt 1)
        {
            $utdTableEntry = "" | Select-Object Name,UpToDatenessVector
            $utdTableEntry.Name = $srv
            $utdTableEntry.UpToDatenessVector = Decode-UpToDateVectorTable $table | Sort-Object Name -des
            $utdTables += $utdTableEntry
        }
        else
        {
            Decode-UpToDateVectorTable $table | Sort-Object Name -des
        }
    }
    if($utdTables){return $utdTables}
}

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.

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.

A collection of LDAP Filter Info

I often find myself googling for LDAP filter info. This time I decided to post the resulting set of websites I hit for this info.

NOTE: MS release the Specs for Active Directory’s LDAP Compliance here. GREAT DOC!

http://download.microsoft.com/download/d/c/8/dc83e0b8-fc2c-4af4-bd27-45b5963ad98d/AD%20LDAP%20Compliance.doc

Blog Entry on LDAP Filters
————————-

http://bsonposh.com/modules/wordpress/?p=78

LDAP Filter Articles
——————-
query Active Directory by using a bitwise filter

http://support.microsoft.com/kb/269181

Search Filter Syntax

http://msdn2.microsoft.com/en-us/library/aa746475.aspx

Mastering the LDAP search filter

http://searchwinit.techtarget.com/tip/0,289483,sid1_gci1191071,00.html

userAccountControl
——————-
UserAccountControl flags

http://support.microsoft.com/kb/305144

User-Account-Control Attribute (Windows)

http://msdn2.microsoft.com/en-us/library/ms680832.aspx

AD Replication Metadata (when did that change?)

There was a discussion on the NG about determining when a user was disabled. The initial request was to determine this based on whenChanged, but I thought that could be invalid as you can easily change an account after it was disabled. I can not think of a way to be sure, but the best way I can think of is to use the replication metadata on the attribute userAccountControl (the second bit is what determines if its disabled or not.) While it is possible to change the useraccountcontrol after a user is disabled it is unlikely.

More info for UserAccountControl bits

http://support.microsoft.com/kb/305144

Of course the next question was how do you check the Replication Metadata for an attribute on and AD object?

Enter Get-ADObjectREplicationMetadata.ps1

This uses

System.DirectoryServices.ActiveDirectory.DirectoryContext
– http://msdn2.microsoft.com/en-us/library/system.directoryservices.activedirectory.directorycontext.aspx
System.DirectoryServices.ActiveDirectory.DomainController
– http://msdn2.microsoft.com/en-gb/library/system.directoryservices.activedirectory.domaincontroller.aspx

# Get-ADObjectREplicationMetadata.ps1
# Brandon Shell (www.bsonposh.com)
# Purpose: Get attribute(s) Replication Metadata from a Domain controller.
Param($Domain,$objectDN,$property)
# Sets Context to Domain for System.DirectoryServices.ActiveDirectory.DomainController
$context = new-object System.DirectoryServices.ActiveDirectory.DirectoryContext("Domain",$domain)
# .NET Class that returns a Domain Controller for Specified Context
$dc = [System.DirectoryServices.ActiveDirectory.DomainController]::findOne($context)
# GetReplicationMetadata returns metadate from the DC for the DN specified.
$meta = $dc.GetReplicationMetadata($objectDN)
if($property){$meta | %{$_.$Property}}else{$meta}

This will return either all the metadata or just the metadata for a specific attribute. I should note that if you do not specify an attribute it returns all of them. You should expect to parse these as each attribute has a child object with the data in it.

All Attributes. The value can be found by .PropertyName

PS# .\Get-ADObjectMetaData.ps1 ‘my.lab.domain’ ‘CN=TestUser,DC=my,dc=lab,dc=domain’

Name                           Value
—-                           —–
countrycode                    System.DirectoryServices.ActiveDirectory.AttributeMetadata
cn                             System.DirectoryServices.ActiveDirectory.AttributeMetadata
mail                           System.DirectoryServices.ActiveDirectory.AttributeMetadata
scriptpath                     System.DirectoryServices.ActiveDirectory.AttributeMetadata
ntsecuritydescriptor           System.DirectoryServices.ActiveDirectory.AttributeMetadata
accountexpires                 System.DirectoryServices.ActiveDirectory.AttributeMetadata
displayname                    System.DirectoryServices.ActiveDirectory.AttributeMetadata
profilepath                    System.DirectoryServices.ActiveDirectory.AttributeMetadata
primarygroupid                 System.DirectoryServices.ActiveDirectory.AttributeMetadata
unicodepwd                     System.DirectoryServices.ActiveDirectory.AttributeMetadata
objectclass                    System.DirectoryServices.ActiveDirectory.AttributeMetadata
objectcategory                 System.DirectoryServices.ActiveDirectory.AttributeMetadata
instancetype                   System.DirectoryServices.ActiveDirectory.AttributeMetadata
homedrive                      System.DirectoryServices.ActiveDirectory.AttributeMetadata
samaccounttype                 System.DirectoryServices.ActiveDirectory.AttributeMetadata
homedirectory                  System.DirectoryServices.ActiveDirectory.AttributeMetadata
whencreated                    System.DirectoryServices.ActiveDirectory.AttributeMetadata
useraccountcontrol             System.DirectoryServices.ActiveDirectory.AttributeMetadata
msmqsigncertificates           System.DirectoryServices.ActiveDirectory.AttributeMetadata
dbcspwd                        System.DirectoryServices.ActiveDirectory.AttributeMetadata
title                          System.DirectoryServices.ActiveDirectory.AttributeMetadata
samaccountname                 System.DirectoryServices.ActiveDirectory.AttributeMetadata
supplementalcredentials        System.DirectoryServices.ActiveDirectory.AttributeMetadata
userparameters                 System.DirectoryServices.ActiveDirectory.AttributeMetadata
givenname                      System.DirectoryServices.ActiveDirectory.AttributeMetadata
description                    System.DirectoryServices.ActiveDirectory.AttributeMetadata
lmpwdhistory                   System.DirectoryServices.ActiveDirectory.AttributeMetadata
pwdlastset                     System.DirectoryServices.ActiveDirectory.AttributeMetadata
msnpallowdialin                System.DirectoryServices.ActiveDirectory.AttributeMetadata
codepage                       System.DirectoryServices.ActiveDirectory.AttributeMetadata
name                           System.DirectoryServices.ActiveDirectory.AttributeMetadata
ntpwdhistory                   System.DirectoryServices.ActiveDirectory.AttributeMetadata
userprincipalname              System.DirectoryServices.ActiveDirectory.AttributeMetadata
admincount                     System.DirectoryServices.ActiveDirectory.AttributeMetadata
objectsid                      System.DirectoryServices.ActiveDirectory.AttributeMetadata
sn                             System.DirectoryServices.ActiveDirectory.AttributeMetadata
msmqdigests                    System.DirectoryServices.ActiveDirectory.AttributeMetadata
logonhours                     System.DirectoryServices.ActiveDirectory.AttributeMetadata
lastlogontimestamp             System.DirectoryServices.ActiveDirectory.AttributeMetadata

Here is a specific Attribute

PS# .\Get-ADObjectMetaData.ps1 ‘my.lab.domain’ ‘CN=TestUser,DC=my,dc=lab,dc=domain’ ‘useraccountcontrol’

Name                        : userAccountControl
Version                     : 8
LastOriginatingChangeTime   : 9/15/2005 1:45:32 PM
LastOriginatingInvocationId : eeaeb6f9-8422-dddd-as34-04d7bd779285
OriginatingChangeUsn        : 47264036
LocalChangeUsn              : 49555172
OriginatingServer           : dc.my.lab.domain