Public/Disable-UserAccount.ps1
function New-SecurePassword { <# .SYNOPSIS Generates a secure random password with complexity requirements. .DESCRIPTION Creates a cryptographically secure password with a mix of uppercase, lowercase, numbers, and special characters to meet modern security requirements. .PARAMETER Length Length of the password to generate (default: 20) .PARAMETER IncludeSymbols Include special characters in the password (default: $true) .EXAMPLE New-SecurePassword Generates a 20-character secure password .EXAMPLE New-SecurePassword -Length 16 -IncludeSymbols:$false Generates a 16-character password without special characters #> [CmdletBinding()] param( [Parameter()] [ValidateRange(12, 128)] [int]$Length = 20, [Parameter()] [bool]$IncludeSymbols = $true ) # Define character sets $lowercase = 'abcdefghijklmnopqrstuvwxyz' $uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' $numbers = '0123456789' $symbols = '!@#$%^&*()_+-=[]{}|;:,.<>?' # Build character set $characterSet = $lowercase + $uppercase + $numbers if ($IncludeSymbols) { $characterSet += $symbols } # Ensure at least one character from each required set $password = @() $password += Get-Random -InputObject $lowercase.ToCharArray() $password += Get-Random -InputObject $uppercase.ToCharArray() $password += Get-Random -InputObject $numbers.ToCharArray() if ($IncludeSymbols) { $password += Get-Random -InputObject $symbols.ToCharArray() } # Fill remaining length with random characters $remainingLength = $Length - $password.Count for ($i = 0; $i -lt $remainingLength; $i++) { $password += Get-Random -InputObject $characterSet.ToCharArray() } # Shuffle the password array and return as string $shuffledPassword = $password | Sort-Object {Get-Random} return -join $shuffledPassword } function Write-TerminationLog { <# .SYNOPSIS Writes messages to the termination log file with timestamps. .DESCRIPTION Centralized logging function for user termination activities. Automatically adds timestamps and ensures proper formatting. .PARAMETER Message The message to log .PARAMETER LogPath Path to the log file .PARAMETER Level Log level (INFO, WARNING, ERROR) #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Message, [Parameter(Mandatory)] [string]$LogPath, [Parameter()] [ValidateSet("INFO", "WARNING", "ERROR")] [string]$Level = "INFO" ) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $logEntry = "[$timestamp] [$Level] $Message" try { Add-Content -Path $LogPath -Value $logEntry -ErrorAction Stop } catch { Write-Warning "Failed to write to log file: $($_.Exception.Message)" } } function Disable-O365UserAccess { <# .SYNOPSIS Disables various O365 access methods for a user. .DESCRIPTION Centralized function to disable multiple O365 access methods including ActiveSync, OWA, MAPI, POP3, IMAP, etc. .PARAMETER UserPrincipalName The user's UPN to disable access for .PARAMETER LogPath Path to the log file for recording actions #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$UserPrincipalName, [Parameter(Mandatory)] [string]$LogPath ) $accessMethods = @{ 'ActiveSyncEnabled' = $false 'OWAEnabled' = $false 'PopEnabled' = $false 'ImapEnabled' = $false 'MAPIEnabled' = $false 'EWSEnabled' = $false } foreach ($method in $accessMethods.GetEnumerator()) { try { Set-CASMailbox -Identity $UserPrincipalName -$($method.Key) $method.Value -ErrorAction Stop Write-Host "✓ Disabled $($method.Key) for $UserPrincipalName" -ForegroundColor Green Write-TerminationLog -Message "Disabled $($method.Key) for $UserPrincipalName" -LogPath $LogPath } catch { Write-Host "✗ Failed to disable $($method.Key): $($_.Exception.Message)" -ForegroundColor Red Write-TerminationLog -Message "Failed to disable $($method.Key): $($_.Exception.Message)" -LogPath $LogPath -Level "ERROR" } } } function Remove-UserLicenses { <# .SYNOPSIS Removes all Microsoft 365 licenses from a user using Microsoft Graph PowerShell. .DESCRIPTION Modern approach to license removal using the Microsoft Graph PowerShell module instead of the deprecated MSOnline module. .PARAMETER UserPrincipalName The user's UPN to remove licenses from .PARAMETER LogPath Path to the log file for recording actions #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$UserPrincipalName, [Parameter(Mandatory)] [string]$LogPath ) try { # Get user's current licenses $user = Get-MgUser -UserId $UserPrincipalName -Property AssignedLicenses -ErrorAction Stop if ($user.AssignedLicenses.Count -gt 0) { Write-Host "Found $($user.AssignedLicenses.Count) licenses assigned to $UserPrincipalName" -ForegroundColor Yellow Write-TerminationLog -Message "Found $($user.AssignedLicenses.Count) licenses assigned to $UserPrincipalName" -LogPath $LogPath # Get all SKU details for logging $skuIds = $user.AssignedLicenses.SkuId foreach ($skuId in $skuIds) { try { $sku = Get-MgSubscribedSku | Where-Object { $_.SkuId -eq $skuId } Write-Host " - Removing license: $($sku.SkuPartNumber)" -ForegroundColor Yellow Write-TerminationLog -Message "Removing license: $($sku.SkuPartNumber) (SKU ID: $skuId)" -LogPath $LogPath } catch { Write-TerminationLog -Message "Removing license with SKU ID: $skuId" -LogPath $LogPath } } # Remove all licenses Set-MgUserLicense -UserId $UserPrincipalName -AddLicenses @() -RemoveLicenses $skuIds -ErrorAction Stop Write-Host "✓ Successfully removed all licenses from $UserPrincipalName" -ForegroundColor Green Write-TerminationLog -Message "Successfully removed all licenses from $UserPrincipalName" -LogPath $LogPath } else { Write-Host "✓ No licenses found for $UserPrincipalName" -ForegroundColor Green Write-TerminationLog -Message "No licenses found for $UserPrincipalName" -LogPath $LogPath } } catch { Write-Host "✗ Failed to remove licenses: $($_.Exception.Message)" -ForegroundColor Red Write-TerminationLog -Message "Failed to remove licenses: $($_.Exception.Message)" -LogPath $LogPath -Level "ERROR" } } function Disable-ADUserAccount { <# .SYNOPSIS Performs Active Directory account termination tasks. .DESCRIPTION Handles all AD-related termination tasks including password reset, account disable, group removal, and GAL hiding. .PARAMETER Username The AD username to terminate .PARAMETER Description New description for the terminated account .PARAMETER LogPath Path to the log file for recording actions #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Username, [Parameter(Mandatory)] [string]$Description, [Parameter(Mandatory)] [string]$LogPath ) # Reset password try { $newPassword = New-SecurePassword $securePassword = ConvertTo-SecureString -String $newPassword -AsPlainText -Force Set-ADAccountPassword -Identity $Username -NewPassword $securePassword -Reset -ErrorAction Stop Write-Host "✓ Password reset successfully" -ForegroundColor Green Write-TerminationLog -Message "Password reset successfully" -LogPath $LogPath } catch { Write-Host "✗ Failed to reset password: $($_.Exception.Message)" -ForegroundColor Red Write-TerminationLog -Message "Failed to reset password: $($_.Exception.Message)" -LogPath $LogPath -Level "ERROR" } # Disable account try { Disable-ADAccount -Identity $Username -ErrorAction Stop # Verify account is disabled $accountStatus = Get-ADUser -Identity $Username -Properties Enabled if (-not $accountStatus.Enabled) { Write-Host "✓ Account disabled successfully" -ForegroundColor Green Write-TerminationLog -Message "Account disabled successfully" -LogPath $LogPath } else { throw "Account disable verification failed" } } catch { Write-Host "✗ Failed to disable account: $($_.Exception.Message)" -ForegroundColor Red Write-TerminationLog -Message "Failed to disable account: $($_.Exception.Message)" -LogPath $LogPath -Level "ERROR" } # Remove from groups (except Domain Users) try { Write-TerminationLog -Message "Starting group removal process" -LogPath $LogPath $groups = Get-ADPrincipalGroupMembership -Identity $Username | Where-Object { $_.Name -ne "Domain Users" } if ($groups.Count -gt 0) { Write-Host "Removing user from $($groups.Count) groups:" -ForegroundColor Yellow Write-TerminationLog -Message "Found $($groups.Count) groups to remove user from" -LogPath $LogPath foreach ($group in $groups) { try { Remove-ADGroupMember -Identity $group -Members $Username -Confirm:$false -ErrorAction Stop Write-Host " ✓ Removed from: $($group.Name)" -ForegroundColor Green Write-TerminationLog -Message "Removed from group: $($group.Name)" -LogPath $LogPath } catch { Write-Host " ✗ Failed to remove from: $($group.Name)" -ForegroundColor Red Write-TerminationLog -Message "Failed to remove from group $($group.Name): $($_.Exception.Message)" -LogPath $LogPath -Level "ERROR" } } } else { Write-Host "✓ No additional groups to remove user from" -ForegroundColor Green Write-TerminationLog -Message "No additional groups to remove user from" -LogPath $LogPath } } catch { Write-Host "✗ Failed to process group memberships: $($_.Exception.Message)" -ForegroundColor Red Write-TerminationLog -Message "Failed to process group memberships: $($_.Exception.Message)" -LogPath $LogPath -Level "ERROR" } # Hide from GAL try { Set-ADUser -Identity $Username -Replace @{msexchhidefromaddresslists=$true} -ErrorAction Stop Write-Host "✓ Hidden from Global Address List" -ForegroundColor Green Write-TerminationLog -Message "Hidden from Global Address List" -LogPath $LogPath } catch { Write-Host "✗ Failed to hide from GAL: $($_.Exception.Message)" -ForegroundColor Red Write-TerminationLog -Message "Failed to hide from GAL: $($_.Exception.Message)" -LogPath $LogPath -Level "ERROR" } # Update description try { Set-ADUser -Identity $Username -Description $Description -ErrorAction Stop Write-Host "✓ Description updated successfully" -ForegroundColor Green Write-TerminationLog -Message "Description updated to: $Description" -LogPath $LogPath } catch { Write-Host "✗ Failed to update description: $($_.Exception.Message)" -ForegroundColor Red Write-TerminationLog -Message "Failed to update description: $($_.Exception.Message)" -LogPath $LogPath -Level "ERROR" } } function Disable-UserAccount { <# .SYNOPSIS Modern user termination function for Active Directory and Microsoft 365. .DESCRIPTION Comprehensive user termination script that handles both on-premises Active Directory and Microsoft 365 cloud services using current PowerShell modules and best practices. Prerequisites: - ActiveDirectory PowerShell module - ExchangeOnlineManagement module - Microsoft.Graph PowerShell module - Appropriate permissions in AD and M365 .PARAMETER Username Active Directory username to terminate .PARAMETER TicketNumber Service request or ticket number for audit trail .PARAMETER TechnicianName Name of the technician performing the termination .PARAMETER OutOfOfficeMessage Optional out-of-office message to set .PARAMETER ForwardEmailTo Optional email address to forward mail to .PARAMETER ConvertToSharedMailbox Convert the user's mailbox to a shared mailbox (default: $true) .PARAMETER RemoveLicenses Remove all Microsoft 365 licenses (default: $true) .PARAMETER LogDirectory Directory to store termination logs (default: C:\Temp\TerminatedUsers) .EXAMPLE Disable-UserAccount -Username "jdoe" -TicketNumber "SR12345" -TechnicianName "IT Admin" .EXAMPLE Disable-UserAccount -Username "jsmith" -TicketNumber "INC67890" -TechnicianName "Help Desk" -ForwardEmailTo "manager@company.com" -OutOfOfficeMessage "This employee is no longer with the company." .NOTES This function replaces the legacy Terminate-ADUser function with modern approaches: - Uses Microsoft Graph PowerShell instead of MSOnline - Uses modern Exchange Online PowerShell connection - Improved error handling and logging - Better security practices - Comprehensive parameter validation #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter( Mandatory, HelpMessage = "Enter the Active Directory username to terminate" )] [ValidateNotNullOrEmpty()] [string]$Username, [Parameter( Mandatory, HelpMessage = "Enter the ticket or service request number" )] [ValidateNotNullOrEmpty()] [string]$TicketNumber, [Parameter( Mandatory, HelpMessage = "Enter the name of the technician performing the termination" )] [ValidateNotNullOrEmpty()] [string]$TechnicianName, [Parameter(HelpMessage = "Out of office message to set")] [string]$OutOfOfficeMessage, [Parameter(HelpMessage = "Email address to forward messages to")] [ValidatePattern("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")] [string]$ForwardEmailTo, [Parameter(HelpMessage = "Convert mailbox to shared mailbox")] [bool]$ConvertToSharedMailbox = $true, [Parameter(HelpMessage = "Remove all Microsoft 365 licenses")] [bool]$RemoveLicenses = $true, [Parameter(HelpMessage = "Directory to store termination logs")] [string]$LogDirectory = "C:\Temp\TerminatedUsers" ) begin { # Initialize variables $startTime = Get-Date $description = "Terminated per $TicketNumber by $TechnicianName on $($startTime.ToString('yyyy-MM-dd'))" # Create log directory if it doesn't exist if (-not (Test-Path $LogDirectory)) { try { New-Item -Path $LogDirectory -ItemType Directory -Force | Out-Null Write-Host "✓ Created log directory: $LogDirectory" -ForegroundColor Green } catch { Write-Error "Failed to create log directory: $($_.Exception.Message)" return } } # Set up log file $logFile = Join-Path $LogDirectory "Termination-$Username-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" Write-Host "Log file: $logFile" -ForegroundColor Cyan # Initialize log Write-TerminationLog -Message "=== USER TERMINATION PROCESS STARTED ===" -LogPath $logFile Write-TerminationLog -Message "Username: $Username" -LogPath $logFile Write-TerminationLog -Message "Ticket: $TicketNumber" -LogPath $logFile Write-TerminationLog -Message "Technician: $TechnicianName" -LogPath $logFile Write-TerminationLog -Message "Start Time: $startTime" -LogPath $logFile # Check required modules $requiredModules = @( @{Name = 'ActiveDirectory'; ImportName = 'ActiveDirectory'}, @{Name = 'ExchangeOnlineManagement'; ImportName = 'ExchangeOnlineManagement'}, @{Name = 'Microsoft.Graph.Authentication'; ImportName = 'Microsoft.Graph.Authentication'}, @{Name = 'Microsoft.Graph.Users'; ImportName = 'Microsoft.Graph.Users'}, @{Name = 'Microsoft.Graph.Identity.DirectoryManagement'; ImportName = 'Microsoft.Graph.Identity.DirectoryManagement'} ) foreach ($module in $requiredModules) { if (-not (Get-Module -ListAvailable -Name $module.Name)) { Write-Error "Required module '$($module.Name)' is not installed. Please install it first." return } try { Import-Module $module.ImportName -Force -ErrorAction Stop Write-Host "✓ Imported module: $($module.ImportName)" -ForegroundColor Green } catch { Write-Error "Failed to import module '$($module.ImportName)': $($_.Exception.Message)" return } } } process { try { # Verify user exists in Active Directory Write-Host "`n=== VERIFYING USER ACCOUNT ===" -ForegroundColor Yellow Write-TerminationLog -Message "=== VERIFYING USER ACCOUNT ===" -LogPath $logFile try { $adUser = Get-ADUser -Identity $Username -Properties DisplayName, UserPrincipalName, Enabled, Description -ErrorAction Stop Write-Host "✓ Found user: $($adUser.DisplayName) ($($adUser.UserPrincipalName))" -ForegroundColor Green Write-Host "✓ Current status: $(if ($adUser.Enabled) { 'Enabled' } else { 'Disabled' })" -ForegroundColor $(if ($adUser.Enabled) { 'Yellow' } else { 'Red' }) Write-TerminationLog -Message "Found user: $($adUser.DisplayName) ($($adUser.UserPrincipalName))" -LogPath $logFile Write-TerminationLog -Message "Current status: $(if ($adUser.Enabled) { 'Enabled' } else { 'Disabled' })" -LogPath $logFile if (-not $adUser.Enabled) { Write-Warning "User account is already disabled!" $continue = Read-Host "Continue with termination process? (y/N)" if ($continue -notmatch '^[Yy]') { Write-Host "Termination cancelled by user." -ForegroundColor Yellow return } } } catch { Write-Error "User '$Username' not found in Active Directory: $($_.Exception.Message)" Write-TerminationLog -Message "User '$Username' not found in Active Directory: $($_.Exception.Message)" -LogPath $logFile -Level "ERROR" return } # Confirmation prompt Write-Host "`nUser to terminate:" -ForegroundColor Red Write-Host " Name: $($adUser.DisplayName)" -ForegroundColor White Write-Host " Username: $Username" -ForegroundColor White Write-Host " UPN: $($adUser.UserPrincipalName)" -ForegroundColor White Write-Host " Ticket: $TicketNumber" -ForegroundColor White Write-Host " Technician: $TechnicianName" -ForegroundColor White if (-not $PSCmdlet.ShouldProcess($Username, "Terminate user account")) { $confirm = Read-Host "`nAre you sure you want to terminate this user? Type 'TERMINATE' to confirm" if ($confirm -ne 'TERMINATE') { Write-Host "Termination cancelled." -ForegroundColor Yellow return } } # Phase 1: Active Directory Tasks Write-Host "`n=== PHASE 1: ACTIVE DIRECTORY TASKS ===" -ForegroundColor Yellow Write-TerminationLog -Message "=== PHASE 1: ACTIVE DIRECTORY TASKS ===" -LogPath $logFile Disable-ADUserAccount -Username $Username -Description $description -LogPath $logFile # Phase 2: Exchange Online Tasks Write-Host "`n=== PHASE 2: EXCHANGE ONLINE TASKS ===" -ForegroundColor Yellow Write-TerminationLog -Message "=== PHASE 2: EXCHANGE ONLINE TASKS ===" -LogPath $logFile # Ensure Exchange Online connection if (-not (Test-O365ExchangeConnection -Quiet)) { Write-Host "Connecting to Exchange Online..." -ForegroundColor Cyan if (-not (Connect-O365Exchange -ShowProgress)) { Write-Error "Failed to connect to Exchange Online" Write-TerminationLog -Message "Failed to connect to Exchange Online" -LogPath $logFile -Level "ERROR" return } } # Disable mailbox access methods Disable-O365UserAccess -UserPrincipalName $adUser.UserPrincipalName -LogPath $logFile # Set out of office message if ($OutOfOfficeMessage) { try { Set-MailboxAutoReplyConfiguration -Identity $adUser.UserPrincipalName -AutoReplyState Enabled -InternalMessage $OutOfOfficeMessage -ExternalMessage $OutOfOfficeMessage -ErrorAction Stop Write-Host "✓ Out of office message set" -ForegroundColor Green Write-TerminationLog -Message "Out of office message set: $OutOfOfficeMessage" -LogPath $logFile } catch { Write-Host "✗ Failed to set out of office message: $($_.Exception.Message)" -ForegroundColor Red Write-TerminationLog -Message "Failed to set out of office message: $($_.Exception.Message)" -LogPath $logFile -Level "ERROR" } } # Set email forwarding if ($ForwardEmailTo) { try { Set-Mailbox -Identity $adUser.UserPrincipalName -ForwardingAddress $ForwardEmailTo -DeliverToMailboxAndForward $false -ErrorAction Stop Write-Host "✓ Email forwarding configured to: $ForwardEmailTo" -ForegroundColor Green Write-TerminationLog -Message "Email forwarding configured to: $ForwardEmailTo" -LogPath $logFile } catch { Write-Host "✗ Failed to configure email forwarding: $($_.Exception.Message)" -ForegroundColor Red Write-TerminationLog -Message "Failed to configure email forwarding: $($_.Exception.Message)" -LogPath $logFile -Level "ERROR" } } # Convert to shared mailbox if ($ConvertToSharedMailbox) { try { Set-Mailbox -Identity $adUser.UserPrincipalName -Type Shared -ErrorAction Stop Write-Host "✓ Mailbox converted to shared mailbox" -ForegroundColor Green Write-TerminationLog -Message "Mailbox converted to shared mailbox" -LogPath $logFile # Verify conversion Start-Sleep -Seconds 5 $mailbox = Get-Mailbox -Identity $adUser.UserPrincipalName if ($mailbox.RecipientTypeDetails -eq 'SharedMailbox') { Write-Host "✓ Shared mailbox conversion verified" -ForegroundColor Green Write-TerminationLog -Message "Shared mailbox conversion verified" -LogPath $logFile } } catch { Write-Host "✗ Failed to convert to shared mailbox: $($_.Exception.Message)" -ForegroundColor Red Write-TerminationLog -Message "Failed to convert to shared mailbox: $($_.Exception.Message)" -LogPath $logFile -Level "ERROR" } } # Phase 3: Microsoft Graph Tasks (License Removal) if ($RemoveLicenses) { Write-Host "`n=== PHASE 3: MICROSOFT 365 LICENSE REMOVAL ===" -ForegroundColor Yellow Write-TerminationLog -Message "=== PHASE 3: MICROSOFT 365 LICENSE REMOVAL ===" -LogPath $logFile # Connect to Microsoft Graph try { $graphContext = Get-MgContext if (-not $graphContext) { Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan Connect-MgGraph -Scopes "User.ReadWrite.All", "Directory.ReadWrite.All" -NoWelcome } Remove-UserLicenses -UserPrincipalName $adUser.UserPrincipalName -LogPath $logFile } catch { Write-Host "✗ Failed to connect to Microsoft Graph: $($_.Exception.Message)" -ForegroundColor Red Write-TerminationLog -Message "Failed to connect to Microsoft Graph: $($_.Exception.Message)" -LogPath $logFile -Level "ERROR" } } # Summary $endTime = Get-Date $duration = $endTime - $startTime Write-Host "`n=== TERMINATION SUMMARY ===" -ForegroundColor Green Write-Host "✓ User termination process completed" -ForegroundColor Green Write-Host "✓ Duration: $($duration.TotalMinutes.ToString('F1')) minutes" -ForegroundColor Green Write-Host "✓ Log file: $logFile" -ForegroundColor Green Write-TerminationLog -Message "=== TERMINATION PROCESS COMPLETED ===" -LogPath $logFile Write-TerminationLog -Message "End Time: $endTime" -LogPath $logFile Write-TerminationLog -Message "Duration: $($duration.TotalMinutes.ToString('F1')) minutes" -LogPath $logFile Write-TerminationLog -Message "Process completed successfully" -LogPath $logFile # Open log file try { Start-Process $logFile } catch { Write-Host "Log file location: $logFile" -ForegroundColor Cyan } } catch { Write-Error "Termination process failed: $($_.Exception.Message)" Write-TerminationLog -Message "Termination process failed: $($_.Exception.Message)" -LogPath $logFile -Level "ERROR" } } } |