Remove-IntunePrimaryUser.ps1

<#PSScriptInfo
.VERSION 1.1
.GUID 98552e9b-8f39-4050-8452-ecb603a2129a
.AUTHOR Alex Marrero
.COMPANYNAME
.COPYRIGHT
.TAGS Intune
.LICENSEURI
.PROJECTURI
.ICONURI
.EXTERNALMODULEDEPENDENCIES
.REQUIREDSCRIPTS
.EXTERNALSCRIPTDEPENDENCIES
.RELEASENOTES
    1.1 - Fixed module version alignment, try/finally structure, and added ScanAllDevices support.
.PRIVATEDATA
#>


<#
.DESCRIPTION
    Clears (removes) the Intune Primary User assignment from devices.
#>


<#
.SYNOPSIS
    Clears (removes) the Intune Primary User assignment from devices.

.DESCRIPTION
    For each targeted Intune managed device, this script checks the current Primary User via:
        GET /beta/deviceManagement/managedDevices/{id}/users
    If the Primary User matches the provided UserPrincipalName, it clears the relationship via:
        DELETE /beta/deviceManagement/managedDevices/{id}/users/$ref

    This does NOT delete the device object from Intune.

.PARAMETER UserPrincipalName
    The UPN of the user whose devices should have Primary User cleared.

.PARAMETER ScanAllDevices
    If specified, scans ALL managed devices in the tenant and checks each device's Primary User.
    This is most accurate but can be slower in large tenants.

.EXAMPLE
    .\Remove-IntunePrimaryUser.ps1 -UserPrincipalName "user@domain.com" -WhatIf

.EXAMPLE
    .\Remove-IntunePrimaryUser.ps1 -UserPrincipalName "user@domain.com" -ScanAllDevices -Confirm
#>


