Comparing Active Directory Permissions


Every now and then you might be required to compare Active Directory account permissions. When it concerns one or few accounts, you could do the manual side-by-side comparison using Active Directory and Computers. However, when you need to check multiple accounts this task becomes tedious.

Now you could follow the practice laid out by Exchange fellow Andy Grogan here,  generating permissions output using Quest Active Roles and comparing the textual output with a comparison utility like WinMerge or WinDiff. But you can also perform this comparison using PowerShell’s Compare-Object cmdlet, which I’ll show you here.

For this task we’re going to use the Quest AD extensions (Active Roles), which you can download here. Install these extensions on a domain-joined system where PowerShell is already installed. After installation, start the ActiveRoles Management Shell and enter the following, where IdA and IdB are the Identities of the objects you want to compare:

$a= Get-QadPermission <IdA> -Inherited -SchemaDefault
$b= Get-QadPermission <IdB> -Inherited –SchemaDefault

Now $a and $b contain the permission sets of both objects. Next, we’re going to utilize compare-object to compare these two sets. When we use Compare-Object $a $b you get the following output:

image

Not quite helpful this output but it isn’t unexpected. Since we’re comparing two object sets compare-object generates a result with objects. We can make this more readable by specifying the PassThru parameter so we can post-process these objects, like displaying certain fields using the Format-Table cmdlet, e.g.

Compare-Object $a $b -PassThru | ft SideIndicator,AccountName,Rights,Source,ApplyTo

image

Presto! The SideIndicator  is included to see in which set the attribute is contained, e.g. “<=” means the element is contained in the 1st specified (reference) object and “=>” means its is contained in the 2nd (difference) object.

If you want to include equal objects in the output as well, add the IncludeEqual parameter to the Compare-Object cmdlet.

Active Directory Migration Tool 3.2


At last, the Active Directory Migration Tool (ADMT) 3.2 was released to public. ADMT can be used to migrate and reorganize objects across in inter-forest (cross-forest) or intra-forest scenarios.

The previous version, ADMT 3.1, dates back to July, 2008 and is supported only on Windows Server 2008, but not R2. With the release of R2 this lead to added  complexity because projects needed to introduce a different OS in their environment.

The new 3.2 version of ADMT only runs on Windows Server 2008 R2, but not on earlier versions. As you can also see from the table below, the new ADMT version requires Windows Server 2003 domain functional level in both the source and the target domain:

ADMT
Version
OS
Support
Source Domain
Target Domain
2000 2003 2008 2008
R2
2000 2003 2008 2008
R2
3.1 2008 YES YES YES YES YES YES
3.2 2008 R2 YES YES YES YES YES YES

You can download ADMT 3.2 here. The updated ADMT Guide: Migrating and Restructuring Active Directory Domains can be found here.

Note that ADMT uses Password Export Server (PES) which is currently still at version 3.1. Be advised that this version of PES isn’t supported on Windows Server 2008 R2, so I expect an update for PES as well. PES 3.1 can be downloaded here (x64) or here (x86).

Kerberos Max Token Size


Ok, not directly Exchange related but an issue I’d like to share. In one of my earlier articles you can read I’m working on on a project where we’ll be performing a cross-forest migration of accounts and Exchange mailboxes. Migrating the Active Directory user accounts is done using ADMT v3.1 with SIDHistory. No problem so far, until we noticed some migrated users weren’t receiving Group Policy Objects and experienced authorization errors from time to time. After identifying several users experiencing similar issues, we noticed the following common eventlog entries:

System eventlog (the number 3888 varied):

Event ID : 6
Source : Kerberos
The kerberos SSPI package generated an output token of size 3888 bytes, which was too large to fit in the 2e00 buffer buffer provided by process id 0. If the condition persists, please contact your system administrator.

The Application eventlog contained the following event:

Event ID : 1053
Source : UserEnv
Windows cannot determine the user or computer name. (). Group Policy processing aborted.

