The good, the bad and sIDHistory

This post is about my personal journey with a cross-forest migration.

When it comes to account migration there is no way to do so without sIDHistory. It would be really hard to have a smooth migration without.

By using this attribute a end-user most likely won’t experience any impact…..unless you start doing a cleanup of this attribute.

In terms of Exchange users might see something like this

Calendar_Error

or this

Inbox_Error

But what’s behind those issues and how could you mitigate this? I was part of a migration, where those issues popped up and I’m going to describe how you could determine possible impact for end-users before it happens.

In my case a migration took place and the team just forgot about to run some tasks to rewrite ACLs on AD and Exchange objects.

Background

In short the migration goal was to migrate from DomainA to DomainB, from one Exchange Org to a new Exchange Org. The approach was using a 3rd Party tool, which means we had to deal with linked mailboxes, which will be converted to mailboxes as soon as the user account was migrated.

Status

After the users were migrated, the next step was to start removing the attribute sIDHistory.

Why removing sIDHîstory and not just leave it there?

Well, if you are already scratching the topic Kerberos Tokensize, you have no choice.

I’m now covering only those topics, which are really hitting your users first and hard. There are 3 parts, which are really important:

  • AD permission like Send-As and Write Personal-Information. Those are most likely used by shared mailboxes or by assistant
  • Mailbox permission like FullAccess
  • Mailboxfolder permission. Here the most used scenario is a delegate scenario, where someone grant another person rights to his calendar or inbox

To describe the scenario I’m going to use the following objects:

Object

Description

Resource.com Source Domain from where the user accounts get migrated
Adatum.com Target Domain towards user accounts get migrated
UserA User with linked mailbox. Account is active in source
UserB User with linked mailbox. Account is active in source
UserC Migrated user with converted mailbox. Account is active in target
Shared01 Shared mailbox. Only an object in target exist. UserA, UserB and UserC have Send-As, Write Personal Information and FullAccess

AD permission

Issue:

Now, when you open ADUC you will get all users and the assigned permission. You can see that UserA@resource.com has permissions:

ADUC01

But how does it look when you’re using EMS?

Well, as you can see Exchange reports that the user object in the target domain has the permission UserA@adatum.com:

EMS01

First thought was to add the target object as a first step and remove the source object in a second step.

Checking with Exchange we can see the object twice:

EMS02

Checking with ADUC we can distinct the objects:

ADUC02

Using ADUC we can also remove only the source object whereas using Exchange both objects were removed. I’ve tested this again in my lab with Exchange 2010 SP3 RU9 and Exchange 2013 CU8 and here only the specified account was removed and not both.

Anyways the only way to remove only the source object in the existing environment was specifying the SID. But how to check that there is such a SID on an object and how remove/replace user’s permissions in an automated way without any 3rd party product?

Resolution

The final resolution was to read the security descriptor of AD objects and search for SIDs from the source. And this only for the rights Send-As and Write Personal-Information. How can you do that?

First thing you have to know that each extended right is specified in AD and has its own GUID:

AD_Rights_01

To get the GUID I used the following function:

function Get-RightsGUID {
param (
$Right
)
try {
 $Filter = "(&(objectClass=controlAccessRight)(name=$right))"
 $root= ([ADSI]'LDAP://RootDse').configurationNamingContext
 $searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]"LDAP://$root")
 $searcher.filter = "$Filter"
 $searcher.pagesize = 1000
 $results = $searcher.findone()
 $results.Properties.rightsguid
}
catch {
 $_.ErrorMessage
}
}

Let’s find the GUID for the right ‘Send-As’

Get-RightsGUID Send-As

AD_Rights_02

As we have the GUID we now need to read the security descriptor(SD) and parse for the GUID of the right.

How can you read the SD? Well, there is a simple trick….using PS and make use of the property psbase.ObjectSecurity of the PS object itself

([ADSI](([ADSISearcher]"(samaccountname=Shared01)").FindOne().Path)).psbase.ObjectSecurity

SD_01

We are looking for the CodeProperty SDDL, but we need to do some formating…

([ADSI](([ADSISearcher]"(samaccountname=Shared01)").FindOne().Path)).psbase.ObjectSecurity.Sddl.split("(") | %{$_.Trim(")")}

SD_02

Security descriptor have a defined format in ACE strings, please read more about it here. As we have the SD and the GUID for the right, we can easily parse the output:

