Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,34 @@ function New-IdleExchangeOnlineAdapter {
$bearerTokenPattern = 'Bearer\s+[^\s]+'
$tokenAssignmentPattern = 'token[^\s]*\s*=\s*[^\s,;]+'

# Transient EXO error patterns: server-side 5xx errors and throttling (429)
$transientErrorPattern = 'server[\s-]+side[\s-]+error|throttl|too[\s-]+many[\s-]+requests|service[\s-]+unavailable|temporarily[\s-]+unavailable|bad[\s-]+gateway'

try {
$result = & $CommandName @Parameters
return $result
}
catch {
# Build error message without exposing sensitive data
$errorMessage = "Exchange Online command '$CommandName' failed"
$isTransient = $false
if ($_.Exception.Message) {
# Sanitize error message to avoid leaking tokens/secrets
$sanitized = $_.Exception.Message -replace $bearerTokenPattern, 'Bearer <REDACTED>'
$sanitized = $sanitized -replace $tokenAssignmentPattern, 'token=<REDACTED>'
$errorMessage += " | $sanitized"

# Mark retryable server-side and throttling errors as transient so the
# plan executor's Invoke-IdleWithRetry can retry the enclosing step.
if ($_.Exception.Message -imatch $transientErrorPattern) {
$isTransient = $true
}
}

$ex = [System.Exception]::new($errorMessage, $_.Exception)
if ($isTransient) {
$ex.Data['Idle.IsTransient'] = $true
}
throw $ex
}
}
Expand Down Expand Up @@ -146,10 +159,8 @@ function New-IdleExchangeOnlineAdapter {
}
}

$this.InvokeSafely('Set-Mailbox', $params)
$null = $this.InvokeSafely('Set-Mailbox', $params)
} -Force

# GetMailboxAutoReplyConfiguration: Get Out of Office settings
$adapter | Add-Member -MemberType ScriptMethod -Name GetMailboxAutoReplyConfiguration -Value {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AccessToken', Justification = 'Reserved for future Graph API integration')]
param(
Expand Down Expand Up @@ -255,7 +266,7 @@ function New-IdleExchangeOnlineAdapter {
$params['ExternalAudience'] = $Config['ExternalAudience']
}

$this.InvokeSafely('Set-MailboxAutoReplyConfiguration', $params)
$null = $this.InvokeSafely('Set-MailboxAutoReplyConfiguration', $params)
} -Force

# GetMailboxPermissions: Get FullAccess permissions for a mailbox
Expand Down Expand Up @@ -342,7 +353,7 @@ function New-IdleExchangeOnlineAdapter {
ErrorAction = 'Stop'
}

$this.InvokeSafely('Add-MailboxPermission', $params)
$null = $this.InvokeSafely('Add-MailboxPermission', $params)
} -Force

# RemoveMailboxPermission: Revoke FullAccess from a mailbox
Expand Down Expand Up @@ -373,7 +384,7 @@ function New-IdleExchangeOnlineAdapter {
ErrorAction = 'Stop'
}

$this.InvokeSafely('Remove-MailboxPermission', $params)
$null = $this.InvokeSafely('Remove-MailboxPermission', $params)
} -Force

# GetRecipientPermissions: Get SendAs permissions for a mailbox
Expand Down Expand Up @@ -459,7 +470,7 @@ function New-IdleExchangeOnlineAdapter {
ErrorAction = 'Stop'
}

$this.InvokeSafely('Add-RecipientPermission', $params)
$null = $this.InvokeSafely('Add-RecipientPermission', $params)
} -Force

# RemoveRecipientPermission: Revoke SendAs from a mailbox
Expand Down Expand Up @@ -490,7 +501,7 @@ function New-IdleExchangeOnlineAdapter {
ErrorAction = 'Stop'
}

$this.InvokeSafely('Remove-RecipientPermission', $params)
$null = $this.InvokeSafely('Remove-RecipientPermission', $params)
} -Force

# GetMailboxSendOnBehalf: Get the GrantSendOnBehalfTo list for a mailbox
Expand Down Expand Up @@ -562,7 +573,7 @@ function New-IdleExchangeOnlineAdapter {
ErrorAction = 'Stop'
}

$this.InvokeSafely('Set-Mailbox', $params)
$null = $this.InvokeSafely('Set-Mailbox', $params)
} -Force

