Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
function Push-StoreOneDriveRootPermissions {
<#
.SYNOPSIS
Post-execution function that aggregates per-batch OneDrive root permission rows and writes the cache.

.DESCRIPTION
Collects the Sites arrays returned by every Push-DBCacheOneDriveRootPermissionsBatch activity,
flattens them into a single row set, and writes OneDriveRootPermissions once via Add-CIPPDbItem.

Completeness guard: if ActualCount -ne ExpectedSiteCount the function throws and does not
call Add-CIPPDbItem (prevents replace-mode wipe on partial orchestrator failure). When counts
match (including 0 for empty tenants handled by the orchestrator parent), a single full-replace
write is performed.

Merge-on-Skip: when batch collection returns Skipped for a site, loads existing
OneDriveRootPermissions via New-CIPPDbRequest and replaces the Skipped row with the prior
Full row (matched by siteId) before writing. Transient SPO/Graph failures therefore do not
wipe previously collected grant data. Skipped rows with no prior Full row are written as-is.
DB read is skipped when no Skipped rows exist in the run.

Logs Skipped count from collection and how many were preserved from prior Full cache.

Cache row schema (one per personal site):
id/siteId, siteUrl, siteDisplayName, ownerPrincipalName, ownerObjectId, ownerDisplayName,
driveId, driveWebUrl, libraryId, libraryHasUniquePermissions, collectionStatus,
collectionError, hasNonStandardAccess (nullable boolean), permissionsJson (string),
grantCount, collectedAt.

permissionsJson is a pre-serialized JSON string of grant objects — consumers must
ConvertFrom-Json before querying grants. Grant identity for dedup:
{permissionSource}_{principalId}_{roleDefinitionId}_{permissionId}

Consumer notes:
- Rows in the cache reflect merge-on-Skip: a site that failed collection this run may still
show collectionStatus Full with prior permissionsJson if a previous Full row existed
- hasNonStandardAccess: use -eq $true / -eq $false; $null means Skipped with no prior Full
row to merge. Never use truthy checks
- DriveRootLink with named recipients produces one grant per person; count sharing links
by distinct permissionId within permissionsJson, not by grant row count (same permissionId
may appear on multiple recipient grants)

.FUNCTIONALITY
Entrypoint
#>
[CmdletBinding()]
param($Item)

$TenantFilter = $Item.Parameters.TenantFilter
$ExpectedSiteCount = [int]$Item.Parameters.ExpectedSiteCount

try {
$AllRows = [System.Collections.Generic.List[object]]::new()
foreach ($BatchResult in @($Item.Results)) {
$Sites = if ($BatchResult.Sites) { @($BatchResult.Sites) } else { @() }
foreach ($Row in $Sites) {
if ($Row) { $AllRows.Add($Row) }
}
}

$ActualCount = $AllRows.Count
if ($ActualCount -ne $ExpectedSiteCount) {
throw "OneDrive root permissions completeness check failed for $TenantFilter : expected $ExpectedSiteCount site rows, got $ActualCount"
}

$SkippedCount = @($AllRows | Where-Object { $_.collectionStatus -eq 'Skipped' }).Count
$MergedCount = 0
if ($SkippedCount -gt 0) {
$ExistingBySiteId = @{}
foreach ($Existing in @(New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'OneDriveRootPermissions')) {
$Key = [string]($Existing.siteId ?? $Existing.id)
if ($Key -and $Existing.collectionStatus -eq 'Full') {
$ExistingBySiteId[$Key] = $Existing
}
}
for ($i = 0; $i -lt $AllRows.Count; $i++) {
$Row = $AllRows[$i]
if ($Row.collectionStatus -ne 'Skipped') { continue }
$Key = [string]($Row.siteId ?? $Row.id)
if ($Key -and $ExistingBySiteId.ContainsKey($Key)) {
$AllRows[$i] = $ExistingBySiteId[$Key]
$MergedCount++
}
}
}

$RemainingSkippedCount = $SkippedCount - $MergedCount
if ($SkippedCount -gt 0) {
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "OneDrive root permissions: $SkippedCount of $ActualCount sites returned Skipped from collection; preserved $MergedCount from prior Full cache; $RemainingSkippedCount written as Skipped" -sev Warning
}

Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveRootPermissions' -Data @($AllRows) -AddCount
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $ActualCount OneDrive root permission site rows ($MergedCount merge-on-Skip) across $(@($Item.Results).Count) batches" -sev Info
return

} catch {
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to store OneDrive root permissions: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_)
throw
}
}
11 changes: 9 additions & 2 deletions Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,15 @@ function Get-GraphToken {
$AppCache = Get-CIPPAzDataTableEntity @ConfigTable -Filter $Filter
#force auth update is appId is not the same as the one in the environment variable.
if ($AppCache.ApplicationId -and $env:ApplicationID -ne $AppCache.ApplicationId) {
Write-Host "Setting environment variable ApplicationID to $($AppCache.ApplicationId)"
$CIPPAuth = Get-CIPPAuthentication
$CIPPAuth = Get-CIPPAuthentication # reload creds from KV (source of truth)
if ($env:ApplicationID -and $env:ApplicationID -ne $AppCache.ApplicationId) {
# KV and AppCache genuinely diverged — reconcile the marker to KV so we
# don't reload on every subsequent token call.
Write-Host "AppCache ApplicationId ($($AppCache.ApplicationId)) differs from KV ($env:ApplicationID); reconciling AppCache."
$null = Add-CIPPAzDataTableEntity @ConfigTable -Entity @{
PartitionKey = 'AppCache'; RowKey = 'AppCache'; ApplicationId = "$env:ApplicationID"
} -Force
}
}
$refreshToken = $env:RefreshToken
#Get list of tenants that have 'directTenant' set to true
Expand Down
3 changes: 2 additions & 1 deletion Modules/CIPPCore/Public/MCP/Get-CippMcpToolList.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ function Resolve-CippMcpNode {
}

if ($Node -is [System.Collections.IEnumerable]) {
return @($Node | ForEach-Object { Resolve-CippMcpNode -Node $_ -Spec $Spec -Depth ($Depth + 1) -Seen $Seen })
$Resolved = @($Node | ForEach-Object { Resolve-CippMcpNode -Node $_ -Spec $Spec -Depth ($Depth + 1) -Seen $Seen })
return , $Resolved
}

return $Node
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ function Invoke-CippPartnerWebhookProcessing {

try {
if ($Data.AuditUri) {
$AuditLog = New-GraphGetRequest -uri $Data.AuditUri -tenantid $env:TenantID -NoAuthCheck $true -scope 'https://api.partnercenter.microsoft.com/.default'
$ParsedAuditUri = $null
if ([System.Uri]::TryCreate([string]$Data.AuditUri, [System.UriKind]::Absolute, [ref]$ParsedAuditUri) -and
$ParsedAuditUri.Scheme -eq 'https' -and
$ParsedAuditUri.Host -eq 'api.partnercenter.microsoft.com') {
$AuditLog = New-GraphGetRequest -uri $ParsedAuditUri.AbsoluteUri -tenantid $env:TenantID -NoAuthCheck $true -scope 'https://api.partnercenter.microsoft.com/.default'
} else {
Write-LogMessage -API 'Webhooks' -message "Partner Center webhook rejected: AuditUri is not a Partner Center API URL ($($Data.AuditUri))" -Sev 'Alert'
return
}
}

switch ($Data.EventName) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
function Set-CIPPDBCacheOneDriveRootPermissions {
<#
.SYNOPSIS
Caches OneDrive root permissions for every personal site in a tenant.

.DESCRIPTION
Self-contained cache writer: enumerates personal sites via paginated Graph getAllSites,
fans out batched collection activities (20 sites each), and aggregates results in
Push-StoreOneDriveRootPermissions (PostExecution).

One row per OneDrive site in OneDriveRootPermissions with permissionsJson (compressed grant
array string), nullable hasNonStandardAccess, and collectionStatus (Full | Skipped).

Does not read OneDriveUsage or other CIPPDB caches. Requires SharePoint/OneDrive license
(Test-CIPPStandardLicense -Preset SharePoint); unlicensed tenants exit before enumeration.
Scheduling via CIPPDBCacheTypes.json is deferred — manual test:
Invoke-ExecCIPPDBCache?TenantFilter={tenant}&Name=OneDriveRootPermissions

Site enumeration dedupes getAllSites results by siteId before batching. Empty tenants write
an empty cache directly (PostExecution is skipped when there are zero batches).

PostExecution passes ExpectedSiteCount; Push-StoreOneDriveRootPermissions throws without writing
if flattened row count does not match (prevents partial replace-mode wipe). Skipped site rows
are merged with prior Full cache data when available (merge-on-Skip) so transient failures
do not overwrite good grant data.

Owner resolution uses drive.owner (not Owners group or OneDriveUsage). permissionsJson grant
paths: SiteAdmin, SiteRoleGroup, WebRoleAssignment, LibraryRoleAssignment, DriveRootGrant,
DriveRootLink. Groups are stored as principals without Entra expansion.

Consumer limitations: grant paths != effective access; unprovisioned OneDrives absent;
Skipped sites without a prior Full row have empty permissionsJson; merge-on-Skip may
restore prior Full data after transient failures; count DriveRootLink entries by distinct
permissionId (named recipients produce multiple grant rows per link); child folder/file
sharing is out of scope (SharePointSharingLinks cache).

.PARAMETER TenantFilter
Tenant to cache OneDrive root permissions for

.PARAMETER QueueId
Optional queue ID for progress tracking
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$TenantFilter,
[string]$QueueId
)

$BatchSize = 20

try {
$LicenseCheck = Test-CIPPStandardLicense -StandardName 'OneDriveRootPermissionsCache' -TenantFilter $TenantFilter -Preset SharePoint -SkipLog
if ($LicenseCheck -eq $false) {
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have SharePoint/OneDrive license, skipping OneDrive root permissions cache' -sev Debug
return
}

Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Starting OneDrive root permissions collection' -sev Debug

$RawSites = @(New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/sites/getAllSites?`$filter=isPersonalSite eq true&`$select=id,webUrl,displayName" -tenantid $TenantFilter -asapp $true)

$SiteById = @{}
foreach ($Site in $RawSites) {
if ($Site.id) { $SiteById[$Site.id] = $Site }
}
$Sites = @($SiteById.Values)
$ExpectedSiteCount = $Sites.Count

if ($ExpectedSiteCount -eq 0) {
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No personal sites found; writing empty OneDriveRootPermissions cache' -sev Debug
Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveRootPermissions' -Data @() -AddCount
return
}

$Batches = [System.Collections.Generic.List[object]]::new()
$TotalBatches = [Math]::Ceiling($Sites.Count / $BatchSize)
for ($i = 0; $i -lt $Sites.Count; $i += $BatchSize) {
$BatchSites = $Sites[$i..[Math]::Min($i + $BatchSize - 1, $Sites.Count - 1)]
$BatchNumber = [Math]::Floor($i / $BatchSize) + 1
$SiteSeeds = foreach ($Site in $BatchSites) {
[PSCustomObject]@{
id = $Site.id
webUrl = $Site.webUrl
displayName = $Site.displayName
}
}
$BatchItem = [PSCustomObject]@{
FunctionName = 'DBCacheOneDriveRootPermissionsBatch'
TenantFilter = $TenantFilter
QueueName = "OneDrive Root Permissions Batch $BatchNumber/$TotalBatches - $TenantFilter"
BatchNumber = $BatchNumber
TotalBatches = $TotalBatches
Sites = @($SiteSeeds)
}
if ($QueueId) {
$BatchItem | Add-Member -NotePropertyName 'QueueId' -NotePropertyValue $QueueId -Force
}
[void]$Batches.Add($BatchItem)
}

if ($QueueId -and $Batches.Count -gt 0) {
try {
Update-CippQueueEntry -RowKey $QueueId -TotalTasks $Batches.Count -IncrementTotalTasks
} catch {
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Could not update queue $QueueId with OneDrive root permission batch tasks: $($_.Exception.Message)" -sev Warning
}
}

$InputObject = [PSCustomObject]@{
Batch = @($Batches)
OrchestratorName = "OneDriveRootPermissions_$TenantFilter"
PostExecution = @{
FunctionName = 'StoreOneDriveRootPermissions'
Parameters = @{
TenantFilter = $TenantFilter
ExpectedSiteCount = $ExpectedSiteCount
}
}
}

$null = Start-CIPPOrchestrator -InputObject $InputObject
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Started OneDrive root permissions collection across $ExpectedSiteCount sites in $($Batches.Count) batches" -sev Debug

} catch {
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to start OneDrive root permissions collection: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_)
throw
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ function Invoke-ExecRemoveSnooze {
.FUNCTIONALITY
Entrypoint,AnyTenant
.ROLE
CIPP.Alert.ReadWrite
CIPP.AlertSnooze.ReadWrite
#>
[CmdletBinding()]
param($Request, $TriggerMetadata)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ function Invoke-ExecSnoozeAlert {
.FUNCTIONALITY
Entrypoint,AnyTenant
.ROLE
CIPP.Alert.ReadWrite
CIPP.AlertSnooze.ReadWrite
#>
[CmdletBinding()]
param($Request, $TriggerMetadata)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ function Invoke-ListSnoozedAlerts {
.FUNCTIONALITY
Entrypoint,AnyTenant
.ROLE
CIPP.Alert.Read
CIPP.AlertSnooze.Read
.DESCRIPTION
Lists alerts that have been snoozed (temporarily suppressed), filterable by cmdlet name. Returns snooze duration and scope details.
#>
Expand Down
2 changes: 1 addition & 1 deletion version_latest.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
10.6.1
10.6.2