UnipharLeaversManagement.psm1
|
# UnipharLeaversManagement.psm1 # Functions for managing employee leavers (disabling accounts, removing access, etc.) # Version: 1.0.1 # Last Modified: 2025-12-18 # Import UnipharSecurityAuth module functions if needed # Note: The main script should import both modules function Convert-MailboxToSharedWithOOO { <# .SYNOPSIS Converts user mailbox to shared mailbox. .DESCRIPTION Converts a user mailbox to a shared mailbox using Exchange Online cmdlets. Checks if the mailbox is already shared before attempting conversion. .PARAMETER UserPrincipalName UPN of the user whose mailbox should be converted. .EXAMPLE Convert-MailboxToSharedWithOOO -UserPrincipalName 'user@uniphar.com' Converts the user mailbox to a shared mailbox. .OUTPUTS PSCustomObject with UPN, ConversionResult, MailboxType, and Error properties. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName ) $result = [PSCustomObject]@{ UPN = $UserPrincipalName ConversionResult = 'Unknown' MailboxType = $null Error = $null } try { Write-Verbose "Retrieving mailbox for $UserPrincipalName..." $mailbox = Get-Mailbox -Identity $UserPrincipalName -ErrorAction Stop $result.MailboxType = $mailbox.RecipientTypeDetails if ($mailbox.RecipientTypeDetails -ne 'SharedMailbox') { if ($PSCmdlet.ShouldProcess($UserPrincipalName, "Convert mailbox to shared")) { Write-Verbose "Converting mailbox to shared for $UserPrincipalName..." Set-Mailbox -Identity $UserPrincipalName -Type Shared -ErrorAction Stop $result.ConversionResult = 'ConvertedToShared' $result.MailboxType = 'SharedMailbox' Write-Verbose "Converted mailbox to shared: $UserPrincipalName" } else { $result.ConversionResult = 'WhatIf-WouldConvert' Write-Verbose "WhatIf: Would convert mailbox to shared: $UserPrincipalName" } } else { $result.ConversionResult = 'AlreadyShared' Write-Verbose "Mailbox already shared: $UserPrincipalName" } } catch { $result.Error = $_.Exception.Message $result.ConversionResult = "Error" Write-Error "Error converting mailbox for $UserPrincipalName : $($_.Exception.Message)" } return $result } function Set-OutOfOfficeMessage { <# .SYNOPSIS Sets automatic Out of Office reply message for a user mailbox. .DESCRIPTION Configures automatic replies (Out of Office) for one year using Microsoft Graph API. Sets both internal and external reply messages with scheduled duration. .PARAMETER UserPrincipalName UPN of the user whose Out of Office message should be set. .PARAMETER InternalMessage Message shown to internal recipients. Default is 'This user is no longer with the organization.' .PARAMETER ExternalMessage Message shown to external recipients. Default is 'This user is no longer with the organization.' .EXAMPLE Set-OutOfOfficeMessage -UserPrincipalName 'user@uniphar.com' Sets default leaver message for internal and external recipients for one year. .EXAMPLE Set-OutOfOfficeMessage -UserPrincipalName 'user@uniphar.com' -InternalMessage 'Contact hr@uniphar.com' -ExternalMessage 'This person has left the company.' Sets custom Out of Office messages. .OUTPUTS PSCustomObject with UPN, OOOSet (bool), and Error properties. #> param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName, [Parameter(Mandatory = $false)] [string]$InternalMessage = 'This user is no longer with the organization.', [Parameter(Mandatory = $false)] [string]$ExternalMessage = 'This user is no longer with the organization.' ) $result = [PSCustomObject]@{ UPN = $UserPrincipalName OOOSet = $false Error = $null } try { Write-Verbose "Setting OOO for $UserPrincipalName using Graph..." $oooSettings = @{ '@odata.context' = 'https://graph.microsoft.com/v1.0/$metadata#users(''' + $UserPrincipalName + ''')/mailboxSettings' automaticRepliesSetting = @{ status = 'scheduled' externalAudience = 'all' scheduledStartDateTime = @{ dateTime = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss') timeZone = 'UTC' } scheduledEndDateTime = @{ dateTime = (Get-Date).AddYears(1).ToString('yyyy-MM-ddTHH:mm:ss') timeZone = 'UTC' } internalReplyMessage = $InternalMessage externalReplyMessage = $ExternalMessage } } Update-MgUserMailboxSetting -UserId $UserPrincipalName -BodyParameter $oooSettings -ErrorAction Stop $result.OOOSet = $true Write-Verbose "OOO set successfully for $UserPrincipalName" } catch { $result.Error = $_.Exception.Message Write-Warning "Failed to set OOO for $UserPrincipalName : $($_.Exception.Message)" } return $result } function Disable-EntraUserAccount { <# .SYNOPSIS Disables a user account in Entra ID (Azure AD). .DESCRIPTION Sets AccountEnabled property to false, preventing the user from signing in to Microsoft 365 and Azure services. .PARAMETER UserPrincipalName UPN of the user account to disable. .EXAMPLE Disable-EntraUserAccount -UserPrincipalName 'user@uniphar.com' Disables the user account in Entra ID. .OUTPUTS PSCustomObject with UPN, Disabled (bool), and Error properties. #> param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName ) $result = [PSCustomObject]@{ UPN = $UserPrincipalName Disabled = $false Error = $null } try { Update-MgUser -UserId $UserPrincipalName -AccountEnabled:$false -ErrorAction Stop $result.Disabled = $true Write-Verbose "Disabled Entra account: $UserPrincipalName" } catch { $result.Error = $_.Exception.Message Write-Warning "Error disabling Entra account $UserPrincipalName : $($_.Exception.Message)" } return $result } function Disable-OnPremUserAccount { <# .SYNOPSIS Disables a user account in on-premises Active Directory. .DESCRIPTION Disables the AD account preventing logon to on-premises resources. Requires connectivity to on-premises AD via Hybrid Runbook Worker. .PARAMETER UserPrincipalName UPN of the user account to disable. .PARAMETER Server FQDN of the on-premises domain controller. .PARAMETER Credential PSCredential object for domain admin authentication (required for Hybrid Worker). .EXAMPLE $cred = Get-OnPremAdCredential -KeyVaultName 'uni-core-on-prem-kv' Disable-OnPremUserAccount -UserPrincipalName 'user@uniphar.com' -Server 'unidc10.uniphar.local' -Credential $cred .OUTPUTS PSCustomObject with UPN, Disabled (bool), and Error properties. #> param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName, [Parameter(Mandatory = $false)] [string]$Server, [Parameter(Mandatory = $false)] [PSCredential]$Credential ) $result = [PSCustomObject]@{ UPN = $UserPrincipalName Disabled = $false Error = $null } try { $escapedUpn = Protect-LdapFilterValue $UserPrincipalName # Check if this looks like a samAccountName (no @ sign) or a full UPN $filterAttribute = if ($UserPrincipalName -notlike '*@*') { 'samAccountName' } else { 'UserPrincipalName' } $aduserparams = @{ Filter = "$filterAttribute -eq '$escapedUpn'" Server = $Server Properties = 'Enabled' ErrorAction = 'Stop' } if ($Credential) { $aduserparams['Credential'] = $Credential } $aduser = Get-ADUser @aduserparams if (-not $aduser) { $result.Error = 'UserNotFound' return $result } if ($aduser.Enabled) { $disparams = @{ Identity = $aduser.DistinguishedName Server = $Server ErrorAction = 'Stop' } if ($Credential) { $disparams['Credential'] = $Credential } Disable-ADAccount @disparams $result.Disabled = $true Write-Verbose "Disabled on-prem AD account: $UserPrincipalName" } else { $result.Disabled = $false $result.Error = 'AlreadyDisabled' Write-Verbose "On-prem AD account already disabled: $UserPrincipalName" } } catch { $result.Error = $_.Exception.Message Write-Warning "Error disabling on-prem AD account $UserPrincipalName : $($_.Exception.Message)" } return $result } # Hide user from Global Address List in Entra ID using Graph function Hide-EntraUserFromGAL { <# .SYNOPSIS Hides a user from the Global Address List in Entra ID. .DESCRIPTION Sets ShowInAddressList property to false, hiding the user from Exchange Online and Outlook address lists. .PARAMETER UserPrincipalName UPN of the user to hide from GAL. .EXAMPLE Hide-EntraUserFromGAL -UserPrincipalName 'user@uniphar.com' Hides the user from Entra ID Global Address List. .OUTPUTS PSCustomObject with UPN, Hidden (bool), and Error properties. #> param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName ) $result = [PSCustomObject]@{ UPN = $UserPrincipalName HiddenFromGAL = $false Error = $null } try { Update-MgUser -UserId $UserPrincipalName -ShowInAddressList:$false -ErrorAction Stop $result.HiddenFromGAL = $true Write-Verbose "Cloud user $UserPrincipalName hidden from GAL using Graph" } catch { $result.Error = $_.Exception.Message Write-Warning "Failed to hide $UserPrincipalName from GAL: $($_.Exception.Message)" } return $result } # Hide user from Global Address List in on-premises AD function Hide-OnPremUserFromGAL { <# .SYNOPSIS Hides a user from Global Address List in on-premises Active Directory. .DESCRIPTION Sets msExchHideFromAddressLists attribute to True, hiding the user from on-premises Exchange and Outlook address lists. .PARAMETER UserPrincipalName UPN of the user to hide from GAL. .PARAMETER Server FQDN of the on-premises domain controller. .PARAMETER Credential PSCredential object for domain admin authentication (required for Hybrid Worker). .EXAMPLE $cred = Get-OnPremAdCredential -KeyVaultName 'uni-core-on-prem-kv' Hide-OnPremUserFromGAL -UserPrincipalName 'user@uniphar.com' -Server 'unidc10.uniphar.local' -Credential $cred .OUTPUTS PSCustomObject with UPN, Hidden (bool), and Error properties. #> param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName, [Parameter(Mandatory = $false)] [string]$Server, [Parameter(Mandatory = $false)] [PSCredential]$Credential ) $result = [PSCustomObject]@{ UPN = $UserPrincipalName HiddenFromGAL = $false Error = $null } try { $escapedUpn = Protect-LdapFilterValue $UserPrincipalName # Check if this looks like a samAccountName (no @ sign) or a full UPN if ($UserPrincipalName -notlike '*@*') { # This is a samAccountName $aduserparams = @{ Filter = "samAccountName -eq '$escapedUpn'" Server = $Server Properties = 'msExchHideFromAddressLists' ErrorAction = 'Stop' } } else { # This is a UPN $aduserparams = @{ Filter = "UserPrincipalName -eq '$escapedUpn'" Server = $Server Properties = 'msExchHideFromAddressLists' ErrorAction = 'Stop' } } if ($Credential) { $aduserparams['Credential'] = $Credential } $aduser = Get-ADUser @aduserparams if (-not $aduser) { $result.Error = 'UserNotFound' return $result } # Set msExchHideFromAddressLists to true $setParams = @{ Identity = $aduser.DistinguishedName Replace = @{msExchHideFromAddressLists = $true } Server = $Server ErrorAction = 'Stop' } if ($Credential) { $setParams['Credential'] = $Credential } Set-ADUser @setParams $result.HiddenFromGAL = $true Write-Verbose "Successfully hidden user from GAL in on-prem AD: $UserPrincipalName" } catch { $result.Error = $_.Exception.Message Write-Warning "Error hiding user from GAL in on-prem AD $UserPrincipalName : $($_.Exception.Message)" } return $result } # Remove email forwarding - requires Exchange Online cmdlets # Note: Microsoft Graph SDK does not provide cmdlets for managing mailbox forwarding # Forwarding must be managed using Exchange Online cmdlets (Get-Mailbox, Set-Mailbox) function Remove-MailboxForwarding { <# .SYNOPSIS Removes mailbox forwarding rules (placeholder - requires Exchange Online). .DESCRIPTION This function is a placeholder. Mailbox forwarding removal requires Exchange Online cmdlets (Get-Mailbox, Set-Mailbox) which are not available via Microsoft Graph SDK. Currently returns 'NotImplemented-RequiresExchangeOnline' status. .PARAMETER UserPrincipalName UPN of the user whose mailbox forwarding should be removed. .EXAMPLE Remove-MailboxForwarding -UserPrincipalName 'user@uniphar.com' Returns not implemented status. .NOTES Requires Exchange Online connection. Proper implementation: $mailbox = Get-Mailbox -Identity $UserPrincipalName if ($mailbox.ForwardingAddress -or $mailbox.ForwardingSmtpAddress) { Set-Mailbox -Identity $UserPrincipalName -ForwardingAddress $null -ForwardingSmtpAddress $null } .OUTPUTS PSCustomObject with UPN, ForwardingRemoved status, and Error properties. #> param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName ) $result = [PSCustomObject]@{ UPN = $UserPrincipalName ForwardingRemoved = $false ForwardingTypes = @() Error = 'Requires Exchange Online cmdlets - not yet implemented' } # PLACEHOLDER - This function requires Exchange Online connection # Will be implemented after Exchange Online is enabled in the runbook # # Correct implementation would be: # try { # $mailbox = Get-Mailbox -Identity $UserPrincipalName -ErrorAction Stop # $forwardingTypes = @() # # if ($mailbox.ForwardingAddress -or $mailbox.ForwardingSmtpAddress) { # if ($mailbox.ForwardingAddress) { $forwardingTypes += 'Internal' } # if ($mailbox.ForwardingSmtpAddress) { $forwardingTypes += 'SMTP' } # # Set-Mailbox -Identity $UserPrincipalName -ForwardingAddress $null -ForwardingSmtpAddress $null -ErrorAction Stop # $result.ForwardingRemoved = $true # $result.ForwardingTypes = $forwardingTypes # $result.Error = $null # Write-Verbose "Forwarding removed for $UserPrincipalName : $($forwardingTypes -join ', ')" # } else { # $result.ForwardingTypes = @('NoForwardingFound') # $result.Error = $null # } # } catch { # $result.Error = $_.Exception.Message # Write-Warning "Error removing forwarding for $UserPrincipalName : $($_.Exception.Message)" # } Write-Verbose "Mailbox forwarding removal skipped for $UserPrincipalName (requires Exchange Online cmdlets)" return $result } # Reset password for on-premises AD user function Reset-OnPremUserPassword { param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName, [Parameter(Mandatory = $false)] [string]$Server, [Parameter(Mandatory = $false)] [PSCredential]$Credential ) $result = [PSCustomObject]@{ UPN = $UserPrincipalName PasswordReset = $false Error = $null } try { # Generate random 90-character password $newPassword = New-RandomPassword -Length 90 $securePassword = ConvertTo-SecureString -String $newPassword -AsPlainText -Force # Get AD user $escapedUpn = Protect-LdapFilterValue $UserPrincipalName # Check if this looks like a samAccountName (no @ sign) or a full UPN $filterAttribute = if ($UserPrincipalName -notlike '*@*') { 'samAccountName' } else { 'UserPrincipalName' } $aduserparams = @{ Filter = "$filterAttribute -eq '$escapedUpn'" Server = $Server ErrorAction = 'Stop' } if ($Credential) { $aduserparams['Credential'] = $Credential } $aduser = Get-ADUser @aduserparams if (-not $aduser) { $result.Error = 'UserNotFound' return $result } # Reset password $setPasswordParams = @{ Identity = $aduser.DistinguishedName NewPassword = $securePassword Reset = $true Server = $Server ErrorAction = 'Stop' } if ($Credential) { $setPasswordParams['Credential'] = $Credential } Set-ADAccountPassword @setPasswordParams $result.PasswordReset = $true Write-Verbose "Successfully reset password for on-prem AD user: $UserPrincipalName" } catch { $result.Error = $_.Exception.Message Write-Warning "Error resetting password for on-prem AD user $UserPrincipalName : $($_.Exception.Message)" } return $result } function Reset-EntraUserPassword { <# .SYNOPSIS Resets Entra ID user password to secure random string. .DESCRIPTION Generates and sets a new secure random password (90 characters) for Entra ID (Azure AD) user account to prevent unauthorized cloud access. .PARAMETER UserPrincipalName UPN of the user whose password should be reset. .EXAMPLE Reset-EntraUserPassword -UserPrincipalName 'user@uniphar.com' Resets Entra ID password to 90-character random string. .OUTPUTS PSCustomObject with UPN, PasswordReset (bool), and Error properties. #> param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName ) $result = [PSCustomObject]@{ UPN = $UserPrincipalName PasswordReset = $false Error = $null } try { # Generate random 90-character password $newPassword = New-RandomPassword -Length 90 # Get user $user = Get-MgUser -UserId $UserPrincipalName -ErrorAction Stop if (-not $user) { $result.Error = 'UserNotFound' return $result } # Reset password using Update-MgUser $passwordProfile = @{ forceChangePasswordNextSignIn = $false password = $newPassword } Update-MgUser -UserId $user.Id -PasswordProfile $passwordProfile -ErrorAction Stop $result.PasswordReset = $true Write-Verbose "Successfully reset password for Entra ID user: $UserPrincipalName" } catch { $result.Error = $_.Exception.Message Write-Warning "Error resetting password for Entra ID user $UserPrincipalName : $($_.Exception.Message)" } return $result } # Remove on-premises group memberships function Remove-OnPremGroupMemberships { <# .SYNOPSIS Removes all on-premises AD group memberships except Domain Users. .DESCRIPTION Removes user from all AD security groups (except Domain Users) and backs up group list to extensionAttribute14 for audit trail. .PARAMETER UserPrincipalName UPN of the user whose group memberships should be removed. .PARAMETER Server FQDN of the on-premises domain controller. .PARAMETER Credential PSCredential object for domain admin authentication (required for Hybrid Worker). .EXAMPLE $cred = Get-OnPremAdCredential -KeyVaultName 'uni-core-on-prem-kv' Remove-OnPremGroupMemberships -UserPrincipalName 'user@uniphar.com' -Server 'unidc10.uniphar.local' -Credential $cred Removes all groups, backs up list to extensionAttribute14. .OUTPUTS PSCustomObject with UPN, RemovedCount, BackedUp (bool), and Error properties. #> param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName, [Parameter(Mandatory = $false)] [string]$Server, [Parameter(Mandatory = $false)] [PSCredential]$Credential ) $result = [PSCustomObject]@{ UPN = $UserPrincipalName GroupsRemoved = 0 GroupsBackup = $null Error = $null } try { $escapedUpn = Protect-LdapFilterValue $UserPrincipalName # Check if this looks like a samAccountName (no @ sign) or a full UPN if ($UserPrincipalName -notlike '*@*') { # This is a samAccountName $aduserparams = @{ Filter = "samAccountName -eq '$escapedUpn'" Server = $Server Properties = 'MemberOf', 'extensionAttribute14' ErrorAction = 'Stop' } } else { # This is a UPN $aduserparams = @{ Filter = "UserPrincipalName -eq '$escapedUpn'" Server = $Server Properties = 'MemberOf', 'extensionAttribute14' ErrorAction = 'Stop' } } if ($Credential) { $aduserparams['Credential'] = $Credential } $aduser = Get-ADUser @aduserparams if (-not $aduser) { $result.Error = 'UserNotFound' return $result } if ($aduser.MemberOf) { # Get group names and backup $groupNames = @() foreach ($groupDN in $aduser.MemberOf) { $getGroupParams = @{ Identity = $groupDN Server = $Server ErrorAction = 'SilentlyContinue' } if ($Credential) { $getGroupParams['Credential'] = $Credential } $group = Get-ADGroup @getGroupParams if ($group -and $group.Name -ne 'Domain Users') { $groupNames += $group.Name # Remove from group $removeParams = @{ Identity = $groupDN Members = $aduser.DistinguishedName Server = $Server Confirm = $false ErrorAction = 'SilentlyContinue' } if ($Credential) { $removeParams['Credential'] = $Credential } Remove-ADGroupMember @removeParams $result.GroupsRemoved++ } } # Backup to extensionAttribute14 if ($groupNames.Count -gt 0) { $groupsBackup = ($groupNames | Sort-Object) -join ';' if ($groupsBackup.Length -gt 1024) { $groupsBackup = $groupsBackup.Substring(0, 1024) Write-Warning "On-prem groups backup truncated for $UserPrincipalName (exceeded 1024 chars)" } $setParams = @{ Identity = $aduser.DistinguishedName Replace = @{extensionAttribute14 = $groupsBackup } Server = $Server ErrorAction = 'Stop' } if ($Credential) { $setParams['Credential'] = $Credential } Set-ADUser @setParams $result.GroupsBackup = $groupsBackup } } Write-Verbose "Removed $($result.GroupsRemoved) on-prem groups from: $UserPrincipalName" } catch { $result.Error = $_.Exception.Message Write-Warning "Error removing on-prem groups for $UserPrincipalName : $($_.Exception.Message)" } return $result } # Remove Entra ID group memberships and licenses function Remove-EntraGroupMemberships { <# .SYNOPSIS Removes all Entra ID group memberships and licenses. .DESCRIPTION Removes user from all Entra ID (Azure AD) groups and assigned licenses. Backs up group list to extensionAttribute15 and license list to extensionAttribute13 for audit trail. For synced users, writes extension attributes to on-prem AD. .PARAMETER UserPrincipalName UPN of the user whose group memberships and licenses should be removed. .PARAMETER Server FQDN of the on-premises domain controller (for synced users). .PARAMETER Credential PSCredential object for domain admin authentication (for synced users). .EXAMPLE Remove-EntraGroupMemberships -UserPrincipalName 'user@uniphar.com' Removes all Entra ID groups and licenses, backs up to extension attributes. .EXAMPLE $cred = Get-OnPremAdCredential -KeyVaultName 'uni-core-on-prem-kv' Remove-EntraGroupMemberships -UserPrincipalName 'user@uniphar.com' -Server 'unidc10.uniphar.local' -Credential $cred For synced user, removes cloud groups and writes backup to on-prem AD extension attributes. .OUTPUTS PSCustomObject with UPN, GroupsRemoved, LicensesRemoved, BackedUp (bool), and Error properties. #> param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName, [Parameter(Mandatory = $false)] [string]$Server, [Parameter(Mandatory = $false)] [PSCredential]$Credential ) $result = [PSCustomObject]@{ UPN = $UserPrincipalName GroupsRemoved = 0 GroupsBackup = $null LicensesRemoved = 0 LicensesBackup = $null Error = $null } try { $user = Get-MgUser -UserId $UserPrincipalName -Property Id, OnPremisesSyncEnabled, MemberOf, AssignedLicenses, LicenseAssignmentStates, extensionAttribute13, extensionAttribute15 -ErrorAction Stop if (-not $user) { $result.Error = 'UserNotFound' return $result } # Remove groups (only cloud groups, skip synced) $groupNames = @() $memberOf = Get-MgUserMemberOf -UserId $user.Id -All -ErrorAction SilentlyContinue if ($memberOf) { foreach ($membership in $memberOf) { if ($membership.AdditionalProperties['@odata.type'] -eq '#microsoft.graph.group') { $group = Get-MgGroup -GroupId $membership.Id -Property Id, DisplayName, OnPremisesSyncEnabled -ErrorAction SilentlyContinue # Only remove from cloud-only groups if ($group -and -not $group.OnPremisesSyncEnabled) { try { Remove-MgGroupMemberByRef -GroupId $group.Id -DirectoryObjectId $user.Id -ErrorAction Stop $groupNames += $group.DisplayName $result.GroupsRemoved++ } catch { Write-Verbose "Could not remove from group $($group.DisplayName): $($_.Exception.Message)" } } } } # Backup to extensionAttribute15 if ($groupNames.Count -gt 0) { $groupsBackup = ($groupNames | Sort-Object) -join ';' if ($groupsBackup.Length -gt 1024) { $groupsBackup = $groupsBackup.Substring(0, 1024) Write-Warning "Entra groups backup truncated for $UserPrincipalName (exceeded 1024 chars)" } # Check if user is synced from on-prem - if so, write to on-prem AD if ($user.OnPremisesSyncEnabled) { Write-Verbose "User is synced from on-prem, attempting to write Entra group backup to on-prem AD extensionAttribute15" # Validate we have on-prem connectivity parameters if (-not $Server) { Write-Warning "Cannot write Entra group backup to on-prem AD: Server parameter not provided" $result.Error = if ($result.Error) { "$($result.Error); NoServerForBackup" } else { "NoServerForBackup" } } elseif (-not $Credential) { Write-Warning "Cannot write Entra group backup to on-prem AD: Credential parameter not provided" $result.Error = if ($result.Error) { "$($result.Error); NoCredentialForBackup" } else { "NoCredentialForBackup" } } else { try { $escapedUpn = Protect-LdapFilterValue $UserPrincipalName $filterAttribute = if ($UserPrincipalName -notlike '*@*') { 'samAccountName' } else { 'UserPrincipalName' } Write-Verbose "Looking up on-prem AD user: $filterAttribute = $UserPrincipalName" $aduserparams = @{ Filter = "$filterAttribute -eq '$escapedUpn'" Server = $Server Credential = $Credential ErrorAction = 'Stop' } $aduser = Get-ADUser @aduserparams if ($aduser) { Write-Verbose "Found on-prem AD user: $($aduser.DistinguishedName)" $setParams = @{ Identity = $aduser.DistinguishedName Replace = @{extensionAttribute15 = $groupsBackup } Server = $Server Credential = $Credential ErrorAction = 'Stop' } Set-ADUser @setParams $result.GroupsBackup = $groupsBackup Write-Verbose "Successfully wrote Entra groups to on-prem AD extensionAttribute15: $groupsBackup" } else { Write-Warning "On-prem AD user not found for $UserPrincipalName" $result.Error = if ($result.Error) { "$($result.Error); ADUserNotFound" } else { "ADUserNotFound" } } } catch { Write-Warning "Failed to write Entra group backup to on-prem AD for $UserPrincipalName : $($_.Exception.Message)" $result.Error = if ($result.Error) { "$($result.Error); ADWriteError: $($_.Exception.Message)" } else { "ADWriteError: $($_.Exception.Message)" } } } } else { # Cloud-only user - write directly to Entra Write-Verbose "Writing Entra group backup to cloud extensionAttribute15 for cloud-only user: $UserPrincipalName" try { Update-MgUser -UserId $user.Id -OnPremisesExtensionAttributes @{extensionAttribute15 = $groupsBackup } -ErrorAction Stop $result.GroupsBackup = $groupsBackup } catch { Write-Warning "Failed to write Entra group backup to cloud for $UserPrincipalName : $($_.Exception.Message)" } } } } # Remove licenses (only direct assignments, not group-based) if ($user.AssignedLicenses -and $user.AssignedLicenses.Count -gt 0) { $licensesToRemove = @() $licenseBackup = @() # Get all organization subscriptions for license name mapping $allSkus = Get-MgSubscribedSku -All -ErrorAction SilentlyContinue $skuLookup = @{} foreach ($sku in $allSkus) { $skuLookup[$sku.SkuId] = $sku.SkuPartNumber } foreach ($license in $user.LicenseAssignmentStates) { # Only remove direct licenses (AssignedByGroup is null/empty) if ([string]::IsNullOrEmpty($license.AssignedByGroup)) { $licensesToRemove += $license.SkuId # Store human-readable name (SkuPartNumber) if available, otherwise SkuId $licenseName = if ($skuLookup.ContainsKey($license.SkuId)) { $skuLookup[$license.SkuId] } else { $license.SkuId } $licenseBackup += $licenseName } } if ($licensesToRemove.Count -gt 0) { Set-MgUserLicense -UserId $user.Id -RemoveLicenses $licensesToRemove -AddLicenses @() -ErrorAction Stop $result.LicensesRemoved = $licensesToRemove.Count # Backup to extensionAttribute13 with human-readable names $licenseBackupString = ($licenseBackup | Sort-Object) -join ';' if ($licenseBackupString.Length -gt 1024) { $licenseBackupString = $licenseBackupString.Substring(0, 1024) Write-Warning "License backup truncated for $UserPrincipalName (exceeded 1024 chars)" } # Check if user is synced from on-prem - if so, write to on-prem AD if ($user.OnPremisesSyncEnabled) { Write-Verbose "User is synced from on-prem, attempting to write license backup to on-prem AD extensionAttribute13" # Validate we have on-prem connectivity parameters if (-not $Server) { Write-Warning "Cannot write license backup to on-prem AD: Server parameter not provided" $result.Error = if ($result.Error) { "$($result.Error); NoServerForLicenseBackup" } else { "NoServerForLicenseBackup" } } elseif (-not $Credential) { Write-Warning "Cannot write license backup to on-prem AD: Credential parameter not provided" $result.Error = if ($result.Error) { "$($result.Error); NoCredentialForLicenseBackup" } else { "NoCredentialForLicenseBackup" } } else { try { $escapedUpn = Protect-LdapFilterValue $UserPrincipalName $filterAttribute = if ($UserPrincipalName -notlike '*@*') { 'samAccountName' } else { 'UserPrincipalName' } Write-Verbose "Looking up on-prem AD user for license backup: $filterAttribute = $UserPrincipalName" $aduserparams = @{ Filter = "$filterAttribute -eq '$escapedUpn'" Server = $Server Credential = $Credential ErrorAction = 'Stop' } $aduser = Get-ADUser @aduserparams if ($aduser) { Write-Verbose "Found on-prem AD user: $($aduser.DistinguishedName)" $setParams = @{ Identity = $aduser.DistinguishedName Replace = @{extensionAttribute13 = $licenseBackupString } Server = $Server Credential = $Credential ErrorAction = 'Stop' } Set-ADUser @setParams $result.LicensesBackup = $licenseBackupString Write-Verbose "Successfully wrote licenses to on-prem AD extensionAttribute13: $licenseBackupString" } else { Write-Warning "On-prem AD user not found for license backup: $UserPrincipalName" $result.Error = if ($result.Error) { "$($result.Error); ADUserNotFoundForLicense" } else { "ADUserNotFoundForLicense" } } } catch { Write-Warning "Failed to write license backup to on-prem AD for $UserPrincipalName : $($_.Exception.Message)" $result.Error = if ($result.Error) { "$($result.Error); ADLicenseWriteError: $($_.Exception.Message)" } else { "ADLicenseWriteError: $($_.Exception.Message)" } } } } else { # Cloud-only user - write directly to Entra Write-Verbose "Writing license backup to cloud extensionAttribute13 for cloud-only user: $UserPrincipalName" try { Update-MgUser -UserId $user.Id -OnPremisesExtensionAttributes @{extensionAttribute13 = $licenseBackupString } -ErrorAction Stop $result.LicensesBackup = $licenseBackupString } catch { Write-Warning "Failed to write license backup to cloud for $UserPrincipalName : $($_.Exception.Message)" } } } } Write-Verbose "Removed $($result.GroupsRemoved) Entra groups and $($result.LicensesRemoved) licenses from: $UserPrincipalName" } catch { $result.Error = $_.Exception.Message Write-Warning "Error removing Entra groups/licenses for $UserPrincipalName : $($_.Exception.Message)" } return $result } # Remove authentication methods function Remove-EntraAuthenticationMethods { <# .SYNOPSIS Removes all authentication methods from Entra ID user account. .DESCRIPTION Removes all registered authentication methods including phone numbers, email, FIDO2 security keys, Microsoft Authenticator app, and other MFA methods to prevent unauthorized access recovery. .PARAMETER UserPrincipalName UPN of the user whose authentication methods should be removed. .EXAMPLE Remove-EntraAuthenticationMethods -UserPrincipalName 'user@uniphar.com' Removes all MFA methods and authentication methods from the account. .OUTPUTS PSCustomObject with UPN, MethodsRemoved count, RemovedMethods list, and Error properties. #> param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName ) $result = [PSCustomObject]@{ UPN = $UserPrincipalName MethodsRemoved = @() Error = $null } try { $user = Get-MgUser -UserId $UserPrincipalName -Property Id -ErrorAction Stop if (-not $user) { $result.Error = 'UserNotFound' return $result } # Phone methods try { $phoneMethods = Get-MgUserAuthenticationPhoneMethod -UserId $user.Id -ErrorAction Stop foreach ($method in $phoneMethods) { try { Remove-MgUserAuthenticationPhoneMethod -UserId $user.Id -PhoneAuthenticationMethodId $method.Id -ErrorAction Stop $result.MethodsRemoved += 'Phone' Write-Verbose "Removed phone method: $($method.PhoneType) - $($method.PhoneNumber)" } catch { Write-Warning "Failed to remove phone method for $UserPrincipalName : $($_.Exception.Message)" } } } catch { if ($_.Exception.Message -notlike '*does not exist*') { Write-Verbose "Phone methods error: $($_.Exception.Message)" } } # Email methods try { $emailMethods = Get-MgUserAuthenticationEmailMethod -UserId $user.Id -ErrorAction Stop foreach ($method in $emailMethods) { try { Remove-MgUserAuthenticationEmailMethod -UserId $user.Id -EmailAuthenticationMethodId $method.Id -ErrorAction Stop $result.MethodsRemoved += 'Email' Write-Verbose "Removed email method: $($method.EmailAddress)" } catch { Write-Warning "Failed to remove email method for $UserPrincipalName : $($_.Exception.Message)" } } } catch { if ($_.Exception.Message -notlike '*does not exist*') { Write-Verbose "Email methods error: $($_.Exception.Message)" } } # FIDO2 (passkeys) try { $fido2Methods = Get-MgUserAuthenticationFido2Method -UserId $user.Id -ErrorAction Stop foreach ($method in $fido2Methods) { try { Remove-MgUserAuthenticationFido2Method -UserId $user.Id -Fido2AuthenticationMethodId $method.Id -ErrorAction Stop $result.MethodsRemoved += 'FIDO2' Write-Verbose "Removed FIDO2 method: $($method.DisplayName)" } catch { Write-Warning "Failed to remove FIDO2 method for $UserPrincipalName : $($_.Exception.Message)" } } } catch { if ($_.Exception.Message -notlike '*does not exist*') { Write-Verbose "FIDO2 methods error: $($_.Exception.Message)" } } # Microsoft Authenticator try { $msAuthMethods = Get-MgUserAuthenticationMicrosoftAuthenticatorMethod -UserId $user.Id -ErrorAction Stop foreach ($method in $msAuthMethods) { try { Remove-MgUserAuthenticationMicrosoftAuthenticatorMethod -UserId $user.Id -MicrosoftAuthenticatorAuthenticationMethodId $method.Id -ErrorAction Stop $result.MethodsRemoved += 'MicrosoftAuthenticator' Write-Verbose "Removed Microsoft Authenticator method: $($method.DisplayName) - $($method.DeviceTag)" } catch { Write-Warning "Failed to remove Microsoft Authenticator for $UserPrincipalName : $($_.Exception.Message)" if (-not $result.Error) { $result.Error = "MicrosoftAuthenticator: $($_.Exception.Message)" } } } } catch { if ($_.Exception.Message -notlike '*does not exist*') { Write-Verbose "MS Authenticator methods error: $($_.Exception.Message)" } } # Software OATH try { $oathMethods = Get-MgUserAuthenticationSoftwareOathMethod -UserId $user.Id -ErrorAction Stop foreach ($method in $oathMethods) { try { Remove-MgUserAuthenticationSoftwareOathMethod -UserId $user.Id -SoftwareOathAuthenticationMethodId $method.Id -ErrorAction Stop $result.MethodsRemoved += 'SoftwareOATH' Write-Verbose "Removed Software OATH method" } catch { Write-Warning "Failed to remove Software OATH for $UserPrincipalName : $($_.Exception.Message)" } } } catch { if ($_.Exception.Message -notlike '*does not exist*') { Write-Verbose "Software OATH methods error: $($_.Exception.Message)" } } # Temporary Access Pass try { $tapMethods = Get-MgUserAuthenticationTemporaryAccessPassMethod -UserId $user.Id -ErrorAction Stop foreach ($method in $tapMethods) { try { Remove-MgUserAuthenticationTemporaryAccessPassMethod -UserId $user.Id -TemporaryAccessPassAuthenticationMethodId $method.Id -ErrorAction Stop $result.MethodsRemoved += 'TAP' Write-Verbose "Removed Temporary Access Pass method" } catch { Write-Warning "Failed to remove TAP for $UserPrincipalName : $($_.Exception.Message)" } } } catch { if ($_.Exception.Message -notlike '*does not exist*') { Write-Verbose "TAP methods error: $($_.Exception.Message)" } } # Windows Hello for Business try { $whfbMethods = Get-MgUserAuthenticationWindowsHelloForBusinessMethod -UserId $user.Id -ErrorAction Stop foreach ($method in $whfbMethods) { try { Remove-MgUserAuthenticationWindowsHelloForBusinessMethod -UserId $user.Id -WindowsHelloForBusinessAuthenticationMethodId $method.Id -ErrorAction Stop $result.MethodsRemoved += 'WindowsHello' Write-Verbose "Removed Windows Hello method: $($method.DisplayName)" } catch { Write-Warning "Failed to remove Windows Hello for $UserPrincipalName : $($_.Exception.Message)" } } } catch { if ($_.Exception.Message -notlike '*does not exist*') { Write-Verbose "Windows Hello methods error: $($_.Exception.Message)" } } # Passwordless Microsoft Authenticator try { $passwordlessMethods = Get-MgUserAuthenticationPasswordlessMicrosoftAuthenticatorMethod -UserId $user.Id -ErrorAction Stop foreach ($method in $passwordlessMethods) { try { Remove-MgUserAuthenticationPasswordlessMicrosoftAuthenticatorMethod -UserId $user.Id -PasswordlessMicrosoftAuthenticatorAuthenticationMethodId $method.Id -ErrorAction Stop $result.MethodsRemoved += 'Passwordless' Write-Verbose "Removed Passwordless Microsoft Authenticator method: $($method.DisplayName)" } catch { Write-Warning "Failed to remove Passwordless Authenticator for $UserPrincipalName : $($_.Exception.Message)" } } } catch { if ($_.Exception.Message -notlike '*does not exist*') { Write-Verbose "Passwordless methods error: $($_.Exception.Message)" } } Write-Verbose "Removed authentication methods from: $UserPrincipalName ($($result.MethodsRemoved.Count) total)" } catch { $result.Error = $_.Exception.Message Write-Warning "Error removing authentication methods for $UserPrincipalName : $($_.Exception.Message)" } return $result } # Clear phone numbers from on-premises AD function Clear-OnPremUserPhoneNumbers { <# .SYNOPSIS Clears all phone numbers from on-premises AD user account. .DESCRIPTION Removes telephoneNumber, mobile, facsimileTelephoneNumber, ipPhone, and otherTelephone attributes from on-premises Active Directory user account. .PARAMETER UserPrincipalName UPN of the user whose phone numbers should be cleared. .PARAMETER Server FQDN of the on-premises domain controller. .PARAMETER Credential PSCredential object for domain admin authentication (required for Hybrid Worker). .EXAMPLE $cred = Get-OnPremAdCredential -KeyVaultName 'uni-core-on-prem-kv' Clear-OnPremUserPhoneNumbers -UserPrincipalName 'user@uniphar.com' -Server 'unidc10.uniphar.local' -Credential $cred .OUTPUTS PSCustomObject with UPN, ClearedCount, and Error properties. #> param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName, [Parameter(Mandatory = $false)] [string]$Server, [Parameter(Mandatory = $false)] [PSCredential]$Credential ) $result = [PSCustomObject]@{ UPN = $UserPrincipalName PhonesCleared = 0 Error = $null } try { $escapedUpn = Protect-LdapFilterValue $UserPrincipalName # Check if this looks like a samAccountName (no @ sign) or a full UPN $filterAttribute = if ($UserPrincipalName -notlike '*@*') { 'samAccountName' } else { 'UserPrincipalName' } $aduserparams = @{ Filter = "$filterAttribute -eq '$escapedUpn'" Server = $Server Properties = 'telephoneNumber', 'mobile', 'otherTelephone', 'homePhone', 'ipPhone', 'pager' ErrorAction = 'Stop' } if ($Credential) { $aduserparams['Credential'] = $Credential } $aduser = Get-ADUser @aduserparams if (-not $aduser) { $result.Error = 'UserNotFound' return $result } $clearAttributes = @{} $phoneAttributes = @('telephoneNumber', 'mobile', 'otherTelephone', 'homePhone', 'ipPhone', 'pager') foreach ($attr in $phoneAttributes) { if ($aduser.$attr) { $clearAttributes[$attr] = $null $result.PhonesCleared++ } } if ($clearAttributes.Count -gt 0) { $setParams = @{ Identity = $aduser.DistinguishedName Clear = $clearAttributes.Keys Server = $Server ErrorAction = 'Stop' } if ($Credential) { $setParams['Credential'] = $Credential } Set-ADUser @setParams Write-Verbose "Cleared $($result.PhonesCleared) phone attributes for: $UserPrincipalName" } } catch { $result.Error = $_.Exception.Message Write-Warning "Error clearing on-prem phone numbers for $UserPrincipalName : $($_.Exception.Message)" } return $result } # Clear phone numbers from Entra ID function Clear-EntraUserPhoneNumbers { <# .SYNOPSIS Clears all phone numbers from Entra ID user account. .DESCRIPTION Removes BusinessPhones, MobilePhone, and FaxNumber properties from Entra ID (Azure AD) user account. .PARAMETER UserPrincipalName UPN of the user whose phone numbers should be cleared. .EXAMPLE Clear-EntraUserPhoneNumbers -UserPrincipalName 'user@uniphar.com' Clears all phone number fields in Entra ID. .OUTPUTS PSCustomObject with UPN, PhonesClearedCount, and Error properties. #> param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName ) $result = [PSCustomObject]@{ UPN = $UserPrincipalName PhonesCleared = 0 Error = $null } try { $user = Get-MgUser -UserId $UserPrincipalName -Property Id, BusinessPhones, MobilePhone, FaxNumber -ErrorAction Stop if (-not $user) { $result.Error = 'UserNotFound' return $result } $updateParams = @{} if ($user.BusinessPhones -and $user.BusinessPhones.Count -gt 0) { $updateParams['BusinessPhones'] = @() $result.PhonesCleared++ } if ($user.MobilePhone) { $updateParams['MobilePhone'] = $null $result.PhonesCleared++ } if ($user.FaxNumber) { $updateParams['FaxNumber'] = $null $result.PhonesCleared++ } if ($updateParams.Count -gt 0) { Update-MgUser -UserId $user.Id @updateParams -ErrorAction Stop Write-Verbose "Cleared $($result.PhonesCleared) phone attributes for: $UserPrincipalName" } } catch { $result.Error = $_.Exception.Message Write-Warning "Error clearing Entra phone numbers for $UserPrincipalName : $($_.Exception.Message)" } return $result } # Remove on-premises contact object function Remove-OnPremContact { param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName, [Parameter(Mandatory = $false)] [string]$Server, [Parameter(Mandatory = $false)] [PSCredential]$Credential ) $result = [PSCustomObject]@{ UPN = $UserPrincipalName ContactRemoved = $false Error = $null } try { # Search for contact by mail or display name $escapedUpn = Protect-LdapFilterValue $UserPrincipalName $searchParams = @{ Filter = "(mail -eq '$escapedUpn') -or (proxyAddresses -like 'smtp:$escapedUpn')" Server = $Server ErrorAction = 'SilentlyContinue' } if ($Credential) { $searchParams['Credential'] = $Credential } $contact = Get-ADObject @searchParams -Properties mail, displayName | Where-Object { $_.ObjectClass -eq 'contact' } if ($contact) { $removeParams = @{ Identity = $contact.DistinguishedName Server = $Server Confirm = $false ErrorAction = 'Stop' } if ($Credential) { $removeParams['Credential'] = $Credential } Remove-ADObject @removeParams $result.ContactRemoved = $true Write-Verbose "Removed on-prem contact for: $UserPrincipalName" } } catch { $result.Error = $_.Exception.Message Write-Warning "Error removing on-prem contact for $UserPrincipalName : $($_.Exception.Message)" } return $result } # Remove Entra ID contact object function Remove-EntraContact { param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName ) $result = [PSCustomObject]@{ UPN = $UserPrincipalName ContactRemoved = $false Error = $null } try { # Search for contact by mail $contacts = Get-MgContact -Filter "mail eq '$UserPrincipalName' or proxyAddresses/any(x:x eq 'smtp:$UserPrincipalName')" -ErrorAction SilentlyContinue if ($contacts) { foreach ($contact in $contacts) { Remove-MgContact -OrganizationalContactId $contact.Id -ErrorAction Stop $result.ContactRemoved = $true Write-Verbose "Removed Entra contact for: $UserPrincipalName" } } } catch { $result.Error = $_.Exception.Message Write-Warning "Error removing Entra contact for $UserPrincipalName : $($_.Exception.Message)" } return $result } # Move on-premises user to Disabled OU function Move-OnPremUserToDisabledOU { <# .SYNOPSIS Moves on-premises AD user to Disabled OU and backs up original location. .DESCRIPTION Backs up the user's current OU to extensionAttribute12, then moves the user to the designated Disabled OU for terminated employees. .PARAMETER UserPrincipalName UPN of the user to move. .PARAMETER DisabledOU Distinguished Name of the Disabled OU. Default: OU=Disabled,OU=Management,OU=IT,OU=Users,OU=Uniphar,DC=uniphar,DC=local .PARAMETER Server FQDN of the on-premises domain controller. .PARAMETER Credential PSCredential object for domain admin authentication (required for Hybrid Worker). .EXAMPLE $cred = Get-OnPremAdCredential -KeyVaultName 'uni-core-on-prem-kv' Move-OnPremUserToDisabledOU -UserPrincipalName 'user@uniphar.com' -Server 'unidc10.uniphar.local' -Credential $cred Moves user to default Disabled OU and backs up original location. .OUTPUTS PSCustomObject with UPN, Moved (bool), OriginalOU, NewOU, and Error properties. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName, [Parameter(Mandatory = $false)] [string]$DisabledOU = 'OU=Disabled,OU=Management,OU=IT,OU=Users,OU=Uniphar,DC=uniphar,DC=local', [Parameter(Mandatory = $false)] [string]$Server, [Parameter(Mandatory = $false)] [PSCredential]$Credential ) $result = [PSCustomObject]@{ UPN = $UserPrincipalName Moved = $false OriginalOU = $null NewOU = $DisabledOU OUBackedUp = $false Error = $null } try { $escapedUpn = Protect-LdapFilterValue $UserPrincipalName # Check if this looks like a samAccountName (no @ sign) or a full UPN $filterAttribute = if ($UserPrincipalName -notlike '*@*') { 'samAccountName' } else { 'UserPrincipalName' } Write-Verbose "Looking up on-prem AD user for OU move: $filterAttribute = $UserPrincipalName" $aduserparams = @{ Filter = "$filterAttribute -eq '$escapedUpn'" Server = $Server Properties = 'DistinguishedName', 'extensionAttribute12' ErrorAction = 'Stop' } if ($Credential) { $aduserparams['Credential'] = $Credential } $aduser = Get-ADUser @aduserparams if (-not $aduser) { $result.Error = 'UserNotFound' return $result } # Extract current OU from DistinguishedName $currentDN = $aduser.DistinguishedName # Remove the CN part to get just the OU path $currentOU = $currentDN -replace '^CN=.+?(?<!\\),', '' $result.OriginalOU = $currentOU Write-Verbose "User current location: $currentDN" Write-Verbose "User current OU: $currentOU" # Check if user is already in the target OU if ($currentOU -eq $DisabledOU) { Write-Verbose "User $UserPrincipalName is already in the Disabled OU" $result.Moved = $false $result.Error = 'AlreadyInDisabledOU' return $result } # Backup current OU to extensionAttribute12 try { Write-Verbose "Backing up original OU to extensionAttribute12: $currentOU" $backupParams = @{ Identity = $aduser.DistinguishedName Replace = @{extensionAttribute12 = $currentOU } Server = $Server ErrorAction = 'Stop' } if ($Credential) { $backupParams['Credential'] = $Credential } Set-ADUser @backupParams $result.OUBackedUp = $true Write-Verbose "Successfully backed up original OU to extensionAttribute12" } catch { Write-Warning "Failed to backup OU to extensionAttribute12 for $UserPrincipalName : $($_.Exception.Message)" $result.Error = "OUBackupFailed: $($_.Exception.Message)" # Continue with move even if backup fails } # Move user to Disabled OU if ($PSCmdlet.ShouldProcess($UserPrincipalName, "Move to Disabled OU: $DisabledOU")) { Write-Verbose "Moving user to Disabled OU: $DisabledOU" $moveParams = @{ Identity = $aduser.DistinguishedName TargetPath = $DisabledOU Server = $Server ErrorAction = 'Stop' } if ($Credential) { $moveParams['Credential'] = $Credential } Move-ADObject @moveParams $result.Moved = $true Write-Verbose "Successfully moved $UserPrincipalName to Disabled OU" } else { Write-Verbose "WhatIf: Would move $UserPrincipalName to Disabled OU" $result.Moved = $false $result.Error = 'WhatIf' } } catch { $result.Error = $_.Exception.Message Write-Warning "Error moving user to Disabled OU for $UserPrincipalName : $($_.Exception.Message)" } return $result } # Export module members Export-ModuleMember -Function @( 'Convert-MailboxToSharedWithOOO', 'Set-OutOfOfficeMessage', 'Remove-MailboxForwarding', 'Disable-EntraUserAccount', 'Disable-OnPremUserAccount', 'Hide-EntraUserFromGAL', 'Hide-OnPremUserFromGAL', 'Reset-OnPremUserPassword', 'Reset-EntraUserPassword', 'Remove-OnPremGroupMemberships', 'Remove-EntraGroupMemberships', 'Remove-EntraAuthenticationMethods', 'Clear-OnPremUserPhoneNumbers', 'Clear-EntraUserPhoneNumbers', 'Remove-OnPremContact', 'Remove-EntraContact', 'Move-OnPremUserToDisabledOU' ) |