My team recently received a request to grant editor access to every employee’s mailbox so that our homegrown time off management system could push approved entries in the calendar of end users. We are currently an Exchange Hybrid environment, and setting this up for Exchange Online Mailboxes was fairly simple as it only required an Entra AD application with Calendar.Read and Calendar.Write. On prem Exchange was a different story as there is no easy way to automatically assign just a folder level permission. While you could do so via a RBAC role that grants impersonation to the account that will be adding and removing the entries, that is a gross over permission.
Seeing that my team already has a scheduled ‘daily tasks’ PowerShell job that we use to call a number of small house keeping scripts that don’t warrant a dedicated script, we figured this would be a good area to include a function to audit this requested permission. As we started down this road, we did run into a few snags that made this a little more difficult then expected. The major issue were:
- There is no easy way to filter for folder level permissions, you need to check each mailbox individually.
- As is there is no -Filter parameter for the Get-MailboxFolderPermission cmdlet to only return mailboxes with the permission you are looking for
- If you are a multiple national or part of your user base does not speak American English, you will run into issues where the default calendar in an Exchange mailbox is in the that users localized language and will need to handle that
- The Get-MailboxFolderPermission cmdlet doesn’t seem to support the -errorvariable parameter the same way as other cmdlets. Meaning that it won’t hold the error if you also specify Ignore or SilentlyContinue
- This is only an issue if you are trying to keep your host output clean, which is a big thing for me personally
- Error trapping from the Exchange Management Shell is not great if you are trying to capture for specific exceptions, as all exceptions are all System.Management.Automation.RemoteException due to the PSSession
So with all that in mind I had do some more trickery then I originally planned for in order to handle all possible scenarios which are
- Permission needs to be added
- Permission is there but with the incorrect access right
- The mailbox has non-US English Localization and thus the Calendar is not name “calendar”
Below is the function we built and added to our daily tasks script
Function SetCalendarPermission {
<#
.SYNOPSIS
Ensures every mailbox's Calendar has the specified ServicePermission set for the specified ServiceAccount
#>
[CmdletBinding()]param(
[String]$ServiceAccount = 'ServiceTimeOff',
[String]$ServicePermission = 'Editor'
)
[System.Collections.ArrayList]$ChangeTable = @()
Try{
#TODO If we add another check that needs to check all mailboxes then lets make this a global variable
Write-Verbose "Gathering All Mailboxes"
$AllMBX = Get-Mailbox -ResultSize Unlimited -ErrorAction Stop
}#Try
Catch {
Write-Warning "Issue gathering all local mailboxes to work on"
$Global:IssueFound = $true
Throw $_
Exit 1
}#Catch
Write-Verbose "Validating that $ServiceAccount has the $ServicePermission permission on the Calendar for all mailboxes in scope"
Foreach ($MBX in $AllMBX) {
Write-Verbose "Checking : $($MBX.UserPrincipalName)"
#Create Table Entry
$TableEntry = [PSCustomObject]@{
Mailbox = $MBX.UserPrincipalName
CalendarPermissionStatus = 'UNKNOWN'
}
#TDOD I really wish there was an easier way to handle errors from the Get-MailboxFolderPermission cmdlet. It doesn't support -errorvariable without also forcing the error to the host
#You also can't capture on exception itself since it's a remote session and all the errors exception types are the same
#So in order to try and support all all known edge cases we really need to get tricky with our calls and error handling
$UserCalendarPath = "$($MBX.UserPrincipalName):\Calendar"
$CompletedCalCheck = $FALSE
do {
Try {
$CalPerm = Get-MailboxFolderPermission -Identity $UserCalendarPath -user $ServiceAccount -ErrorAction Stop
$CompletedCalCheck = $TRUE
}
Catch {
if ($_.Exception.Message -eq "There is no existing permission entry found for user: $ServiceAccount.") {
$CompletedCalCheck = $TRUE
#I dont think I need to to do this, but just being safe
$CalPerm = $NULL
}
#This will catch anyone whose mailbox is not using the English Latin alphabet
#TODO depending on how often this happens it might make more sense to check the locality first then go this route, but in my current environment this is a rare exception and not worth the extra call
Elseif ($_.Exception.Message -like "The operation couldn't be performed because '*:\Calendar' couldn't be found.") {
Write-Warning "Issue finding calendar, attempting a search : $($MBX.UserPrincipalName)"
Try {
$CalenderFolder = Get-MailboxFolderStatistics -Identity $MBX.UserPrincipalName -FolderScope Calendar -ErrorAction Stop
if ($CalenderFolder) {
#On the off chance there are more then one Calendar present we will assume the first one is the default one. I wish I could figure out a better way to handle this if it happens
$UserCalendarPath ="$($CalenderFolder[0].Identity -replace '\\',':\')"
}
}#TRY
Catch {
Write-Warning "Can't find Calendar : $($MBX.UserPrincipalName)"
$TableEntry.CalendarPermissionStatus = "ERROR : $_"
$CompletedCalCheck = $TRUE
}#CATCH
}#Elseif ($_.Exception.Message -like "The operation couldn't be performed because '*:\Calendar' couldn't be found.") {
ELse {
Write-Warning "Issue trying to pull info : $($MBX.UserPrincipalName)"
$TableEntry.CalendarPermissionStatus = "ERROR : $_"
$CompletedCalCheck = $TRUE
}#Else
}#Catch
}While ($CompletedCalCheck -ne $TRUE)
#Handle any hard errors encountered while looking for the permission
If ($TableEntry.CalendarPermissionStatus -ne "UNKNOWN") {
$ChangeTable.add($TableEntry) | Out-Null
Continue
}#If ($TableEntry.CalendarPermissionStatus -ne "UNKNOWN") {
#Handle adds and changes to the permission
If (-not $CalPerm) {
Write-Verbose "ADDING : $($MBX.UserPrincipalName)"
Try {
Add-MailboxFolderPermission -Identity $UserCalendarPath -User $ServiceAccount -AccessRights $ServicePermission -erroraction Stop | Out-Null
$TableEntry.CalendarPermissionStatus = "ADDED"
}#try
Catch {
Write-Warning "Issue adding permission : $($MBX.UserPrincipalName)"
$TableEntry.CalendarPermissionStatus = "ERROR : $_"
}#catch
}#If ($CalPerm -eq $NULL) {
#! I had a really hard time checking the accessrights as the reverse ($CalPerm.AccessRights -eq $ServicePermission) was giving me odd results
ElseIf ($CalPerm.user.ADRecipient.Name -ne $ServiceAccount -or $ServicePermission -ne $CalPerm.AccessRights) {
Write-Verbose "FIXING : $($MBX.UserPrincipalName)"
Try {
Set-MailboxFolderPermission -Identity $UserCalendarPath -User $ServiceAccount -AccessRights $ServicePermission -erroraction Stop | Out-Null
$TableEntry.CalendarPermissionStatus = "CORRECTED"
}#TRY
Catch {
Write-Warning "Issue fixing permission : $($MBX.UserPrincipalName)"
$TableEntry.CalendarPermissionStatus = "ERROR : $_"
}#Catch
}#ElseIf ($CalPerms.user.ADRecipient.Name -ne $ServiceAccount -or (-not ($CalPerms.AccessRights -ne $ServicePermission))) {
Else {
Write-Verbose "NO ACTION NEEDED : $($MBX.UserPrincipalName)"
Continue
}#Else
$ChangeTable.add($TableEntry) | Out-Null
}#Foreach ($MBX in $AllMBX) {
if ($ChangeTable.count -gt 0) {
Write-Warning "$($ChangeTable.count) mailboxes whose calendar was either set or fixed so that the $ServiceAccount has the $ServicePermission permission"
Return $ChangeTable |
Sort-Object Mailbox
}#if ($ChangeTable.count -gt 0) {
Else {
Write-Verbose "All Mailboxes have their calendar set so that the $ServiceAccount has the $ServicePermission permission"
}
}#Function SetCalendarPermission {