Zoho Banner September 2011

Powershell script to check for duplicate attribute values

Here’s a handy Powershell script to check for duplicate attribute values on AD objects.  Why is this useful?  Well, you might have provisioning systems that assign unique values (e.g. employeeID) to AD objects.  Things can start to go wrong if it turns out that more than one object has been assigned the same attribute value.  In the example below, I have used the adminDisplayname attribute, but you can easily change this to your attribute of choice.

# Import the AD Powershell module
ipmo ActiveDirectory
# Create an array from LDAP search
$adobjs = Get-ADObject -LDAPFilter "(admindisplayname=*)" -pr admindisplayname `
| Select-Object -ExpandProperty admindisplayname
# Create a new empty hash table object
$hash = @{}
# Add each item from the LDAP results to the hash table
$adobjs | % {$hash["$_"] += 1}
# Find the duplicates by examining the hash table
$hash.keys | ? {$hash["$_"] -gt 1} `
| % {write-host "Duplicate attribute value found: $_" }

Create new bulk AD delegations with Powershell

Here’s a Powershell script I wrote to delegate permissions to a service account to manage user objects within a number of OUs.  The script takes as input a file containing the distinguished names (DNs) of the OUs.  The approach should be reasonably obvious from the comments in the script.  The only complexity comes from having to get the correct System.DirectoryServices syntax for the Access Control Entries (ACEs).

#########################################################
#
# Name: Add-UserOUACEs.ps1
# Author: Tony Murray
# Version: 1.0
# Date: 16/04/2012
# Comment: PowerShell script to add Access Control
# entries to a target object
#
######################################################### 

Write-Verbose "Script starting..." 

# Import the AD module
ipmo ActiveDirectory 

# Set the verbosity preference
$VerbosePreference = "Continue" # Default is "SilentlyContinue", i.e. no verbosity 

### Set Global Variables 

# Specify the import file to use
$impfile = "c:\User_OUs.txt" 

# Specify the security principal to which perms will be granted
$svc = Get-ADUser MyServiceAccount 

# Get the SID of the security principal
$sid = new-object System.Security.Principal.SecurityIdentifier $svc.SID 

###  

# Change to the AD drive
CD AD: 

$ous = Import-Csv $impfile
foreach ($dn in $ous) {
    $ou = $dn.distinguishedname 

    ## Get the DACL of the OU 

    $acl = get-acl $ou 

    ## Note that  bf967aba-0de6-11d0-a285-00aa003049e2 is the schemaIDGuid for the user object class. 

    $guid = new-object Guid  bf967aba-0de6-11d0-a285-00aa003049e2                           

    # ACE for creating and deleting child User objects
    $ace1 = new-object System.DirectoryServices.ActiveDirectoryAccessRule $sid,"CreateChild,DeleteChild","Allow",$guid
    # ACE for full control over descendent User objects
    $ace2 = new-object System.DirectoryServices.ActiveDirectoryAccessRule $sid,"GenericAll","Allow","Descendents ",$guid 

    ## Add the ACE in the ACL and set the ACL on the object
    $acl.AddAccessRule($ace1)
    $acl.AddAccessRule($ace2) 

    Write-Verbose "Adding ACEs to ACL on $ou" 

    set-acl -aclobject $acl $ou 

    # Clean up variables used in ForEach loop
    Clear-Variable -ErrorAction SilentlyContinue -Name dn
    Clear-Variable -ErrorAction SilentlyContinue -Name ou
    Clear-Variable -ErrorAction SilentlyContinue -Name acl
    Clear-Variable -ErrorAction SilentlyContinue -Name guid
    Clear-Variable -ErrorAction SilentlyContinue -Name ace1
    Clear-Variable -ErrorAction SilentlyContinue -Name ace2 

} # End foreach loop 

# Clean up Global Variables
Write-Verbose "Cleaning global variables..."
Clear-Variable -ErrorAction SilentlyContinue -Name impfile
Clear-Variable -ErrorAction SilentlyContinue -Name sid
Clear-Variable -ErrorAction SilentlyContinue -Name svc
Clear-Variable -ErrorAction SilentlyContinue -Name ous 

# End
Write-Verbose "Script finished"
$VerbosePreference = "SilentlyContinue"

Powershell script to log DAG database queues to file

The other day I was doing some troubleshooting on a DAG member in a remote site.  I needed to get a picture of the copy and replay queues for the server over a period of time. To do this I wrote a small script to poll the queues at 60 second intervals over a 24 hour period.  The output is in CSV format to allow the results to examined/graphed using Excel.  I thought it might be useful to others.