[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
param(
    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [string]$UserPrincipalName,

    [switch]$ScanAllDevices
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

#region ── Module Import (version-aligned) ──────────────────────────────────────

$requiredModules = @(
    'Microsoft.Graph.Authentication',
    'Microsoft.Graph.DeviceManagement',
    'Microsoft.Graph.Users'
)

# Determine the highest version common to ALL required modules so assemblies
# bind against the same Microsoft.Graph.Authentication.dll at runtime.
$commonVersions = $null
foreach ($m in $requiredModules) {
    $available = Get-Module -ListAvailable -Name $m -ErrorAction SilentlyContinue
    if (-not $available) {
        Write-Host "Installing missing module: $m" -ForegroundColor Yellow
        Install-Module -Name $m -Scope CurrentUser -Force -AllowClobber
        $available = Get-Module -ListAvailable -Name $m
    }
    $versions = $available | ForEach-Object { $_.Version } | Sort-Object -Descending
    if ($null -eq $commonVersions) {
        $commonVersions = $versions
    }
    else {
        $commonVersions = $commonVersions | Where-Object { $_ -in $versions }
    }
}

if (-not $commonVersions) {
    throw "No common module version found across: $($requiredModules -join ', '). Run: Install-Module Microsoft.Graph -Scope CurrentUser -Force"
}

$targetVersion = ($commonVersions | Sort-Object -Descending | Select-Object -First 1).ToString()
Write-Host "Loading Microsoft.Graph modules at aligned version: $targetVersion" -ForegroundColor Cyan

foreach ($m in $requiredModules) {
    Import-Module $m -RequiredVersion $targetVersion -ErrorAction Stop
}

#endregion ──────────────────────────────────────────────────────────────────────

try {
    #region ── Connect ──────────────────────────────────────────────────────────
    Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan
    Connect-MgGraph -Scopes @(
        'DeviceManagementManagedDevices.ReadWrite.All',
        'User.Read.All'
    ) -NoWelcome
    #endregion

    #region ── Resolve user ────────────────────────────────────────────────────
    $user = Get-MgUser -UserId $UserPrincipalName
    Write-Host "Resolved user: $($user.DisplayName) ($($user.Id))" -ForegroundColor Green
    #endregion

    #region ── Enumerate devices ───────────────────────────────────────────────
    $devicesToProcess = [System.Collections.Generic.List[PSObject]]::new()

    if ($ScanAllDevices) {
        Write-Host "ScanAllDevices specified — enumerating all managed devices..." -ForegroundColor Yellow
        $allDevices = Get-MgDeviceManagementManagedDevice -All
        $total = $allDevices.Count
        $i = 0

        foreach ($device in $allDevices) {
            $i++
            Write-Progress -Activity "Scanning devices" -Status "$i / $total - $($device.DeviceName)" -PercentComplete (($i / $total) * 100)

            try {
                $primaryUser = Invoke-MgGraphRequest -Method GET `
                    -Uri "beta/deviceManagement/managedDevices/$($device.Id)/users" `
                    -ErrorAction Stop

                $primaryUpn = ($primaryUser.value | Select-Object -First 1).userPrincipalName

                if ($primaryUpn -eq $UserPrincipalName) {
                    $devicesToProcess.Add($device)
                }
            }
            catch {
                Write-Verbose "Could not query primary user for $($device.DeviceName): $_"
            }
        }
        Write-Progress -Activity "Scanning devices" -Completed
    }
    else {
        # Faster path: query devices registered to the user via filter
        Write-Host "Querying devices with userPrincipalName = '$UserPrincipalName'..." -ForegroundColor Cyan
        $userDevices = Get-MgDeviceManagementManagedDevice -Filter "userPrincipalName eq '$UserPrincipalName'" -All

        foreach ($device in $userDevices) {
            # Confirm primary user actually matches (filter is on enrolled user, not always primary user)
            try {
                $primaryUser = Invoke-MgGraphRequest -Method GET `
                    -Uri "beta/deviceManagement/managedDevices/$($device.Id)/users" `
                    -ErrorAction Stop

                $primaryUpn = ($primaryUser.value | Select-Object -First 1).userPrincipalName

                if ($primaryUpn -eq $UserPrincipalName) {
                    $devicesToProcess.Add($device)
                }
                else {
                    Write-Verbose "Skipping $($device.DeviceName) — primary user is '$primaryUpn', not target."
                }
            }
            catch {
                Write-Verbose "Could not query primary user for $($device.DeviceName): $_"
            }
        }
    }
    #endregion

    #region ── Clear Primary User ──────────────────────────────────────────────
    if ($devicesToProcess.Count -eq 0) {
        Write-Host "No devices found with '$UserPrincipalName' as Primary User." -ForegroundColor Yellow
        return
    }

    Write-Host "`nFound $($devicesToProcess.Count) device(s) with Primary User = '$UserPrincipalName':" -ForegroundColor Cyan
    $devicesToProcess | ForEach-Object {
        Write-Host " • $($_.DeviceName) ($($_.Id)) — OS: $($_.OperatingSystem)" -ForegroundColor White
    }
    Write-Host ""

    $cleared = 0
    $failed  = 0

    foreach ($device in $devicesToProcess) {
        $target = "$($device.DeviceName) ($($device.Id))"

        if ($PSCmdlet.ShouldProcess($target, "Remove Primary User '$UserPrincipalName'")) {
            try {
                Invoke-MgGraphRequest -Method DELETE `
                    -Uri "beta/deviceManagement/managedDevices/$($device.Id)/users/`$ref" `
                    -ErrorAction Stop

                Write-Host " ✓ Cleared Primary User on $($device.DeviceName)" -ForegroundColor Green
                $cleared++
            }
            catch {
                Write-Warning " ✗ Failed to clear Primary User on $($device.DeviceName): $_"
                $failed++
            }
        }
    }

    Write-Host "`n── Summary ──" -ForegroundColor Cyan
    Write-Host " Devices processed : $($devicesToProcess.Count)"
    Write-Host " Cleared : $cleared" -ForegroundColor Green
    if ($failed -gt 0) {
        Write-Host " Failed : $failed" -ForegroundColor Red
    }
    #endregion
}
catch {
    Write-Error "Script failed: $_"
}
finally {
    try { Disconnect-MgGraph | Out-Null } catch { }
    Write-Host "Disconnected from Microsoft Graph" -ForegroundColor Cyan
}