Turns out, Kerberos is the culprit. GPO processing aborted because their Kerberos information exceeded the maximum Kerberos token size. This problem may occur when users belong to (too) many groups (.. don’t ask). In addition, memberships coming from SIDhistory are also added to the token, roughly doubling numbers.

As MS KB articles 263693 and 327825 suggest, we raised the MaxTokenSize limited to 65535 (0xFFFF) in the following registry location (if the value is not present, create it as REG_DWORD):

HKLM\SYSTEM\CurrentControlSet\Control\LSA\Kerberos\Parameters\MaxTokenSize

After a restart, all problems were gone. This isn’t a standard GPO setting; when required, you need to create an .adm GPO template yourself which is described in KB article 938118. Hope you’ll find this information useful to keep in mind when performing your ADMT scenarios at clients with excessive group usage.

ForeFront Identity Manager 2010 RTM


By now you’ve probably already heared ForeFront Identity Manager 2010 went RTM on March 2nd. FIM 2010 is the successor to ILM, the Identity Lifecycle Manager. FIM is an solution to manage identities and credentials in heterogeneous environments. It contains functionality for user (de)provisioning, password synchronization, group management, self-service and workflow-like applications. So for instance, FIM can enable organizations to automatically create an Active Directory user with an Exchange mailbox with all the proper settings when a new employee has been entered into the HRM system (or disabled or removed when the employee leaves the organization, depending on requirements).

You can download the trial here. More information on the FIM portal here.

Cross-Forest Mailbox Move (2)


Note: This is part 2; part 1 can be found here.

After the post on experiences regarding Cross-Forest Mailbox Move, the problems with the “sample” Powershell script and the script created in good ol’ VB, I got lots of requests to publish the script. After thinking this over, I made it ready for publishing. That means stripping excessive code and changing domain names etc. to a more descriptive labels.

The script does require some explanation:

  • The script uses 1 input file (users.txt) and produces 1 output file (output.log) (included below);
  • The reason for sending output to screen (optional) as well as file is to be able to check it properly (e.g. using notepad) when running it for a set of users;
  • users.txt contains a single line with the source and target account names. This is the same file we used for ADMT imput. Reason for having a source as well as a new account name is that in the migration process account are renamed. ADMT can do this for you, but the script will need both the old and the new name name to connect to the objects and copy/set several attributes;
  • It’s VB, had been kept simple and didn’t went through a code beautifier. That means no full function headers, input/output descriptions or extensive error handling;
  • Modify the constants using information from your environment, e.g. source/target domain, servers, LegacyExchangeDN etc.
  • The script uses fixed servers. This is to make sure we’re talking to the same server(s) as ADMT and to prevent replication issues because of lag;
  • Use the script at your own risk. I cannot accept any responsibility for consequences when using this in your production environment;
  • Use it in a lab environment first; test, test, test!

Users.txt

SourceName,TargetSAM
jtest,jtest

CrossForestMovePrep.vbs

'*--------------------------------------------------------------------------------
'* Name     : PrepareForestMove
'* Created By      : Michel de Rooij
'* E-mail    : michel@eightwone.com
'* Date            : 20100217
'* Version    : 0.22
'*--------------------------------------------------------------------------------
'* Changes:
'* 0.21 Initial version
'* 0.22 Made changes to address single-value proxyAddress attributes
'*--------------------------------------------------------------------------------

Option Explicit

Const strUserfile            = "users.txt"
Const strOutputFileName            = "output.log"
Const DEBUGOUTPUT            = 1

Const conSourceServer               = "dc.olddomain.nl"
Const conSourceDomain                = "olddomain.nl"
Const contargetServer                = "dc.newdomain.com"
Const conTargetDomain                = "newdomain.com"

Const conLegacyExchangeDN            = "/o=NEWEXORG/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn="
Const conTargetEmailDomain            = "target.com"

' AD putex cmds
Const ADS_PROPERTY_CLEAR = 1
Const ADS_PROPERTY_UPDATE = 2
Const ADS_PROPERTY_APPEND = 3
Const ADS_PROPERTY_DELETE = 4

' FileSystem
Const ForWriting =2
Const ForReading =1