$allSIDs=([ADSI](([ADSISearcher]"(samaccountname=Shared01)").FindOne().Path)).psbase.ObjectSecurity.Sddl.split("(") | %{$_.Trim(")")}
$sids = $allSIDs -match $right | select @{l="SID";e={$_.Split(";")[5]}} | ?{$_ -match "S-1"}
$sids

SD_03

Putting all together you can create another function:

function Get-SIDforRight {
param (
$Right,
$Samaccountname
)
 $allSIDs=([ADSI](([ADSISearcher]"(samaccountname=$Samaccountname)").FindOne().Path)).psbase.ObjectSecurity.Sddl.split("(") | %{$_.Trim(")")}
 $sids = $allSIDs -match $right | select @{l="SID";e={$_.Split(";")[5]}} | ?{$_ -match "S-1"}
 $sids
}

With both functions you can parse the SD of object for a specific right like Send-As, which doesn’t match the DomainSID:

$root= [ADSI]'LDAP://RootDse'
$domain = New-Object System.DirectoryServices.DirectoryEntry
$domainsid= (New-Object System.Security.Principal.SecurityIdentifier($domain.objectSid[0],0)).value
$domainsid
Get-SIDforRight -Samaccountname shared01 -Right $guid | ?{$_ -notmatch $domainsid}

SD_04

Now that you have the SID from the source and you can go ahead and remove them.

Mailbox permission

A similar approach was taken with mailbox permission. Here you have the same behavior when you query Exchange for permissions

MB_01

First idea was to use the returned property SecurityIdentifier for a user

MB_02

Here we got a value of S-1-5-21-2660594480-276202035-994542512-1175 from Exchange for UserB. But is this really correct? How can we check this?

Exchange writes back to AD the mailbox permission. You can find the values in the attribute msExchMailboxSecurityDescriptor, which has the same format as a security descriptor. In order to read this attribute like a SD you need to convert the value

#get the user object
$User=([ADSISearcher]"(mailnickname=shared01)").FindOne().Properties
#get the value into a variable with type bytearray
[byte[]]$DaclByte = $User.msexchmailboxsecuritydescriptor[0]
#create a new object of ActiveDirectorySecurity class
$adDACL = new-object System.DirectoryServices.ActiveDirectorySecurity
#finally set the created object with the value of the bytearray
$adDACL.SetSecurityDescriptorBinaryForm($DaclByte)
#return object
$adDACL.Sddl.Split("(") | %{$_.Trim(")")}

MB_03

Now that you have the native SD you need to translate it somehow. Again also this SD has a format. It has a semicolon as delimiter and on the third rank you can find the right. In this case it’s “CC”, which means “Create Child” and in terms of Exchange “FullAccess”. Double-check the SIDs we have here in this SD

MB_04

You can see that both objects(source and target) of users UserA and UserB have FullAccess. Exchange shows only Adatum*, while you can see in the SD that both SIDs are present. Below I queried the attributes from AD and in the SIDHistory attribute the SID of the source is shown.

Looking at the CmdLet Add-Mailboxpermission we can set 6 different rights, which ends in the following mapping:

# WD = ChangePermission
# WO = ChangeOwner
# SD = DeleteItem
# LC = ExternalAccount
# CC = FullAccess
# RC = ReadPermission
# SendAS is an AD-permission

When you put everything together you have a function, which is retrieving the attribute msExchMailboxSecurityDescriptor, extract the SIDs, which are not match the DomainSID and the translated rights. I called it Check-SIDfromMBSecurity

