public/Get-MsecExchangeMailboxPermission.ps1
|
function Get-MsecExchangeMailboxPermission { <# .SYNOPSIS Who can open which mailbox - one row per (mailbox, grantee, access right). .DESCRIPTION Shared mailbox access is a standing grant that survives the person who set it up, and it is invisible to every Entra-side review: a Full Access grant on a shared mailbox does not appear in group membership, in a directory role, or in any Conditional Access report. This is the only place it shows up. NOT AVAILABLE THROUGH GRAPH, and that is why this needs ExchangeOnlineManagement rather than Invoke-MsecGraphRequest like the rest of the module. Mailbox permissions are an Exchange concept - there is no /users/{id}/mailboxPermissions endpoint. Every other identity command here reads Graph; this one cannot. NT AUTHORITY\SELF IS EXCLUDED. Exchange grants every mailbox Full Access to itself, so it appears on every row and means nothing. Including it would put a meaningless finding on every mailbox and train the reader to skim past the column that matters. ONE ROW PER (MAILBOX, GRANTEE, RIGHT). A grantee holding both FullAccess and SendAs on one mailbox is two rows, because they are two separate grants made in two places - collapsing them would hide one of them being removed. .PARAMETER RecipientTypeDetails Which mailbox kinds to inspect. Default SharedMailbox, which is where standing delegated access accumulates. 'UserMailbox' covers delegate access to people's own mailboxes - a bigger and noisier set, but the same question. .PARAMETER IncludeInherited Include permissions inherited from a parent object rather than set on the mailbox itself. Excluded by default: an inherited right is a property of the organisation's RBAC, not a decision someone made about this mailbox. .EXAMPLE Connect-Msec -KeyVaultName kv-msec -TenantId <guid> -ClientId <guid> Connect-MsecExchangeOnline -Organization contoso.onmicrosoft.com Get-MsecExchangeMailboxPermission .EXAMPLE # The access review question: who can read mailboxes they do not own? Get-MsecExchangeMailboxPermission | Where-Object AccessRights -match 'FullAccess' | Sort-Object MailboxUserPrincipalName, Grantee .EXAMPLE # Grantees with access to several mailboxes - usually a service account or a leaver. Get-MsecExchangeMailboxPermission | Group-Object Grantee | Where-Object Count -gt 1 | Sort-Object Count -Descending .OUTPUTS PSCustomObject per grant, PSTypeName 'MsecExchangeMailboxPermission'. .NOTES Needs Connect-MsecExchangeOnline first - see that command for why Exchange requires a DIRECTORY ROLE and not just the Exchange.ManageAsApp app role. A mailbox whose permissions cannot be read emits a row with Grantee 'Unreadable' rather than contributing nothing, so a permission failure cannot read as a mailbox nobody has access to. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [ValidateSet('SharedMailbox', 'UserMailbox', 'RoomMailbox', 'EquipmentMailbox', 'All')] [string[]] $RecipientTypeDetails = 'SharedMailbox', [switch] $IncludeInherited ) if (-not (Get-Command Get-EXOMailbox -ErrorAction SilentlyContinue)) { throw 'Not connected to Exchange Online. Run Connect-MsecExchangeOnline -Organization <domain> first.' } $mailboxParams = @{ ErrorAction = 'Stop' } if ($RecipientTypeDetails -notcontains 'All') { $mailboxParams['RecipientTypeDetails'] = $RecipientTypeDetails } $mailboxes = @() try { $mailboxes = @(Get-EXOMailbox @mailboxParams -ResultSize Unlimited) } catch { throw "Could not list mailboxes. The msec app needs Exchange.ManageAsApp AND a directory role (Exchange Administrator, Exchange Recipient Administrator or Global Reader) - the app role alone is not enough, and Exchange reports the difference as a plain authorisation failure. Original error: $($_.Exception.Message)" } if (-not $mailboxes.Count) { Write-Warning "No mailboxes of type $($RecipientTypeDetails -join ', ') found." return } foreach ($mailbox in $mailboxes) { $common = [ordered]@{ MailboxDisplayName = $mailbox.DisplayName MailboxUserPrincipalName = $mailbox.UserPrincipalName MailboxPrimarySmtpAddress = $mailbox.PrimarySmtpAddress MailboxType = $mailbox.RecipientTypeDetails } $permissions = $null try { $permissions = @(Get-EXOMailboxPermission -Identity $mailbox.UserPrincipalName -ErrorAction Stop) } catch { # A mailbox that could not be read must not look like a mailbox nobody can open. Write-Warning "Could not read permissions on '$($mailbox.UserPrincipalName)': $($_.Exception.Message)" [PSCustomObject]($common + [ordered]@{ PSTypeName = 'MsecExchangeMailboxPermission' Grantee = 'Unreadable' AccessRights = $null IsInherited = $null Deny = $null }) continue } $emitted = 0 foreach ($permission in $permissions) { # Exchange grants every mailbox Full Access to itself - see .DESCRIPTION. if ($permission.User -eq 'NT AUTHORITY\SELF') { continue } if (-not $IncludeInherited -and $permission.IsInherited) { continue } $emitted++ [PSCustomObject]($common + [ordered]@{ PSTypeName = 'MsecExchangeMailboxPermission' Grantee = [string] $permission.User AccessRights = (@($permission.AccessRights) -join ', ') IsInherited = [bool] $permission.IsInherited Deny = [bool] $permission.Deny }) } if (-not $emitted) { # Nobody but the mailbox itself. A real answer, and a good one - said explicitly so # it is distinguishable from the Unreadable row above. [PSCustomObject]($common + [ordered]@{ PSTypeName = 'MsecExchangeMailboxPermission' Grantee = 'None' AccessRights = $null IsInherited = $null Deny = $null }) } } } |