#########################################################
#
# Name: Get-QueueLength.ps1
# Author: Tony Murray
# Version: 1.0
# Date: 25/01/2012
# Comment: PowerShell script to output DAG database
# queue lengths to file
#
#########################################################

$outfile = "c:\QueueLength.csv"

$server = "MyExchangeServer"

$head = "Date,Time,Database,CopyQLength,ReplayQLength"

if (Test-Path $OutFile) {Remove-Item $outfile}

Add-Content -Value $head -Path $outfile

$i = 0
do {
    $dat = Get-Date -Format d
    $tim = get-date -Format HH:mm
    $stats = Get-MailboxDatabaseCopyStatus -Server $server
    foreach ($stat in $stats) {
        $dba = $stat.databasename
        $clen = $stat.CopyQueueLength
        $rlen = $stat.ReplayQueueLength
        $line = "$dat,$tim,$dba,$clen,$rlen"
        Write-Host $line
        Write-Host $i
        Add-Content -Value $line -Path $Outfile
    } # End foreach
    $i = $i + 1
    Start-Sleep -Seconds 60
    } # End of Do
While ($i -le 1439)

What’s in a name?

 

I’ve been working with the Windows Server 2008 R2 Active Directory cmdlets quite a bit recently.  If you’ve come from a background of using ADFIND, the cmdlets take a bit of getting used, but for me it’s definitely been worth the time spent.  One thing that frustrates me a little is that the names used by the cmdlets for AD attributes tend to vary greatly.  Not only is there inconsistency between cmdlets that stem from different modules and snap-ins, but also between the cmdlets and the traditional UI tools.  The table below shows some examples:

 

AD Users and Computers

ldapDisplayName

AD cmdlet property name

Exchange 2010 cmdlet property name

Telephone Number

telephoneNumber

OfficePhone

Phone

E-mail

mail

EmailAddress

WindowsEmailAddress

P.O. Box

postofficeBox

POBox

PostOfficeBox

 

I guess the Microsoft Product Groups try to protect the long-suffering customer from the obscurity of some of the attribute LDAP display names by giving them friendly names.  This approach would work well if there were universal consistency, but it not it just becomes confusing.  My preference is always to use the ldapDisplayName values whenever possible – and certainly always with the AD cmdlets, e.g.

Get-ADUser –LDAPFilter “(samaccountname=bobm)”

If in doubt however, refer to the object model:

get-help about_ActiveDirectory_ObjectModel

Tony

Search Result differences with Export-Mailbox

I came across an anomaly with the Exchange 2007 Export-Mailbox cmdlet at a customer site recently.  It created a major inconvenience for some bulk mailbox exports, so I thought I would share it here.  Basically, I was able to generate two different search results depending on whether or not I specified a PST file as target.  I’ve since managed to reproduce the behaviour in my own test lab, so the problem appears to be generic and not limited to the specific customer’s environment.

This is what my test environment looks like:

Mailox Server  = Windows Server 2008 SP2 with Exchange Server 2007 SP2 RU5

Export workstation = Windows 7 SP1 with Outlook 2010 and Exchange Server 2007 SP2 RU5 Management Tools

My goal was to export all items that contain the string [blah] (including the square brackets) to a target.  If I specify a PST file as the target then all items that contain the specified string are exported to the PST file as expected, e.g.

Export-Mailbox -Identity c781e3a3-1e08-40a7-abab-ba71b9dddc0b -AllContentKeywords “[blah]” `
-DeleteContent:$false -DeleteAssociatedMessages:$false -PSTFolderPath $pstpath -Confirm:$false

However if I specify a folder in another mailbox as the target and use the same search string then items matching [blah] are copied to the target as well as all items matching blah (i.e. without the square brackets), e.g.

Export-Mailbox -Identity c781e3a3-1e08-40a7-abab-ba71b9dddc0b -AllContentKeywords “[blah]” `
-TargetFolder “EM” -TargetMailbox d4aa986b-c33c-4a89-9e08-1a3ceb5c796e `
-DeleteContent:$false -DeleteAssociatedMessages:$false -Confirm:$false

As you can see, the string passed to the AllContentKeywords parameter is exactly the same in both examples, but the result is different. 

I haven’t yet found a reasonable explanation for why this is happening, but it seems that the search behaviour is different depepending on whether or not the cmdlet includes the option to export to PST.  For example, a straight delete using export-mailbox (i.e. no target at all) will also match both [blah] and blah:

Export-Mailbox -Identity c781e3a3-1e08-40a7-abab-ba71b9dddc0b -AllContentKeywords “[blah]” `
-DeleteContent:$true -DeleteAssociatedMessages:$true -Confirm:$false