'*********************************************************
' MAIN
'*********************************************************

Dim oFSO, strFile, objFile, hOutputFileHandle, bProcessLine, strLine
Dim arrUsers, strUser, strCmd, strNewUser

Set oFSO = CreateObject("Scripting.FileSystemObject")
Set hOutputFileHandle= oFSO.OpenTextFile( strOutputFileName, ForWriting, True)

debug("Start")

strFile= strUserFile

if NOT oFSO.fileExists( strFile) then
 die( "Input file "& strFile& " does not exist")
end if

debug("Reading names from "& strFile)
set objFile= oFSO.OpenTextFile( strFile, ForReading, True)

while not objFile.atEndOfStream

 bProcessLine= True
 strLine= objFile.readLine

 if isEmpty(strLine) then
 bProcessLine= False
 Else
 If left(strLine, 1)= ";" Then
 bProcessLine= False
 Else
 If inStr( strLine, ",") > 0 Then
 ' Line OK
 arrUsers= split( strLine, ",")
 strUser= arrUsers(0)
 strNewUser= arrUsers(1)

 If strUser= "SourceName" Then
 ' Input file header, skip        
 bProcessLine= False
 End If
 Else
 bProcessLine= False
 debug("** INFO: Skipping line "& strLine)
 End If    
 End If
 End If

 If bProcessLine Then

 debug(strNewUser& ": Syncing Exchange Attributes from "& struser)
 syncAttributes strUser, conSourceServer, conSourceDomain, strNewUser, conTargetServer, conTargetDomain

 End If
Wend

debug("Finished")

objFile.Close
hOutputFileHandle.Close  

set hOutputFileHandle= Nothing
set objFile= Nothing
set oFSO= Nothing

wscript.quit(0)

'*******************************************************************
' Purpose: output to screen when DEBUGOUTPUT is 1, always to file
'*******************************************************************
Function debug(strMsg)
 hOutputFileHandle.write ("["& FormatDateTime(now(),4) & "] "& strMsg & chr(13)& chr(10))
 if DEBUGOUTPUT=1 then
 wscript.echo strMsg
 end if
End Function

'*********************************************************
' Purpose: terminate with message
'*********************************************************
Function die(strMsg)
 wscript.echo strMsg
 wscript.quit (1)
End Function

'*********************************************************
' displayString
' Returns string from varType item/elements
'*********************************************************
Function displayString( varObj)
 Dim tmp, item
 tmp= ""
 select case VarType( varObj)

 Case vbEmpty
 tmp= "(Empty)"
 Case vbNull
 tmp= "(Null)"
 Case vbInteger, vbLong, vbSingle, vbDouble, vbByte, vbDecimal, vbCurrency, vbDate, vbBoolean
 tmp= cStr( varObj)
 Case vbString
 tmp= varObj
 Case vbObject
 tmp= "(Object)"
 Case vbvariant
 tmp= "(Variant)"
 Case 8209
 tmp= "("& OctetToHexStr( varObj)& ")"
 Case vbArray, 8204
 For each item in varObj
 If tmp="" Then
 tmp= tmp+ item
 Else
 tmp= tmp+ ", "+item
 End If
 Next
 tmp= "["& tmp& "]"
 Case Else

 End Select

 displaystring= tmp& " #"& varType( varObj)

End Function

'*********************************************************
' OctetToHexStr
' Convert OctetString (byte array) to Hex string.
'*********************************************************
Function OctetToHexStr (arrbytOctet)
 Dim k
 OctetToHexStr = ""
 For k = 1 To Lenb (arrbytOctet)
 OctetToHexStr = OctetToHexStr & Right("0" & Hex(Ascb(Midb(arrbytOctet, k, 1))), 2)
 Next
End Function