return $adapter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ function New-IdleExchangeOnlineProvider {
PSTypeName = 'IdLE.Provider.ExchangeOnlineProvider'
Name = 'ExchangeOnlineProvider'
Adapter = $Adapter
EventSink = $null
}

$provider | Add-Member -MemberType ScriptMethod -Name ExtractAccessToken -Value $extractAccessToken -Force
Expand Down Expand Up @@ -446,6 +447,9 @@ function New-IdleExchangeOnlineProvider {

$changed = $false

# Helper: emit diagnostic event if EventSink is available
$hasEventSink = ($this.PSObject.Properties.Name -contains 'EventSink' -and $null -ne $this.EventSink)

# --- FullAccess ---
$desiredFullAccess = @($Permissions | Where-Object { $_.Right -eq 'FullAccess' })
if ($desiredFullAccess.Count -gt 0) {
Expand All @@ -456,15 +460,40 @@ function New-IdleExchangeOnlineProvider {
Where-Object { $_.AccessRight -eq 'FullAccess' -and -not $_.IsInherited } |
ForEach-Object { $_.User.ToLowerInvariant() })

if ($hasEventSink) {
$null = $this.EventSink.WriteEvent(
'Provider.ExchangeOnline.Permissions.Evaluated',
"FullAccess current state evaluated for '$mailboxSmtp'",
'EnsureMailboxPermissions',
Comment thread
blindzero marked this conversation as resolved.
@{ MailboxSmtp = $mailboxSmtp; Right = 'FullAccess'; CurrentUsers = $currentFullAccessUsers }
)
}

foreach ($entry in $desiredFullAccess) {
$userLower = ([string]$entry.AssignedUser).ToLowerInvariant()
$isPresent = $currentFullAccessUsers -contains $userLower

if ($entry.Ensure -eq 'Present' -and -not $isPresent) {
if ($hasEventSink) {
$null = $this.EventSink.WriteEvent(
'Provider.ExchangeOnline.Permissions.Applying',
"Granting FullAccess on '$mailboxSmtp' to '$($entry.AssignedUser)'",
'EnsureMailboxPermissions',
@{ MailboxSmtp = $mailboxSmtp; Right = 'FullAccess'; User = [string]$entry.AssignedUser; Action = 'Add' }
)
}
$this.Adapter.AddMailboxPermission($mailboxSmtp, [string]$entry.AssignedUser, $accessToken)
$changed = $true
}
elseif ($entry.Ensure -eq 'Absent' -and $isPresent) {
if ($hasEventSink) {
$null = $this.EventSink.WriteEvent(
'Provider.ExchangeOnline.Permissions.Applying',
"Revoking FullAccess on '$mailboxSmtp' from '$($entry.AssignedUser)'",
'EnsureMailboxPermissions',
@{ MailboxSmtp = $mailboxSmtp; Right = 'FullAccess'; User = [string]$entry.AssignedUser; Action = 'Remove' }
)
}
$this.Adapter.RemoveMailboxPermission($mailboxSmtp, [string]$entry.AssignedUser, $accessToken)
$changed = $true
}
Expand All @@ -480,15 +509,40 @@ function New-IdleExchangeOnlineProvider {
Where-Object { $_.AccessRight -match 'SendAs' -and -not $_.IsInherited } |
ForEach-Object { $_.Trustee.ToLowerInvariant() })

if ($hasEventSink) {
$null = $this.EventSink.WriteEvent(
'Provider.ExchangeOnline.Permissions.Evaluated',
"SendAs current state evaluated for '$mailboxSmtp'",
'EnsureMailboxPermissions',
@{ MailboxSmtp = $mailboxSmtp; Right = 'SendAs'; CurrentUsers = $currentSendAsTrustees }
)
}

foreach ($entry in $desiredSendAs) {
$trusteeLower = ([string]$entry.AssignedUser).ToLowerInvariant()
$isPresent = $currentSendAsTrustees -contains $trusteeLower

if ($entry.Ensure -eq 'Present' -and -not $isPresent) {
if ($hasEventSink) {
$null = $this.EventSink.WriteEvent(
'Provider.ExchangeOnline.Permissions.Applying',
"Granting SendAs on '$mailboxSmtp' to '$($entry.AssignedUser)'",
'EnsureMailboxPermissions',
@{ MailboxSmtp = $mailboxSmtp; Right = 'SendAs'; User = [string]$entry.AssignedUser; Action = 'Add' }
)
}
$this.Adapter.AddRecipientPermission($mailboxSmtp, [string]$entry.AssignedUser, $accessToken)
$changed = $true
}
elseif ($entry.Ensure -eq 'Absent' -and $isPresent) {
if ($hasEventSink) {
$null = $this.EventSink.WriteEvent(
'Provider.ExchangeOnline.Permissions.Applying',
"Revoking SendAs on '$mailboxSmtp' from '$($entry.AssignedUser)'",
'EnsureMailboxPermissions',
@{ MailboxSmtp = $mailboxSmtp; Right = 'SendAs'; User = [string]$entry.AssignedUser; Action = 'Remove' }
)
}
$this.Adapter.RemoveRecipientPermission($mailboxSmtp, [string]$entry.AssignedUser, $accessToken)
$changed = $true
}
Expand All @@ -501,6 +555,15 @@ function New-IdleExchangeOnlineProvider {
$currentDelegates = $this.Adapter.GetMailboxSendOnBehalf($mailboxSmtp, $accessToken)
$currentDelegatesLower = @($currentDelegates | ForEach-Object { $_.ToLowerInvariant() })

if ($hasEventSink) {
$null = $this.EventSink.WriteEvent(
'Provider.ExchangeOnline.Permissions.Evaluated',
"SendOnBehalf current state evaluated for '$mailboxSmtp'",
'EnsureMailboxPermissions',
@{ MailboxSmtp = $mailboxSmtp; Right = 'SendOnBehalf'; CurrentUsers = $currentDelegatesLower }
)
}

# Compute desired final list based on Present/Absent entries
$updatedDelegates = [System.Collections.Generic.List[string]]::new()
foreach ($d in $currentDelegates) { $updatedDelegates.Add($d) }
Expand All @@ -511,10 +574,26 @@ function New-IdleExchangeOnlineProvider {
$isPresent = $currentDelegatesLower -contains $userLower

if ($entry.Ensure -eq 'Present' -and -not $isPresent) {
if ($hasEventSink) {
$null = $this.EventSink.WriteEvent(
'Provider.ExchangeOnline.Permissions.Applying',
"Granting SendOnBehalf on '$mailboxSmtp' to '$($entry.AssignedUser)'",
'EnsureMailboxPermissions',
@{ MailboxSmtp = $mailboxSmtp; Right = 'SendOnBehalf'; User = [string]$entry.AssignedUser; Action = 'Add' }
)
}
$updatedDelegates.Add([string]$entry.AssignedUser)
$sobChanged = $true
}
elseif ($entry.Ensure -eq 'Absent' -and $isPresent) {
if ($hasEventSink) {
$null = $this.EventSink.WriteEvent(
'Provider.ExchangeOnline.Permissions.Applying',
"Revoking SendOnBehalf on '$mailboxSmtp' from '$($entry.AssignedUser)'",
'EnsureMailboxPermissions',
@{ MailboxSmtp = $mailboxSmtp; Right = 'SendOnBehalf'; User = [string]$entry.AssignedUser; Action = 'Remove' }
)
}
# Remove case-insensitively
$toRemove = $updatedDelegates | Where-Object { $_.ToLowerInvariant() -eq $userLower }
foreach ($r in @($toRemove)) { $updatedDelegates.Remove($r) | Out-Null }
Expand All @@ -528,6 +607,15 @@ function New-IdleExchangeOnlineProvider {
}
}

if ($hasEventSink) {
$null = $this.EventSink.WriteEvent(
'Provider.ExchangeOnline.Permissions.Result',
"EnsureMailboxPermissions completed for '$mailboxSmtp': Changed=$changed",
'EnsureMailboxPermissions',
@{ MailboxSmtp = $mailboxSmtp; Changed = $changed }
)
}

return [pscustomobject]@{
PSTypeName = 'IdLE.ProviderResult'
Operation = 'EnsureMailboxPermissions'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,13 @@ function Invoke-IdleStepMailboxPermissionsEnsure {
$effectiveWith['AuthSessionName'] = $providerAlias
}

# Inject EventSink into the provider so it can emit diagnostics events
$provider = $Context.Providers[$providerAlias]
if ($provider.PSObject.Properties.Name -contains 'EventSink' -and
$Context.PSObject.Properties.Name -contains 'EventSink') {
$provider.EventSink = $Context.EventSink
}

$result = Invoke-IdleProviderMethod `
-Context $Context `
-With $effectiveWith `
Expand Down
Loading