function Check-SIDfromMBSecurity {
param(
 [parameter( ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$true,Mandatory=$false, Position=0)]
 [string]$Alias
)
process {
# retrieve local domain SID
$root= [ADSI]'LDAP://RootDse'
$domain = New-Object System.DirectoryServices.DirectoryEntry
$domainsid= (New-Object System.Security.Principal.SecurityIdentifier($domain.objectSid[0],0)).value
#$domainsid
try {
# retrieve SDDL from AD attribute sexchmailboxsecuritydescriptor
function Get-msExchMailboxSecurityDescriptor {
param(
$Samaccount
)
 $User=([ADSISearcher]"(mailnickname=$Samaccount)").FindOne().Properties
 [byte[]]$DaclByte = $User.msexchmailboxsecuritydescriptor[0]
 $adDACL = new-object System.DirectoryServices.ActiveDirectorySecurity
 $adDACL.SetSecurityDescriptorBinaryForm($DaclByte)
 $adDACL
}
# convert SDDL to readable form from attribute msexchmailboxsecuritydescriptor
function Get-SIDfromDescriptor {
param(
$Samaccount
)
 $temp = (Get-msExchMailboxSecurityDescriptor $Samaccount).Sddl.Split("(") | %{$_.Trim(")")} | select @{l='RightsRaw';e={$_.Split(';')[2]}},@{l='SID';e={$_.Split(';')[5]}},@{l='ACEType';e={$_.Split(';')[0] | ?{($_ -eq 'D') -or ($_ -eq 'A')}}}
 $temp
}
# map permission to readable Exchange permission
function TranslateSDDL {
param (
[string]$SDDL
)
 $temp = $SDDL -split '(?<=\G\w{2})(?=\w{2})'
 # WD = ChangePermission
 # WO = ChangeOwner
 # SD = DeleteItem
 # LC = ExternalAccount
 # CC = FullAccess
 # RC = ReadPermission
 # SendAS is an AD-permission
 [string]$TranslatedRights = ""
 ForEach ($right in $temp) {
  switch ($right) {
   "WD" {$TranslatedRights += "ChangePermission,"}
   "WO" {$TranslatedRights += "ChangeOwner,"}
   "SD" {$TranslatedRights += "DeleteItem,"}
   "LC" {$TranslatedRights += "ExternalAccount,"}
   "CC" {$TranslatedRights += "FullAccess,"}
   "RC" {$TranslatedRights += "ReadPermission,"}
   default {"Unkown $($right)"}
  }
 }
$TranslatedRights.Trim(',')
}
# resolve a user for a given SID
function Get-UserForSID {
param (
 [parameter( Mandatory=$true, Position=0)]
 [String]$SID
)
try {
 $objSID = New-Object System.Security.Principal.SecurityIdentifier("$SID")
 $objUser = $objSID.Translate( [System.Security.Principal.NTAccount])
 $objUser.Value
catch {
 $_.Exception.Message
}
}
$oldSIDs=Get-SIDfromDescriptor $alias | ?{($_.SID -notmatch $domainsid) -and ($_.SID -match 'S-1')}
If ($oldSIDs) {
 $objcol = @()
 ForEach ($oldSID in $oldSIDs){
  $data = new-object PSObject
  $data | add-member -type NoteProperty -Name Mailbox -Value $alias
  $data | add-member -type NoteProperty -Name OldSID -Value $oldSID.SID
  $data | add-member -type NoteProperty -Name RightsRaw -Value $oldSID.RightsRaw
  $data | add-member -type NoteProperty -Name RightsTranslated -Value $(TranslateSDDL $oldSID.RightsRaw)
  $data | add-member -type NoteProperty -Name UserID -Value $((Get-UserForSID $oldSID.SID).Split('\')[1])
  $data | add-member -type NoteProperty -Name Domain -Value $((Get-UserForSID $oldSID.SID).Split('\')[0])
  $data | add-member -type NoteProperty -Name ACE -Value $oldSID.ACEType
  $objcol += $data
 }
$objcol
}
}
Catch {
 $Error[0].Exception
}
}
}

MB_05Of course the returned domain in those cases here is the local domain Adatum. I will cover this later, but for now I can tell you it’s because the SID could be found in the attribute SIDHistory.

Anyways this gives you now the posibility to do a cleanup, which means either to replace or remove permission entries.

In my case we first exported all the permissions to a file in order to have a backup and an idea how many mailboxes were affected.Then we first removed the permission and added them again (of course with the correct object!).

Mailboxfolderpermission

This part was really the hardest one as there is no AD attribute, which you could parse to get the raw SD to look for non-local domain SID entries. So far the only way to get the property PR_NT_Security_Descriptor property of a mailboxfolder in a human readable format is MFCMAPI and MrMAPI (which plays a major role later!”).

As an example I made UserB as delegate for UserA. So UserB has access on Calendar and Inbox

MBFolder_01

Looks okay, but when you look at the property PR_NT_Security_Descriptor you can see that Exchange stamped the SID from the source on the folder

MBFolder_02

Now the question was how to get all the affected mailboxes and folders? When you have a cross-forest migration and you’re working with linked mailbox, you will end in the situation that you will have folders with SIDs from the source domain, when the users were added to the folder before they were migrated, means they were a linked mailbox. Whereas when users with normal mailbox were added the domain local SID is used and stamped on the mailbox. To get this proven we will use UserC, which was migrated the same way as the other users, but the difference to UserB is that he was added as a delegate after he was migrated. So at the time he was added his mailbox was not a linked mailbox. The SD looks like this now:

Security Descriptor:

Security Info: 0x0

Security Version: 0x0003 = SECURITY_DESCRIPTOR_TRANSFER_VERSION

Descriptor:

Account: ADATUM\userb

SID: S-1-5-21-2660594480-276202035-994542512-1175

Access Type: 0x00000000 = ACCESS_ALLOWED_ACE_TYPE

Access Flags: 0x00000002 = CONTAINER_INHERIT_ACE

Access Mask: 0x001208AB = fsdrightListContents | fsdrightCreateItem | fsdrightReadProperty | fsdrightExecute | fsdrightReadAttributes | fsdrightViewItem | fsdrightReadControl | fsdrightSynchronize

Account: ADATUM\userb

SID: S-1-5-21-2660594480-276202035-994542512-1175

Access Type: 0x00000001 = ACCESS_DENIED_ACE_TYPE

Access Flags: 0x00000002 = CONTAINER_INHERIT_ACE

Access Mask: 0x000D4114 = fsdrightCreateContainer | fsdrightWriteProperty | fsdrightWriteAttributes | fsdrightOwner | fsdrightWriteSD | fsdrightDelete | fsdrightWriteOwner

Account: ADATUM\userb

SID: S-1-5-21-2660594480-276202035-994542512-1175

Access Type: 0x00000000 = ACCESS_ALLOWED_ACE_TYPE

Access Flags: 0x00000009 = OBJECT_INHERIT_ACE | INHERIT_ONLY_ACE

Access Mask: 0x001F0FBF = fsdrightListContents | fsdrightCreateItem | fsdrightCreateContainer | fsdrightReadProperty | fsdrightWriteProperty | fsdrightExecute | fsdrightReadAttributes | fsdrightWriteAttributes | fsdrightViewItem | fsdrightWriteSD | fsdrightDelete | fsdrightWriteOwner | fsdrightReadControl | fsdrightSynchronize | 0x600

Account: ADATUM\userc

SID: S-1-5-21-398472632-1282482148-99536377-1236

Access Type: 0x00000000 = ACCESS_ALLOWED_ACE_TYPE

Access Flags: 0x00000002 = CONTAINER_INHERIT_ACE

Access Mask: 0x001208AB = fsdrightListContents | fsdrightCreateItem | fsdrightReadProperty | fsdrightExecute | fsdrightReadAttributes | fsdrightViewItem | fsdrightReadControl | fsdrightSynchronize

Account: ADATUM\userc

SID: S-1-5-21-398472632-1282482148-99536377-1236

Access Type: 0x00000001 = ACCESS_DENIED_ACE_TYPE

Access Flags: 0x00000002 = CONTAINER_INHERIT_ACE

Access Mask: 0x000D4114 = fsdrightCreateContainer | fsdrightWriteProperty | fsdrightWriteAttributes | fsdrightOwner | fsdrightWriteSD | fsdrightDelete | fsdrightWriteOwner

Account: ADATUM\userc

SID: S-1-5-21-398472632-1282482148-99536377-1236

Access Type: 0x00000000 = ACCESS_ALLOWED_ACE_TYPE

Access Flags: 0x00000009 = OBJECT_INHERIT_ACE | INHERIT_ONLY_ACE

Access Mask: 0x001F0FBF = fsdrightListContents | fsdrightCreateItem | fsdrightCreateContainer | fsdrightReadProperty | fsdrightWriteProperty | fsdrightExecute | fsdrightReadAttributes | fsdrightWriteAttributes | fsdrightViewItem | fsdrightWriteSD | fsdrightDelete | fsdrightWriteOwner | fsdrightReadControl | fsdrightSynchronize | 0x600

For UserC the SID S-1-5-21-398472632-1282482148-99536377-1236 is reported and not S-1-5-21-2660594480-276202035-994542512-1179 from the source domain

MBFolder_03

The next try was to use EWS and query the permission

MBFolder_04

Okay, the SID for UserC is correct, but the one for UserB is incorrect. The problem is that EWS reports only what Exchange resolves. And even less. When you have invalid entries like a disabled user it won’t be return. Orphaned entries like ‘NT User:*****’ won’t be returned. Those will be listed only when you use the CmdLet Get-MailboxFolderPermission. So how to overcome this issue and get to know which mailbox might be affected?

At this point I really would like to thank MCM Danijel Klaric, who came up with the idea to use MrMapi.exe to parse the property PR_NT_Security_Descriptor!

I wrote a script to get all the permission and some other data from the mailboxes using EWS, but I couldn’t find a way to read the property 0x0E27 on a folder. I was able to get the binary blob, but couldn’t find a way to convert it into a readable format. By using MrMapi.exe I was able to do so and in the end I had a combination of a script using EWS and MrMapi.

I will publish this script in the near future, but for now I can show you the difference. Pure EWS looks like this

MBFolder_05

Exchange CmdLet reports this

MBFolder_06

I disabled UserC’s mailbox. EWS doesn’t report it, but Exchange CmdLet does. And now using MrMapi.exe and parsing the property

MBFolder_07

The script returns some values:

Property

Description

Mailbox the mailbox, which was parsed
User if resolvable the user
SIDinSD the SID stamped on the folder
Foldername foldername
FolderType type of the folder
Folderpath path of folder within the mailbox
EwsID EWS value of the folderID
StoreID to storeID converted EwsID. So you can use this ID to access the folder using Exchange CmdLets

Main part for the script was taken from Glen Scales. I modified it and wrapped a script around it so it accepts some parameters. What is really efficient is that I made it mutli-threaded, but more about it in the future.

Now I was able to identify mailboxes and the solution was almost the same as with the mailbox permission….removing and to add them again with the correct object.

First get a list of affected mailboxes and save the output into a file

$root= [ADSI]'LDAP://RootDse'
$domain = New-Object System.DirectoryServices.DirectoryEntry
$domainsid= (New-Object System.Security.Principal.SecurityIdentifier($domain.objectSid[0],0)).value
Get-Mailbox -Database ex02-db01 | .\Get-MailboxFolderPermissionEWS.ps1 -Impersonate -MultiThread -UseMrMapi -MrMapi C:\Scripts\mrmapi.exe |
?{($_.User -notmatch 'ANONYMOUSLOGON')-and ($_.User -ne $null) -and ($_.User -notmatch 'Everyone') -and ($_.SIDinSD -notmatch $domainsid)} |
| Export-Csv -NoTypeInformation -Path .\ForeignSIDs.csv

MBFolder_08

Second is to retrieve for all affected mailboxes the current permission and also write them into a file(just to be on the safe side as a backup and easier clean-up). Therefore I needed a small function to read the objects from the file and query the permissions with Exchange CmdLets

function Get-FolderPermission {
 param(
 [parameter( ValueFromPipelineByPropertyName=$true,Mandatory=$true, Position=0)]
 [Alias('Mailbox')]
 [string]$MB,
 [parameter( ValueFromPipelineByPropertyName=$true,Mandatory=$false, Position=1)]
 [Alias('StoreID')]
 [string]$Folder,
 [parameter( ValueFromPipelineByPropertyName=$true,Mandatory=$false, Position=2)]
 [Alias('User')]
 [string]$ID
 )
 process {
 If (!(Get-Command Get-MailboxFolderStatistics -ErrorAction silentlycontinue)){
 Write-Warning "No CmdLets found!"
 Exit
 }
 $Error.Clear()
 $objcol = @()
 $Perms = Get-MailboxFolderPermission $($MB+":"+$Folder) -User $ID
 If (!($Error.count -gt 0)) {
  $data = new-object PSObject
  $data | add-member -type NoteProperty -Name Mailbox -Value $MB
  $data | add-member -type NoteProperty -Name User -Value $ID
  $data | add-member -type NoteProperty -Name FolderName -Value $Perms.FolderName
  $data | add-member -type NoteProperty -Name AccessRights -Value $($Perms.AccessRights -join ',')
  $data | add-member -type NoteProperty -Name FolderID -Value $Folder
  $data | add-member -type NoteProperty -Name Valid -Value 'True'
  $objcol += $data
}
Else {
 $data = new-object PSObject
 $data | add-member -type NoteProperty -Name Mailbox -Value $MB
 $data | add-member -type NoteProperty -Name User -Value $ID
 $data | add-member -type NoteProperty -Name FolderName -Value 'n/a'
 $data | add-member -type NoteProperty -Name AccessRights -Value 'n/a'
 $data | add-member -type NoteProperty -Name FolderID -Value $Folder
 $data | add-member -type NoteProperty -Name Valid -Value 'False'
 $objcol += $data
}
$objcol
}
}
#import from previous file
$affected = Import-Csv .\ForeignSIDs.csv
#pipe the import into the function and write it into a file
$affected | Get-FolderPermission | Export-Csv -NoTypeInformation -Path .\ForeignSIDs_Perm_Backup.csv

Now you have a final file with all the necessary data. Import the last file and then remove and add the permission with the following function

function Clean-FolderPermission {
 Param (
 [parameter( ValueFromPipelineByPropertyName=$true,Mandatory=$true, Position=0)]
 [Alias('Mailbox')]
 [string]$MB,
 [parameter( ValueFromPipelineByPropertyName=$true,Mandatory=$false, Position=1)]
 [Alias('FolderID')]
 [string]$Folder,
 [parameter( ValueFromPipelineByPropertyName=$true,Mandatory=$false, Position=2)]
 [Alias('AccessRights')]
 [string]$Rights,
 [parameter( ValueFromPipelineByPropertyName=$true,Mandatory=$false, Position=3)]
 [Alias('User')]
 [string]$UserID,
 [parameter( ValueFromPipelineByPropertyName=$true,Mandatory=$false, Position=4)]
 [Alias('Valid')]
 [string]$Status,
 [bool]$WhatIf=$True
 )
 If ($WhatIf){
 $Expr= "Remove-MailboxFolderPermission $($MB+":"+$Folder) -User $UserID -WhatIf"
 Invoke-Expression $Expr
 If ($Status -eq 'True'){
 $Expr= "Add-MailboxFolderPermission $($MB+":"+$Folder) -User $UserID -AccessRights $Rights -WhatIf"
 Invoke-Expression $Expr
 }
 }
 Else {
 Remove-MailboxFolderPermission $($MB+":"+$Folder) -User $UserID -Confirm:$false
 If ($Status -eq 'True'){
 $Expr= "Add-MailboxFolderPermission $($MB+":"+$Folder) -User $UserID -AccessRights $Rights"
 Invoke-Expression $Expr
 }
 }
 }
#import the backup of permissions
$final = Import-Csv .\ForeignSIDs_Perm_Backup.csv
#pipe the import into the function to clean
$final | %{$_ | Clean-FolderPermission -WhatIf 0}

Now let’s check the Calendar of UserA and the SIDs stamped on this folder

Get-Mailbox usera | .\Get-MailboxFolderPermissionEWS.ps1 -Impersonate -CalendarOnly -UseMrMapi -MrMapi C:\Scripts\mrmapi.exe

MBFolder_09

Note: Those step you will do only after the account migration took place!

The question why?

Why are we not able to see the correct user as soon as the attribute sIDHistory is used?

This behavior is by design. When you read the following article it’s getting more clear:

Implementation

LookupAccountSid will call into LsaLookupSids with a single SID to resolve. So LsaLookupSids is covered in this section.

LSA on the computer that the call is sent to (using the LSA RPC interface) will resolve the SIDs it can map and send on the remaining unresolved SIDs to a domain controller in the primary domain. The domain controller will resolve additional SIDs to account names from the local database, including SIDs found in SidHistory on a global catalog.

If SIDs cannot be resolved there, the domain controller will send remaining SIDs to domain controllers in a trusted domain where the domain part of the SID matches the trust information.

So either way calling the function LsaLookupSids or LookupAccountSid the domain controller will always return the local object and this is what you will see.

Challenges

Limitations

As mentioned we used a 3rd party tool for the migration. We stumbled across some limitations/issues:

Limitation #1

The first is that it seems to be a known issue related to folders with foldertype IPF.Appointment means all Calendar. When you have users with exact this permission “LimitedDetails” or in Outlook “Free/Busy time.subject,location”

Limits_01

In this case the permission were set to “AvailabilityOnly” or in Outlook “Free/Busy time”

Limits_02

Of course this shouldn’t be a big deal…..but not when you have a whole bunch of rooms, which are restricted with a BookinPolicy. Guess what the default permission Exchange is granting those users? Yes, “LimitedDetails” or in Outlook “Free/Busy time.subject,location”.

Limitation #2

Another limitation is that the toll stopped processing a folder as soon as there was a SID, which couldn’t be resolved. In this case all entries of this folder were skipped and the stamped SID was not replaced.

Limitation #3

This limitation gave us some headaches. It took a while until we figured out what’s going on. We got for some mailboxes an error 404 (the tool uses EWS). Some network and Fiddler traces revealed that the tool tried to connect to the mailbox straight using the mailbox server(we have split roles). In the end the problem was really easy to solve:

 The attribute msExchHomeServerName was not matching with the name of the server where the database was active. This is also by design, when you have a DAG with multiple copies. Nevertheless the tool just throw an error ‘Could not find client access server for database xxx. Use server xxx.’

I changed the attribute to the matching servername and the tool was able to process the mailboxes.

Issues

Issue #1

I modified on many mailboxes the mailboxpermissions like FullAccess. Afterwards we received some calls from users. They got prompted for credentials when they tried to access shared mailboxes. We checked everything on client-side and also the permission on the shared mailbox. In the end I compared the attribute msExchMailboxSecurityDescriptor with the output from the CmdLet Get-MailboxPermission. Use the following function to do so:

function Check-SecurityDescriptor {
param(
 [parameter( ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$true,Mandatory=$false, Position=0)]
 [string]$Alias
)process {
# retrieve SDDL from AD attribute sexchmailboxsecuritydescriptor
function Get-msExchMailboxSecurityDescriptor {
param(
$MailNick
)
 $User=([ADSISearcher]"(mailnickname=$MailNick)").FindOne().Properties
 [byte[]]$DaclByte = $User.msexchmailboxsecuritydescriptor[0]
 $adDACL = new-object System.DirectoryServices.ActiveDirectorySecurity
 $adDACL.SetSecurityDescriptorBinaryForm($DaclByte)
 $adDACL
}# convert SDDL to readable form from attribute msexchmailboxsecuritydescriptor
function Get-SIDfromDescriptor {
param(
$MailNick
)
 $temp = (Get-msExchMailboxSecurityDescriptor $MailNick).Sddl.Split("(") | %{$_.Trim(")")} | select @{l='RightsRaw';e={$_.Split(';')[2]}},@{l='SID';e={$_.Split(';')[5]}},@{l='ACEType';e={$_.Split(';')[0] | ?{($_ -eq 'D') -or ($_ -eq 'A')}}}
 $temp
}
#get SD
[array]$SD = Get-SIDfromDescriptor -MailNick $Alias | ?{$_.SID -like 'S-*'} | %{$_.SID}
#get MB permission
$MBPerms = Get-MailboxPermission $Alias | ?{$_.IsInherited -like 'False'}
$objcol = @()
ForEach ($SID in $MBPerms) {
 If ($SID.User -notlike 'NT AUTHORITY\SELF') {
  $data = new-object PSObject
  $data | add-member -type NoteProperty -Name Mailbox -Value $Alias
  $data | add-member -type NoteProperty -Name User -Value $SID.User
  $data | add-member -type NoteProperty -Name AccessRights -Value $($SID.AccessRights -join ',')
  $data | add-member -type NoteProperty -Name SID -Value $SID.User.SecurityIdentifier.Value
  #Write-Host "Checking " $SID.User
  #$SID.User.SecurityIdentifier.Value
  If ($SD -contains $SID.User.SecurityIdentifier.Value) {
   #Write-Host -fore green "$($SID.User) is in SD!"
   $data | add-member -type NoteProperty -Name Valid -Value 'True'
  }
  Else {
   #Write-Host -fore red "$($SID.User) is NOT in SD!"
   $data | add-member -type NoteProperty -Name Valid -Value 'False'
  }
 }
 $objcol += $data
}
$objcol
}
}

This is how it looks like

Issues_01

Issue #2

I stumbled across some mailboxes where I got an error that the ACE table could not be updated. This error came when I tried to fix folderpermissions. The only way was to use Outlook to get those fixed. But this was really rare.

Conclusion

When you’re doing a cross-forest migration with 3rd party tolls, I strongly encourage you to use them to get the job done! I wrote this article not in order to have a replacement for those tools. But also those tools have their limitations or known issues. With this article at least you can have a plan B when something went wrong. In my case some parts of the infrastructure was already removed and couldn’t be recovered or it made no sense to rebuild it.

I hope this helps someone when you run into a similar situation.

Update:

The script I used to collect the folder-level permissions and the SIDs is now available for download.

Please have a look at the post here and direct link to download is here.

13 thoughts on “The good, the bad and sIDHistory

  1. Great article! For those curious – if using Dell Software’s Migration Manager to perform a cross forest AD & Exchange migration you can use their accompanying Exchange Processing Wizard (EPW) and Active Directory Processing Wizard (ADPW) to update Exchange and AD permissions post-migration to remove any reliance on SIDHistory within those components.

    Unfortunately all too many migrations finish without considering this cleanup as part of the project scope. This leaves future administrators with a real challenge, especially if the migration console software was decommissioned and no longer available. Your article will come in real handy for people in this camp.

    Also important not to forget about any file system permissions that may not have been updated during the migration, and could be relying on SIDHistory. If cleanup wasn’t completed as a part of the migration, there is a good chance there was further negligence! SQL can also rely on SIDHistory too…

    Before removing SIDHistory, it is a good idea to spot check NTFS, AD, Exchange & SQL permissions for any source domain SID values.

    Like

    • Hi Brad, I absolutely agree. As mentioned I touched only some part of a migration. Sadly the limitations I mentioned above are related to the product you mentioned. The major part of the folder-level permission were done with this SW, but the rest I did with my scripts. AD and Mailbox permission I did completely by myself.
      Nevertheless, you really should use your product as this is really very special and in case of issues you should have support from the vendor and again: I covered only the Exchange part.

      Like

  2. Pingback: Get mailbox folder permissions using EWS multithreading | The clueless guy

  3. Thanks for this great post. We want to keep the Sid History in the target.

    3 questions if you do not mind.

    1. Is it necessary to change the mailbox permissions that rely on SID history to work, into target forest mailbox permissions?

    2. After the migration is complete and the source Forest is decommissioned, the trust is also deleted. Would the mailbox permissions with the source sid still work (assuming they are in the sid history of a user object in the target)

    3. Will the friendly name still appear in the mailbox permissions in ems after the source forest is decommissioned or will it show the SID?

    You reminded me of a technet post I wrote regarding what I saw in mailbox permissions:
    https://social.technet.microsoft.com/Forums/exchange/en-US/192aff17-0cd7-41e8-99dc-cbdc977ed8ba/full-access-permissions-to-a-mailbox-in-userresource-forest-configuration-where-sid-of-user-forest?forum=exchange2010#192aff17-0cd7-41e8-99dc-cbdc977ed8ba

    Like

    • Hi mstuffnet,
      you’re welcome! I hope the post helps. In general you don’t have to remove the sIDHistory. In fact most people just leave it. it really depends on the environment and setup. As long as the old SID is found on the object everything is working. Even when the source domain was decomissioned. DC/GC is always looking at the objectSID AND sIDHistory attribute first in his domain as described here for LsaLookupSids or LookupAccountSid(have a look in the post for the links). EMS will show you the friendly name as long as the SID can be resolved. If not you will see the SID.
      Ciao,
      Ingo

      Like

  4. Pingback: Get-DatabaseEvent: Who deleted my items? | The clueless guy

  5. Pingback: 获取邮箱目录权限 - Exchange中文站

    • Hi Ziemek, of course you can have nested functions. I used the functions as they were posted. As of today I might would have wrote them differently, but that’s because you always improve your knowledge.

      Like

  6. Where in the process can you identify if sidhistory is even been used? I am coming into a relatively new environment and my task is to clear out sidhistory, but no one knows if sidhistory is being used on ACL’s for AD or Exchange.

    Like

    • Hi,
      the values in sIDHistory might be used wherever you grant someone permission. As I described you can see this only with tools, which reads raw ACL and don’t let the Domain Controller resolve the SID. This post describes exact this for Exchange. When you check permissions in AD or on a filer, you need to make sure you read raw SID and make sure it’s the one from the actual AD. If you find a foreign SID…most likely sIDHistory of someone is used.
      Ciao,
      Ingo

      Like

  7. Pingback: Exchange Online migration and TooManyBadItemsPermanetException | The clueless guy

Leave a comment