SourcePrincipalMappingException: Tombstoned AccessRights

Recently we stumbled across some issues, while migrating mailboxes to Exchange Online. Not sure whether the kind of RecipientTypeDetails matters, but we see that permissions for mailboxes get completely stripped off. Of course this causes major trouble as users won’t be able accessing the mailbox.

We currently have a case open and trying to identify the root cause, but for now we are trying to avoid invalid entries as it seems to be related to.

What are we talking about?

When it comes to shared mailboxes, admins or any kind of provisioning process granting other users permissions e.g.: Full Access, Send As or Send on Behalf. This can be achieved easily using the UI or PowerShell and is described on Docs here.

Problem is similar to the one I described my previous blog post Exchange Online migration and TooManyBadItemsPermanetException.

When an object get deleted you will still see the stamped SID on the object and this will cause failures during migration.

Time for spring-cleaning

In order to avoid running into this issue and due to the fact it’s always a good idea doing a cleanup, I wrote a little function. The function will read the AD property msExchMailboxSecurityDescriptor and translated it to be human readable.

function Get-MBSecurityDescriptor {
    [CmdletBinding()]
    param(
        [parameter( ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$true,Mandatory=$false, Position=0)]
        [System.String[]]$SamaccountName
    )
    Begin
    {
        # retrieve SDDL from AD attribute sexchmailboxsecuritydescriptor
        function Get-msExchMailboxSecurityDescriptor
        {
            param(
                $SamaccountName
            )
            $User = ([ADSISearcher]"(SamAccountName=$SamaccountName)").FindOne().Properties
            if (-not [System.String]::IsNullOrWhiteSpace($User.msexchmailboxsecuritydescriptor))
            {
                [System.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(
                $SamaccountName
            )
            $SDDescriptor = Get-msExchMailboxSecurityDescriptor $SamaccountName
            if ($SDDescriptor)
            {
                $temp = $SDDescriptor.Sddl.Split("(") | foreach{$_.Trim(")")} |
                select @{l='RightsRaw'; e={$_.Split(';')[2]}}, @{l='SID';e={$_.Split(';')[5]}}, @{l='ACEType';e={$_.Split(';')[0] | Where-Object {($_ -eq 'D') -or ($_ -eq 'A')}}}
                $temp
            }
        }

        # map permission to readable Exchange permission
        function TranslateSDDL
        {
            param (
                [System.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
            [System.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)]
                [System.String]$SID
            )
            try
            {
                $objSID = New-Object System.Security.Principal.SecurityIdentifier("$SID")
                $objUser = $objSID.Translate( [System.Security.Principal.NTAccount])
                $objUser.Value
            }
            catch
            {
                #$_.Exception.Message
                return "N/A"
            }
        }

        # create variables
        $result = @()
        $timer = [System.Diagnostics.Stopwatch]::StartNew()
    }

    Process
    {
        ForEach ($SAM in $SamAccountName)
        {
            Write-Verbose "Processing $($SAM) processing time:$($timer.Elapsed.ToString())"
            $SIDEntrys = Get-SIDfromDescriptor $SAM | Where-Object {$_.SID -match 'S-1'}
            If ($SIDEntrys)
            {
                $objcol = @()
                ForEach ($SIDEntry in $SIDEntrys) {
                    $data = new-object PSObject
                    $data | add-member -type NoteProperty -Name Mailbox -Value $SAM
                    $data | add-member -type NoteProperty -Name SID -Value $SIDEntry.SID
                    $data | add-member -type NoteProperty -Name RightsRaw -Value $SIDEntry.RightsRaw
                    $data | add-member -type NoteProperty -Name RightsTranslated -Value $(TranslateSDDL $SIDEntry.RightsRaw)
                    $data | add-member -type NoteProperty -Name UserID -Value $((Get-UserForSID $SIDEntry.SID).Split('\')[1])
                    $data | add-member -type NoteProperty -Name Domain -Value $((Get-UserForSID $SIDEntry.SID).Split('\')[0])
                    $data | add-member -type NoteProperty -Name ACE -Value $SIDEntry.ACEType
                    $objcol += $data
                }
                $result += $objcol
            }
        }
    }

    End
    {
        $result
        $timer.Stop()
        Write-Verbose "ScriptRuntime:$($timer.Elapsed.ToString())"
    }
}

The function accepts piped objects and now I just have to perform another LDAP query and have the SamAccountName piped. In this example I just search for objects underneath a specific OU, which have homeMDB set (this was sufficient for us, but you might want to use another filter):

$broken = Get-ADUser -SearchBase 'OU=Resources,DC=contoso,DC=local' -Filter {homemdb -like '*'} |
 Get-MBSecurityDescriptor -verbose |
 Where-Object Domain -Match 'N/A'

Once we have the objects with invalid entries, we can remove these permissions:

$broken | foreach{Remove-MailboxPermission $_.Mailbox -User $_.SID -AccessRights $_.RightsTranslated -Confirm:$false}

I used on purpose only LDAP queries for querying the invalid entries. It’s a matter of performance. It took me almost 16 minutes to gather the report for approx. 16.000 shared mailboxes, but over 1 hour using Exchange Cmdlets to remove the invalid permissions from only approx. 3000.

Conclusion

It’s always good to get rid of old and invalid settings, before you run into issues. In this case the root cause for the not migrated permissions has not been 100% identified. nevertheless We decided for removing these. I hope this somehow helps others.

Leave a comment