However combining the delete option with an export to PST will just match on [blah].

My guess is that the PST option somehow causes the cmdlet to use a different search method (or index?).  When the PST option isn’t used the cmdlet simply ignores the square brackets (and I guess any other special characters).  I haven’t yet found a way to escape the special characters to ensure they are included in all searches.  If anyone knows how to do this, please let me know.

The books that keep on giving

Good books continue to deliver value long after the publication date.  Here’s an example of how they helped repair a broken bed support recently.

img_2631_small.jpg

(with apologies to Gil Kirkpatrick, whose Active Directory Programming book has been a huge help to me over the last 10 years).

PS. Details of how exactly the bed was broken are not available at this time.

Powershell script to find objects using objectGUID value

The objectGUID attribute is a little tricky to work with, especially if you want to use it as part of an LDAP filter.  This is because the value in stored within the directory as an octet string - essentially an array of one-byte characters.  This syntax is not especially user-friendly, which is why it is typically displayed (by tools such as LDP.EXE and the AD Powershell cmdlets) in a registry string format, e.g. “af97d4c7-5f17-4ce2-9245-687d410b4b20″Another way of displaying the value is using a hex string format, e.g. “C7D497AF175FE24C9245687D410B4B20″.  If you’re into migrations using Quest Migration Manager for AD, the hex string representation is used for the matching attribute value (using either extenstionAttribute15 or adminDisplayName).  As you can see, the two representations are similar, but have a slightly different ordering of the bytes.

You’d think that you would be able to use one of these two string representations of the objectGUID as part of an LDAP search filter, wouldn’t you?  Well, no, that would be too helpful.  Instead, you need a slightly modified version of the hex string, i.e. “\C7\D4\97\AF\17\5F\E2\4C\92\45\68\7D\41\0B\4B\20″.   The search filter syntax would look like this “(objectGUID=\C7\D4\97\AF\17\5F\E2\4C\92\45\68\7D\41\0B\4B\20)”

The script below accepts either of the two string formats as input and prompts you for the naming context in which to search for the object represented by the objectGUID attribute.

#########################################################
#
# Name: Get-ADObjectFromGUID.ps1
# Author: Tony Murray
# Version: 1.0
# Date: 24/08/2010
# Comment: PowerShell script to search for an AD object
# using 'objectGUID' as the LDAP search filter
#
#########################################################

# Import the AD module
ipmo ActiveDirectory

# Get the objectGUID value
$Input1 = @'
    Please enter the objectGUID value in one
    of the two following formats:

    Registry string format, e.g.
        bb67681f-0ac1-471a-bf3d-f7f4c4cb1290
    or
    Hex string format, e.g.
        455E54E9D58B0F4B8F389E4982791D40

'@

$strGUID = Read-Host $Input1

# Present Menu
[string] $menu = @'
    Choose the AD partition to search within
    1. Current Domain
    2. Configuration
    3. Schema
    4. Other

'@

$a = Read-Host $menu
switch ($a)
{
    1 { $SearchBase = (Get-ADRootDSE).defaultNamingContext }
    2 { $SearchBase = (Get-ADRootDSE).configurationNamingContext }
    3 { $SearchBase = (Get-ADRootDSE).schemaNamingContext }
    4 { $SearchBase = Read-Host "Enter the search base: " }

}

if ($strGUID.length -eq 36)
{
    # We have a string in registry format and need to convert it to Hex string
    $strHex = -join (([guid]$strGUID).tobytearray() | %{$_.tostring("X").padleft(2,"0")})
}
elseif ($strGUID.length -eq 32)
{
    # We have a string in Hex format - no need to modify
    $strHex = $strGUID
}
else
{
    # Unrecognised string format
    Write-host "Unrecognised string format - remove any leading or trailing spaces and try again"
    Break
}

# We need to modify the Hex string to allow it to be used as a filter
$strSearch = $strHex -replace '(..)','\$1'

# Go ahead and search for the object
Get-ADObject -LDAPFilter "(objectGUID=$strSearch)" -SearchBase $SearchBase -Properties * | fl

# End

You can download the script here: get-adobjectfromguid.zip

Teched 2011 New Zealand - Powershell files

As promised, here are the Powershell data files from my Teched NZ 2011 demos.  I’ve also included the FSMO role mover script.

te_2011.zip

Let me know if you have any questions. Enjoy!

How to find duplicate sAMAccountNames between two forests