'*********************************************************
' syncAttributes
' migrates attributes from source to target
'*********************************************************
Function syncAttributes (strUser, SourceServer, SourceDomain, strNewUser, TargetServer, TargetDomain)

 dim strDNSource, strDNTarget, objSource, objTarget, n, strMail
 strDNSource= getDN( struser, SourceServer, SourceDomain, "")
 strDNTarget= getDN( strNewuser, TargetServer, TargetDomain, "")
 If strDNSource<>"" AND strDNTarget <> "" Then
 set objSource= getObject( "LDAP://"& SourceServer& "/"& strDNSource)
 set objTarget= getObject( "LDAP://"& TargetServer& "/"& strDNTarget)

 copyAttribute "mail", objSource, objTarget, False
 copyAttribute "mailNickname", objSource, objTarget, False
 copyAttribute "msExchMailboxGuid", objSource, objTarget, False
 setAttribute "targetaddress", objSource.get( "mail"), objTarget
 copyAttribute "proxyAddresses", objSource, objTarget, True
 addAttribute "proxyAddresses", "X500:"& objSource.get( "LegacyExchangeDN"), objTarget

 strMail= objSource.get( "mail")
 n= instr( strMail, "@")
 debug( strMail)
 addAttribute "proxyAddresses", "smtp:"& left( strMail, n-1)& "@"& conTargetEMailDomain, objTarget

 setAttribute "msExchRecipientDisplayType", -2147483642, objTarget
 setAttribute "msExchRecipientTypeDetails", 128, objTarget
 setAttribute "legacyExchangeDN", conLegacyExchangeDN& objSource.get("cn"), objTarget

 objTarget.setInfo

 Else
 debug("*** ERR: Cannot retrieve DNs for Source or Target")
 syncAttributes= False
 End If
End Function

'*********************************************************
' getDN
' Retrieves the DN for a user object
'*********************************************************
Function getDN( struser, strServer, strDomain, strOU)
 dim objConn, objCmd, strQuery, objRS, strAttr, strRDNLDAP, strDNSLDAP
 strRDNLDAP= RDN2LDAPPATH( strOU)
 strDNSLDAP= DNSDomain2LDAPPath( strDomain)
 strAttr= "distinguishedName"
 set objConn= createObject( "ADODB.Connection")
 set objCmd= createObject( "ADODB.Command")
 objConn.Provider= "ADsDSOObject"
 objConn.Open "ADs provider"
 objCmd.ActiveConnection= objConn
 strQuery= "<LDAP://"& strServer
 If strServer <> "" Then
 strQuery= strQuery& "/"
 End If
 strQuery= strQuery& strRDNLDAP
 If strOU <> "" Then
 strQuery= strQuery& ","
 End If
 strQuery= strQuery& strDNSLDAP& ">"
 strQuery= strQuery+ ";(&(objectCategory=person)(objectClass=user)(SAMAccountName="& strUser&"));"& strAttr& ";subtree"
 objCmd.CommandText = strQuery
 on error resume next
 set objRS= objCmd.execute
 if err.number <> 0 Then
 debug( "*** ERR: Error "& err.number& " executing ["& strQuery& "]")
 getDN= ""
 Else
 on error goto 0
 Select Case objRS.recordCount
 Case 0
 debug( "*** ERR: User object "& struser& " not found")
 getDN= ""
 Case 1
 getDN= objRS.Fields( strAttr)
 'debug( getDN)
 Case Else
 debug("*** ERR: Ambigious user object "& struser)
 getDN= ""
 End Select
 End If
 set objRS= Nothing
 set objCmd= Nothing
 set objConn= Nothing
End Function

'*********************************************************
' DNSDomain2LDAPPath( str)
' Makes an LDAP notation for a DNS domain name
' e.g. corp.local => DC=corp,DC=local
'*********************************************************
Function DNSDomain2LDAPPath( str)
 Dim tmp1, tmp2, tmp3
 tmp1= split( str, ".")
 tmp2= ""
 For each tmp3 in tmp1
 If tmp2<>""  then
 tmp2= tmp2& ","
 End If
 tmp2= tmp2& "dc="& tmp3
 Next
 DNSDomain2LDAPPath= tmp2
End Function

