Private/RoleManagement/Invoke-PIMRoleDeactivation.ps1
function Invoke-PIMRoleDeactivation { <# .SYNOPSIS Deactivates selected active PIM roles. .DESCRIPTION Handles the deactivation of active PIM roles including: - Both Entra ID directory roles and PIM-enabled groups - Progress tracking with splash screen - Comprehensive error handling .PARAMETER CheckedItems Array of checked ListView items representing the active roles to deactivate. .PARAMETER Form Reference to the main form for UI updates. .EXAMPLE Invoke-PIMRoleDeactivation -CheckedItems $selectedRoles -Form $mainForm #> [CmdletBinding()] param( [Parameter(Mandatory)] [array]$CheckedItems, [Parameter(Mandatory)] [System.Windows.Forms.Form]$Form ) Write-Verbose "Starting deactivation process for $($CheckedItems.Count) role(s)" # Show operation splash $operationSplash = Show-OperationSplash -Title "Role Deactivation" -InitialMessage "Preparing role deactivation..." -ShowProgressBar $true try { # Confirm deactivation $roleNames = @($CheckedItems | ForEach-Object { $_.Tag.DisplayName }) $message = "Are you sure you want to deactivate the following role(s)?`n`n$($roleNames -join "`n")" $result = Show-TopMostMessageBox -Message $message -Title "Confirm Deactivation" -Buttons YesNo -Icon Question if ($result -ne 'Yes') { Write-Verbose "Deactivation cancelled by user" $operationSplash.Close() return } # Process deactivations $deactivationErrors = @() $successCount = 0 $totalRoles = $CheckedItems.Count $currentRole = 0 foreach ($item in $CheckedItems) { try { $currentRole++ $roleData = $item.Tag $progressPercent = [int](($currentRole / $totalRoles) * 100) $operationSplash.UpdateStatus("Deactivating $($roleData.DisplayName)... ($currentRole of $totalRoles)", $progressPercent) Write-Verbose "Deactivating role: $($roleData.DisplayName) [Type: $($roleData.Type)]" # Create cancellation request $requestBody = @{ principalId = $script:CurrentUser.Id action = "selfDeactivate" justification = "Deactivated via PowerShell" } switch ($roleData.Type) { 'Entra' { # Find the active assignment schedule ID if ($roleData.ScheduleId) { $requestBody.roleAssignmentScheduleId = $roleData.ScheduleId } else { # Query for the active schedule Write-Verbose "Querying for active role assignment schedules for RoleDefinitionId: $($roleData.RoleDefinitionId)" $activeSchedules = @(Get-MgRoleManagementDirectoryRoleAssignmentSchedule -Filter "principalId eq '$($script:CurrentUser.Id)' and roleDefinitionId eq '$($roleData.RoleDefinitionId)'" -ErrorAction SilentlyContinue) if ($activeSchedules -and $activeSchedules.Count -gt 0) { Write-Verbose "Found $($activeSchedules.Count) active schedule(s), using first one: $($activeSchedules[0].Id)" $requestBody.roleAssignmentScheduleId = $activeSchedules[0].Id } else { throw "Could not find active assignment schedule for deactivation of role: $($roleData.DisplayName)" } } $requestBody.roleDefinitionId = $roleData.RoleDefinitionId $requestBody.directoryScopeId = if ($roleData.DirectoryScopeId) { $roleData.DirectoryScopeId } else { "/" } $response = New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter $requestBody Write-Verbose "Entra role deactivated successfully" } 'Group' { Write-Verbose "Processing group deactivation for GroupId: $($roleData.GroupId)" # Validate required group data if (-not $roleData.GroupId) { throw "Missing GroupId for group role deactivation: $($roleData.DisplayName)" } $groupRequestBody = @{ principalId = $script:CurrentUser.Id groupId = $roleData.GroupId action = "selfDeactivate" justification = "Deactivated via PowerShell" accessId = "member" } # Find the active assignment schedule ID if ($roleData.ScheduleId) { $groupRequestBody.assignmentScheduleId = $roleData.ScheduleId } else { # Query for the active schedule Write-Verbose "Querying for active group assignment schedules for GroupId: $($roleData.GroupId)" $activeSchedules = @(Get-MgIdentityGovernancePrivilegedAccessGroupAssignmentSchedule -Filter "principalId eq '$($script:CurrentUser.Id)' and groupId eq '$($roleData.GroupId)'" -ErrorAction SilentlyContinue) if ($activeSchedules -and $activeSchedules.Count -gt 0) { Write-Verbose "Found $($activeSchedules.Count) active schedule(s), using first one: $($activeSchedules[0].Id)" $groupRequestBody.assignmentScheduleId = $activeSchedules[0].Id } else { throw "Could not find active assignment schedule for group deactivation: $($roleData.DisplayName). The group role may not be currently active or may have been assigned through a different mechanism." } } $response = New-MgIdentityGovernancePrivilegedAccessGroupAssignmentScheduleRequest -BodyParameter $groupRequestBody Write-Verbose "Group role deactivated successfully" } default { throw "Unsupported role type: $($roleData.Type)" } } $successCount++ } catch { $errorMessage = if ($_.Exception.Message) { $_.Exception.Message } else { $_.ToString() } $deactivationErrors += "$($roleData.DisplayName): $errorMessage" Write-Warning "Failed to deactivate $($roleData.DisplayName): $errorMessage" } } $operationSplash.UpdateStatus("Completing deactivation process...", 95) # Display results if ($deactivationErrors.Count -gt 0) { $message = "Successfully deactivated $successCount of $totalRoles role(s).`n`nErrors:`n$($deactivationErrors -join "`n")" Show-TopMostMessageBox -Message $message -Title "Deactivation Results" -Icon Warning } else { Show-TopMostMessageBox -Message "Successfully deactivated all $successCount role(s)!" -Title "Success" -Icon Information } # Refresh role lists $operationSplash.UpdateStatus("Refreshing role lists...", 98) # Clear role cache to ensure fresh data is fetched after deactivation if ($successCount -gt 0) { Write-Verbose "Waiting for Microsoft Graph to process deactivation changes..." Start-Sleep -Seconds 3 # Add delay for Graph propagation Write-Verbose "Clearing role cache to force fresh data retrieval after deactivation" $script:CachedEligibleRoles = @() $script:CachedActiveRoles = @() $script:LastRoleFetchTime = $null } try { Update-PIMRolesList -Form $Form -RefreshActive -RefreshEligible } catch { Write-Warning "Failed to refresh role lists: $_" } } finally { # Ensure splash is closed if ($operationSplash -and -not $operationSplash.IsDisposed) { $operationSplash.Close() } } Write-Verbose "Deactivation process completed - Success: $successCount, Errors: $($deactivationErrors.Count)" } |