Skip to content

Commit e801464

Browse files
Feat: Add allTenants support for shared mailbox enabled report (KelvinTegelaar#2103)
Frontend PR: KelvinTegelaar/CIPP#6206
2 parents 0288227 + f44ff88 commit e801464

3 files changed

Lines changed: 259 additions & 0 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
function Get-CIPPSharedMailboxAccountEnabledReport {
2+
<#
3+
.SYNOPSIS
4+
Generates the "shared mailbox with enabled account" report from the CIPP Reporting database
5+
6+
.DESCRIPTION
7+
Reproduces the live Invoke-ListSharedMailboxAccountEnabled payload entirely from cached data,
8+
joining the cached 'Mailboxes' dataset (to identify SharedMailbox recipients) with the cached
9+
'Users' dataset (for accountEnabled / assignedLicenses / onPremisesSyncEnabled) by UPN. Only
10+
shared mailboxes whose user account is enabled are returned. No dedicated cache writer is needed —
11+
both source datasets are already populated on the scheduled cache cycle.
12+
13+
.PARAMETER TenantFilter
14+
The tenant to generate the report for. 'AllTenants' fans out across every tenant present in the
15+
Mailboxes cache.
16+
17+
.EXAMPLE
18+
Get-CIPPSharedMailboxAccountEnabledReport -TenantFilter 'contoso.onmicrosoft.com'
19+
#>
20+
[CmdletBinding()]
21+
param(
22+
[Parameter(Mandatory = $true)]
23+
[string]$TenantFilter
24+
)
25+
26+
try {
27+
# Handle AllTenants by recursing per tenant present in the Mailboxes cache
28+
if ($TenantFilter -eq 'AllTenants') {
29+
$AllMailboxItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'Mailboxes'
30+
$Tenants = @($AllMailboxItems | Where-Object { $_.RowKey -ne 'Mailboxes-Count' } | Select-Object -ExpandProperty PartitionKey -Unique)
31+
32+
$TenantList = Get-Tenants -IncludeErrors
33+
$Tenants = $Tenants | Where-Object { $TenantList.defaultDomainName -contains $_ }
34+
35+
$AllResults = [System.Collections.Generic.List[PSCustomObject]]::new()
36+
foreach ($Tenant in $Tenants) {
37+
try {
38+
$TenantResults = Get-CIPPSharedMailboxAccountEnabledReport -TenantFilter $Tenant
39+
foreach ($Result in $TenantResults) {
40+
$Result | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $Tenant -Force
41+
$AllResults.Add($Result)
42+
}
43+
} catch {
44+
Write-LogMessage -API 'SharedMailboxAccountEnabledReport' -tenant $Tenant -message "Failed to get report for tenant: $($_.Exception.Message)" -sev Warning
45+
}
46+
}
47+
return $AllResults
48+
}
49+
50+
# Mailboxes cache identifies which mailboxes are shared (recipientTypeDetails) and the join key (UPN)
51+
$MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' | Where-Object { $_.RowKey -ne 'Mailboxes-Count' }
52+
if (-not $MailboxItems) {
53+
throw 'No mailbox data found in reporting database. Sync the report data first.'
54+
}
55+
56+
# Users cache carries the account/license fields the live endpoint pulls from Graph /users
57+
$UserItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' | Where-Object { $_.RowKey -ne 'Users-Count' }
58+
59+
# Most-recent cache timestamp across both source datasets
60+
$CacheTimestamp = (@($MailboxItems) + @($UserItems) | Where-Object { $_.Timestamp } | Sort-Object Timestamp -Descending | Select-Object -First 1).Timestamp
61+
62+
# Build a UPN -> user lookup (hashtable string keys are case-insensitive, matching UPN semantics)
63+
$UserByUPN = @{}
64+
foreach ($Item in $UserItems) {
65+
$User = $Item.Data | ConvertFrom-Json
66+
if ($User.userPrincipalName) {
67+
$UserByUPN[$User.userPrincipalName] = $User
68+
}
69+
}
70+
71+
$Results = [System.Collections.Generic.List[PSCustomObject]]::new()
72+
foreach ($Item in $MailboxItems) {
73+
$Mailbox = $Item.Data | ConvertFrom-Json
74+
if ($Mailbox.recipientTypeDetails -ne 'SharedMailbox') { continue }
75+
76+
$User = $UserByUPN[$Mailbox.UPN]
77+
if (-not $User -or -not $User.accountEnabled) { continue }
78+
79+
# Match the live Invoke-ListSharedMailboxAccountEnabled shape exactly. 'id' must be the user's
80+
# object id — the page's "Block Sign In" action posts it to ExecDisableUser.
81+
$Results.Add([PSCustomObject]@{
82+
UserPrincipalName = $User.userPrincipalName
83+
displayName = $User.displayName
84+
givenName = $User.givenName
85+
surname = $User.surname
86+
accountEnabled = $User.accountEnabled
87+
assignedLicenses = $User.assignedLicenses
88+
id = $User.id
89+
onPremisesSyncEnabled = $User.onPremisesSyncEnabled
90+
CacheTimestamp = $CacheTimestamp
91+
})
92+
}
93+
94+
return $Results
95+
96+
} catch {
97+
Write-LogMessage -API 'SharedMailboxAccountEnabledReport' -tenant $TenantFilter -message "Failed to generate shared mailbox account enabled report: $($_.Exception.Message)" -sev Error
98+
throw
99+
}
100+
}

Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSharedMailboxAccountEnabled.ps1

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,33 @@ function Invoke-ListSharedMailboxAccountEnabled {
66
Exchange.Mailbox.Read
77
.DESCRIPTION
88
Lists shared mailboxes that have direct sign-in enabled (account not disabled), which is a security concern.
9+
Supports UseReportDB=true to read cached data from the reporting database (required for AllTenants).
910
#>
1011
[CmdletBinding()]
1112
param($Request, $TriggerMetadata)
1213

1314
$APIName = $Request.Params.CIPPEndpoint
1415
$TenantFilter = $Request.Query.tenantFilter
16+
$UseReportDB = $Request.Query.UseReportDB
1517

1618
# Get Shared Mailbox Stuff
1719
try {
20+
# If UseReportDB is specified, retrieve from the report database (cached Mailboxes + Users join)
21+
if ($UseReportDB -eq 'true') {
22+
try {
23+
$GraphRequest = Get-CIPPSharedMailboxAccountEnabledReport -TenantFilter $TenantFilter -ErrorAction Stop
24+
$StatusCode = [HttpStatusCode]::OK
25+
} catch {
26+
$StatusCode = [HttpStatusCode]::InternalServerError
27+
$GraphRequest = $_.Exception.Message
28+
}
29+
30+
return ([HttpResponseContext]@{
31+
StatusCode = $StatusCode
32+
Body = @($GraphRequest)
33+
})
34+
}
35+
1836
$SharedMailboxList = (New-GraphGetRequest -uri "https://outlook.office365.com/adminapi/beta/$($TenantFilter)/Mailbox?`$filter=RecipientTypeDetails eq 'SharedMailbox'" -Tenantid $TenantFilter -scope ExchangeOnline)
1937
$AllUsersInfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$select=id,userPrincipalName,accountEnabled,displayName,givenName,surname,onPremisesSyncEnabled,assignedLicenses' -tenantid $TenantFilter
2038
$SharedMailboxDetails = foreach ($SharedMailbox in $SharedMailboxList) {
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Pester tests for Get-CIPPSharedMailboxAccountEnabledReport
2+
# Verifies the cached Mailboxes + Users join, accountEnabled filtering, payload shape, and AllTenants fan-out
3+
4+
BeforeAll {
5+
$RepoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath))
6+
$ReportPath = Join-Path $RepoRoot 'Modules/CIPPCore/Public/Get-CIPPSharedMailboxAccountEnabledReport.ps1'
7+
8+
# Minimal stubs so Mock has commands to replace during tests
9+
function Get-CIPPDbItem { param($TenantFilter, $Type) }
10+
function Get-Tenants { param([switch]$IncludeErrors) }
11+
function Write-LogMessage { param($API, $tenant, $message, $sev) }
12+
13+
. $ReportPath
14+
15+
function New-DbItem {
16+
param($PartitionKey, $RowKey, $Data, $Timestamp)
17+
[pscustomobject]@{
18+
PartitionKey = $PartitionKey
19+
RowKey = $RowKey
20+
Timestamp = $Timestamp
21+
Data = ($Data | ConvertTo-Json -Depth 5 -Compress)
22+
}
23+
}
24+
}
25+
26+
Describe 'Get-CIPPSharedMailboxAccountEnabledReport' {
27+
BeforeEach {
28+
$script:Tenant = 'contoso.onmicrosoft.com'
29+
30+
$script:SharedMailbox = @{ UPN = 'shared@contoso.com'; recipientTypeDetails = 'SharedMailbox' }
31+
$script:RegularMailbox = @{ UPN = 'user@contoso.com'; recipientTypeDetails = 'UserMailbox' }
32+
33+
$script:EnabledUser = @{
34+
userPrincipalName = 'shared@contoso.com'
35+
displayName = 'Shared Mailbox'
36+
givenName = 'Shared'
37+
surname = 'Mailbox'
38+
accountEnabled = $true
39+
assignedLicenses = @(@{ skuId = 'sku-1' })
40+
id = 'user-id-shared'
41+
onPremisesSyncEnabled = $false
42+
}
43+
$script:RegularUser = @{
44+
userPrincipalName = 'user@contoso.com'
45+
displayName = 'Regular User'
46+
accountEnabled = $true
47+
id = 'user-id-regular'
48+
onPremisesSyncEnabled = $false
49+
}
50+
51+
$script:Now = Get-Date
52+
53+
Mock -CommandName Write-LogMessage -MockWith { }
54+
Mock -CommandName Get-Tenants -MockWith { @([pscustomobject]@{ defaultDomainName = 'contoso.onmicrosoft.com' }) }
55+
}
56+
57+
It 'joins a shared mailbox to its user and returns the live payload shape' {
58+
Mock -CommandName Get-CIPPDbItem -ParameterFilter { $Type -eq 'Mailboxes' } -MockWith {
59+
@(
60+
New-DbItem -PartitionKey $script:Tenant -RowKey 'Mailboxes-Count' -Data @{ Count = 2 } -Timestamp $script:Now
61+
New-DbItem -PartitionKey $script:Tenant -RowKey '1' -Data $script:SharedMailbox -Timestamp $script:Now
62+
New-DbItem -PartitionKey $script:Tenant -RowKey '2' -Data $script:RegularMailbox -Timestamp $script:Now
63+
)
64+
}
65+
Mock -CommandName Get-CIPPDbItem -ParameterFilter { $Type -eq 'Users' } -MockWith {
66+
@(
67+
New-DbItem -PartitionKey $script:Tenant -RowKey 'Users-Count' -Data @{ Count = 2 } -Timestamp $script:Now
68+
New-DbItem -PartitionKey $script:Tenant -RowKey 'u1' -Data $script:EnabledUser -Timestamp $script:Now
69+
New-DbItem -PartitionKey $script:Tenant -RowKey 'u2' -Data $script:RegularUser -Timestamp $script:Now
70+
)
71+
}
72+
73+
$Result = Get-CIPPSharedMailboxAccountEnabledReport -TenantFilter $script:Tenant
74+
75+
@($Result).Count | Should -Be 1
76+
$Result[0].UserPrincipalName | Should -Be 'shared@contoso.com'
77+
$Result[0].id | Should -Be 'user-id-shared'
78+
$Result[0].accountEnabled | Should -BeTrue
79+
$Result[0].onPremisesSyncEnabled | Should -BeFalse
80+
$Result[0].CacheTimestamp | Should -Not -BeNullOrEmpty
81+
# Must not leak the regular (non-shared) mailbox
82+
$Result.UserPrincipalName | Should -Not -Contain 'user@contoso.com'
83+
}
84+
85+
It 'excludes shared mailboxes whose user account is disabled' {
86+
$script:EnabledUser.accountEnabled = $false
87+
Mock -CommandName Get-CIPPDbItem -ParameterFilter { $Type -eq 'Mailboxes' } -MockWith {
88+
@(
89+
New-DbItem -PartitionKey $script:Tenant -RowKey 'Mailboxes-Count' -Data @{ Count = 1 } -Timestamp $script:Now
90+
New-DbItem -PartitionKey $script:Tenant -RowKey '1' -Data $script:SharedMailbox -Timestamp $script:Now
91+
)
92+
}
93+
Mock -CommandName Get-CIPPDbItem -ParameterFilter { $Type -eq 'Users' } -MockWith {
94+
@(New-DbItem -PartitionKey $script:Tenant -RowKey 'u1' -Data $script:EnabledUser -Timestamp $script:Now)
95+
}
96+
97+
$Result = Get-CIPPSharedMailboxAccountEnabledReport -TenantFilter $script:Tenant
98+
99+
@($Result).Count | Should -Be 0
100+
}
101+
102+
It 'returns an empty result (no throw) when the cache holds no enabled shared mailboxes' {
103+
Mock -CommandName Get-CIPPDbItem -ParameterFilter { $Type -eq 'Mailboxes' } -MockWith {
104+
@(
105+
New-DbItem -PartitionKey $script:Tenant -RowKey 'Mailboxes-Count' -Data @{ Count = 1 } -Timestamp $script:Now
106+
New-DbItem -PartitionKey $script:Tenant -RowKey '1' -Data $script:RegularMailbox -Timestamp $script:Now
107+
)
108+
}
109+
Mock -CommandName Get-CIPPDbItem -ParameterFilter { $Type -eq 'Users' } -MockWith {
110+
@(New-DbItem -PartitionKey $script:Tenant -RowKey 'u2' -Data $script:RegularUser -Timestamp $script:Now)
111+
}
112+
113+
{ Get-CIPPSharedMailboxAccountEnabledReport -TenantFilter $script:Tenant } | Should -Not -Throw
114+
@(Get-CIPPSharedMailboxAccountEnabledReport -TenantFilter $script:Tenant).Count | Should -Be 0
115+
}
116+
117+
It 'throws when no mailbox data is cached' {
118+
Mock -CommandName Get-CIPPDbItem -ParameterFilter { $Type -eq 'Mailboxes' } -MockWith { @() }
119+
Mock -CommandName Get-CIPPDbItem -ParameterFilter { $Type -eq 'Users' } -MockWith { @() }
120+
121+
{ Get-CIPPSharedMailboxAccountEnabledReport -TenantFilter $script:Tenant } | Should -Throw '*Sync the report data first*'
122+
}
123+
124+
It 'adds a Tenant column for AllTenants' {
125+
Mock -CommandName Get-CIPPDbItem -ParameterFilter { $Type -eq 'Mailboxes' } -MockWith {
126+
@(
127+
New-DbItem -PartitionKey $script:Tenant -RowKey 'Mailboxes-Count' -Data @{ Count = 1 } -Timestamp $script:Now
128+
New-DbItem -PartitionKey $script:Tenant -RowKey '1' -Data $script:SharedMailbox -Timestamp $script:Now
129+
)
130+
}
131+
Mock -CommandName Get-CIPPDbItem -ParameterFilter { $Type -eq 'Users' } -MockWith {
132+
@(New-DbItem -PartitionKey $script:Tenant -RowKey 'u1' -Data $script:EnabledUser -Timestamp $script:Now)
133+
}
134+
135+
$Result = Get-CIPPSharedMailboxAccountEnabledReport -TenantFilter 'AllTenants'
136+
137+
@($Result).Count | Should -Be 1
138+
$Result[0].Tenant | Should -Be 'contoso.onmicrosoft.com'
139+
$Result[0].UserPrincipalName | Should -Be 'shared@contoso.com'
140+
}
141+
}

0 commit comments

Comments
 (0)