'*********************************************************
' RDN2LDAPPath( str)
' Makes an LDAP notation for a Relative Distinguished Name
' e.g. Domain Accounts/3rd party => OU=3rd party,OU=Domain Accounts
'*********************************************************
Function RDN2LDAPPath( str)
 Dim tmp1, tmp2, tmp3
 tmp1= split( str, "/")
 tmp2= ""
 For each tmp3 in tmp1
 If tmp2<>"" then
 tmp2= ","& tmp2
 End If
 tmp2= "ou="& tmp3& tmp2
 Next
 RDN2LDAPPath= tmp2
End Function

'*********************************************************
' copyAttribute
' Copies attribute(s) to target
'*********************************************************
Function copyAttribute( strAttribute, objSource, objTarget, boolMulti)
dim boolUpdate, varItem
If isEmpty( objSource.get( strAttribute)) Then
  debug( strAttribute& " not set, clearing")
  objTarget.PutEx ADS_PROPERTY_CLEAR, strAttribute, 0
Else
  varItem= objSource.get( strAttribute)
  If boolMulti Then
    if isArray( varItem) Then
      debug( "Setting "& strAttribute& " to multi-value "& displayString( varItem))
      objTarget.PutEx ADS_PROPERTY_UPDATE, strAttribute, varItem
    Else
      debug( "Setting "& strAttribute& " to single-value "& displayString( varItem))
      objTarget.PutEx ADS_PROPERTY_UPDATE, strAttribute, array( varItem)
    End If
  Else
    debug( "Setting "& strAttribute& " to "& displayString( varItem))
    objTarget.Put strAttribute, varItem
  End If
End If
on error goto 0
objTarget.SetInfo
End Function

'*********************************************************
' setAttribute
' Sets attribute to target
'*********************************************************
Function setAttribute( strAttribute, strValue, objTarget)
 debug("Setting "& strAttribute& " to "& displayString(strValue))
 objTarget.Put strAttribute, strValue
 objTarget.SetInfo
End Function

'*********************************************************
' addAttribute
' Adds attribute to target
'*********************************************************
Function addAttribute( strAttribute, varAttribute, objTarget)
 dim boolUpdate, tmp
 boolUpdate= True
 If isEmpty( varAttribute) Then
 ' not set, skipping
 Else
 'on error resume next
 If isEmpty( objTarget.get( strAttribute)) Then
 boolUpdate= True
 Else
 If isArray( objTarget.get( strAttribute)) Then
 For each tmp in objTarget.get( strAttribute)
 If tmp = varAttribute Then
 boolUpdate= False
 End If
 Next
 Else
 boolUpdate= varAttribute= objTarget.get( strAttribute)
 End If
 End If
 on error goto 0
 If boolUpdate Then
 debug("Adding "& varAttribute& " to "& strAttribute)
 objTarget.PutEx ADS_PROPERTY_APPEND, strAttribute, array( varAttribute)
 Else
 debug( varAttribute& " already in "& strAttribute)
 End If
 End If
 objTarget.SetInfo
End Function

Output.log (sample)

[16:24] Start
[16:24] Reading names from users.txt
[16:24] jtest: Syncing Exchange Attributes from jtest
[16:24] Setting mail to jtest@source.nl #8
[16:24] Setting mailNickname to jtest #8
[16:24] Setting msExchMailboxGuid to (70C2360FB0330346A925172CA0473B9F) #8209
[16:24] Setting targetaddress to jtest@target.com #8
[16:24] Setting proxyAddresses to [SMTP:jtest@source.nl, X400:c=US;a= ;p=DemoOrg;o=Exchange;s=Old;g=Mr;] #8204
[16:24] Adding X500:/o=DemoOrg/ou=First Administrative Group/cn=Recipients/cn=jtest to proxyAddresses
[16:24] jtest@source.nl.nl
[16:24] Adding smtp:jtest@target.com to proxyAddresses
[16:24] Setting msExchRecipientDisplayType to -2147483642 #3
[16:24] Setting msExchRecipientTypeDetails to 128 #2
[16:24] Setting legacyExchangeDN to /o=NEWORG/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=Mr Old #8
[16:24] Finished