When preparing for a migration of AD objects from one forest to another it is useful to know if any of the names are going to conflict.  There are, as you probably know, a number of different naming attributes in AD, but the one most likely to cause problems in the event of a conflict is sAMAccountName.  This is because sAMAccountName is used for domain logon (assuming UPN is not used).  If you know which names conflict between source and target you can plan changes before you get into the migration itself.

I’ve written a couple of small scripts to detect sAMAccountName conflicts.  I’ve used two scripts instead of one as there is not always trust connectivity between the two forests.  The first script (SourceUsersToCSV) is run in the source domain and basically just dumps all the user sAMaccountName attribute values to a file.  The second script (DupeCheckFromCSV) is run in the target domains and reads the exported file information line-by-line and checks to see if there are any conflicts in the target domain.  If a conflict is found the sAMAccountName is written to a file.

Enjoy!

#########################################################
#
# Name: SourceUsersToCSV.ps1
# Author: Tony Murray
# Version: 1.0
# Date: 14/07/2011
# Comment: PowerShell script to export AD user info
# to CSV to support search for duplicate users
#
#
#########################################################

### --- Varible Definitions ---

$DOMAIN = "source.com"
$EXPFILE = "C:\util\CSV\domain_export.csv"
$sourceOU = "OU=MyUsers,DC=source,DC=com"
$filter = "(&(objectClass=user)(!iscriticalsystemobject=TRUE))"

### --- Main ---

# Export Source AD User info to file

if(@(get-module `
| where-object {$_.Name -eq "ActiveDirectory"} ).count -eq 0) {import-module ActiveDirectory}

$objSourceDC = Get-ADDomainController -Discover -DomainName $DOMAIN
$sourceDC = [string]$objSourceDC.HostName
$UserInfo = '' | Select 'UsrsAMAccountName'
$AllUsers = @()
$MyUsers = Get-ADUser -LDAPFilter $filter -Server $sourceDC -SearchBase $sourceOU
foreach($User in $MyUsers) {
    $UserInfo.'UsrsAMAccountName' = $User.sAMAccountname
    $AllUsers += $UserInfo | Select 'UsrsAMAccountName'
}
$AllUsers | Export-Csv $EXPFILE -NoTypeInformation
#########################################################
#
# Name: DupeCheckfromCSV.ps1
# Author: Tony Murray
# Version: 1.0
# Date: 14/07/2011
# Comment: PowerShell script to import AD User info
# from CSV to check for duplicates
#
#########################################################

### Set Global variables

$domain = "target.com"
$impfile = "C:\util\CSV\Domain_Export.csv"
$EXPFILE = "C:\util\CSV\Duplicate_Users.csv"
$dc = Get-ADDomainController -Discover -DomainName $domain
$targetdc = [string]$dc.HostName
$arrSrcSAMs=@()
$arrTarSAMs = @()
$arrDupeUsers = @()

### Load the CSV file and extract the source domain unique User names

$colsrcUsers = import-csv $impfile #| select UsrSAMAccountName

foreach ($srcUser in $colsrcUsers)
	{
		$srcSAM = $srcUser.UsrSAMAccountName
		$arrsrcSAMs += $srcSAM
	}    

# Check for empty file
if ($colsrcUsers.Count -eq 0)
    {
        write-host "No Users found in CSV import file!"
        break
    }

### Enumerate Users in the local (target) domain 

$coltarUsers = Get-ADUser -Filter '*' -Server $targetDC -Properties samaccountname

foreach ($tarUser in $coltarUsers)
	{
		$tarSAM = [string]$tarUser.samaccountname
		$arrTarSAMs += $tarSAM
	}    

### Find Users to Add or Modify

foreach ($tarUser in $arrTarSAMs)
	{
		if ($arrSrcSAMs -contains $tarUser)
		{
			write-host "Duplicate User found for " $tarUser
			$arrDupeUsers += $tarUser
		}
		else
		{
			write-host "No Duplicate found for " $tarUser
		}
	}

### Add new Users to target and apply the membership

$arrDupeUsers | out-file $EXPFILE

How to clear group membership with Powershell

Something I often recommend to my customers is keep the membership of the Enterprise Admins and Schema Admins groups empty and only populate them (temporarily) when required.  The privileges assigned to these groups are obviously high and removing the members reduces the potential for costly mistakes and/or compromise. 

Here’s a quick Powershell snippet that will perform the removal:

$grps = "Enterprise Admins", "Schema Admins"
foreach ($grp in $grps) {
Get-ADGroupMember -Identity $grp `
| %{Remove-ADGroupMember -Identity $grp -Members $_ -Confirm:$false}
}

This is something that you could consider running as a scheduled task to ensure the memberships are kept clear.

Next Page »