Add functions to check and repair Microsoft.Graph module environment for compatibility

This commit is contained in:
Petr Štěpán
2026-07-27 21:31:07 +02:00
parent 4c49f49f11
commit 7b82711ec7
+196 -13
View File
@@ -151,6 +151,172 @@ function Test-PremiumAuthReportLicense {
return $false
}
function Test-GraphModuleEnvironment {
<#
.SYNOPSIS
Checks the Microsoft.Graph modules this script depends on for the
most common cause of hard-to-diagnose runtime failures: multiple
mismatched versions of the same module installed side by side.
.DESCRIPTION
When several versions of a Microsoft.Graph module are installed at
once, PowerShell can end up loading one version's assembly into the
session and then, mid-script, trying to auto-load a different
module that depends on a different version - which fails with
"assembly already loaded" or "could not load file or assembly"
errors. This surfaces as scattered per-user warnings deep into a
run rather than one clear message, so it's worth catching upfront.
.PARAMETER RequiredModuleNames
Names of the Microsoft.Graph modules this script depends on.
.OUTPUTS
[bool] $true if the environment looks consistent, $false if a
problem was found (details and a fix are written via Write-Warning).
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string[]]
$RequiredModuleNames
)
$requiredModuleNames = $RequiredModuleNames
# @(...) around each pipeline (not just an [array] cast on the variable)
# is required here: when a pipeline produces zero output, PowerShell
# assigns $null even to an [array]-typed variable, not an empty array -
# only wrapping the pipeline itself in @(...) guarantees a real array.
[array]$installed = @(Get-Module -ListAvailable -Name $requiredModuleNames -ErrorAction SilentlyContinue)
# Piped through Select-Object rather than direct dot-access ($installed.Name):
# member-enumeration on an empty array throws under StrictMode, and
# $installed IS legitimately empty on a machine with none of these
# modules installed at all.
[array]$installedNames = @($installed | Select-Object -ExpandProperty Name)
[array]$missingNames = @($requiredModuleNames | Where-Object { $_ -notin $installedNames })
[array]$allVersions = @($installed | Select-Object -ExpandProperty Version -Unique)
$hasVersionMismatch = $allVersions.Count -gt 1
# Display-only reference: whichever version most of the required
# modules currently have installed. Not the authoritative "correct"
# version (Repair-GraphModuleEnvironment resolves that separately from
# the Beta module's manifest / the gallery) - just enough to flag which
# rows are the odd ones out when printing the table below.
$referenceVersion = $null
if ($installed.Count -gt 0) {
$referenceVersion = ($installed | Group-Object Version | Sort-Object Count -Descending | Select-Object -First 1).Name
}
Write-Host 'Checking required Microsoft.Graph modules...' -ForegroundColor Cyan
$nameColumnWidth = 48
foreach ($name in $requiredModuleNames) {
[array]$moduleVersions = @($installed | Where-Object { $_.Name -eq $name } | Select-Object -ExpandProperty Version | Sort-Object -Descending)
$isOk = $false
if ($moduleVersions.Count -eq 1) {
if ($moduleVersions[0].ToString() -eq $referenceVersion) {
$isOk = $true
}
}
if ($isOk) {
Write-Host (" [OK] {0,-$nameColumnWidth} {1}" -f $name, $moduleVersions[0]) -ForegroundColor Green
}
else {
$versionDisplay = if ($moduleVersions.Count -gt 0) { $moduleVersions -join ', ' } else { 'not installed' }
Write-Host (" [FAIL] {0,-$nameColumnWidth} {1}" -f $name, $versionDisplay) -ForegroundColor Red
}
}
if ($missingNames.Count -eq 0 -and -not $hasVersionMismatch) {
return $true
}
Write-Warning 'Microsoft.Graph module check found a problem (see [FAIL] rows above) likely to cause runtime failures (e.g. "assembly already loaded" errors partway through the report).'
return $false
}
function Repair-GraphModuleEnvironment {
<#
.SYNOPSIS
Installs missing Microsoft.Graph modules and removes mismatched
versions of the ones this script depends on.
.DESCRIPTION
Determines the single version every required module must share -
the exact version Microsoft.Graph.Beta.Identity.SignIns' own
manifest pins for Microsoft.Graph.Authentication (a RequiredVersion,
not just a minimum), or, if that module isn't installed at all yet,
the latest version currently published on the gallery. For each
required module, it then installs that exact version if missing and
offers to remove any other installed version of it.
Every install/uninstall changes state outside this repository (and
removing a module version can affect other scripts on the machine
that depend on it), so this supports -WhatIf/-Confirm and prompts
for confirmation by default (ConfirmImpact = 'High').
.PARAMETER RequiredModuleNames
Names of the Microsoft.Graph modules this script depends on.
#>
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string[]]
$RequiredModuleNames
)
$betaModuleName = 'Microsoft.Graph.Beta.Identity.SignIns'
$betaModule = Get-Module -ListAvailable -Name $betaModuleName -ErrorAction SilentlyContinue |
Sort-Object Version -Descending | Select-Object -First 1
if ($betaModule) {
$pin = $betaModule.RequiredModules | Where-Object { $_.Name -eq 'Microsoft.Graph.Authentication' } | Select-Object -First 1
$targetVersion = if ($pin) { $pin.Version } else { $betaModule.Version }
}
else {
try {
Write-Host "Looking up the latest published version of $betaModuleName..." -ForegroundColor Cyan
$targetVersion = (Find-Module -Name $betaModuleName -ErrorAction Stop).Version
}
catch {
Write-Warning "Could not look up the latest version of $betaModuleName from the gallery: $_"
return
}
}
Write-Host "Target Microsoft.Graph module version: $targetVersion" -ForegroundColor Cyan
foreach ($name in $RequiredModuleNames) {
[array]$copies = @(Get-Module -ListAvailable -Name $name -ErrorAction SilentlyContinue)
$hasTarget = $copies | Where-Object { $_.Version -eq $targetVersion }
if (-not $hasTarget) {
if ($PSCmdlet.ShouldProcess("$name $targetVersion", 'Install-Module')) {
try {
Install-Module -Name $name -RequiredVersion $targetVersion -Scope CurrentUser -Force -Confirm:$false -AllowClobber -ErrorAction Stop
Write-Host "Installed $name $targetVersion" -ForegroundColor Green
}
catch {
Write-Warning "Could not install $name ${targetVersion}: $_"
}
}
else {
Write-Warning "$name $targetVersion was not installed - the script cannot continue without it."
}
}
[array]$stale = @($copies | Where-Object { $_.Version -ne $targetVersion })
foreach ($old in $stale) {
if ($PSCmdlet.ShouldProcess("$name $($old.Version)", 'Uninstall-Module')) {
try {
Uninstall-Module -Name $name -RequiredVersion $old.Version -Force -Confirm:$false -ErrorAction Stop
Write-Host "Removed $name $($old.Version)" -ForegroundColor Green
}
catch {
Write-Warning "Could not remove $name $($old.Version) - it may be installed system-wide (Scope AllUsers) and need an elevated PowerShell session to remove: $_"
}
}
}
}
}
function ConvertTo-FriendlyAuthMethod {
<#
.SYNOPSIS
@@ -410,23 +576,34 @@ $($methodRows -join "`n")
# --- Main --------------------------------------------------------------
try {
# Fail fast and loudly if a module this script depends on isn't
# installed OR can't actually be loaded (e.g. an assembly-version
# conflict with another installed Microsoft.Graph module version),
# rather than letting every per-user call into it fail silently later
# and produce an incomplete/misleading report. Microsoft.Graph.Beta.
# Identity.SignIns provides Get-MgBetaUserAuthenticationSignInPreference,
# which reads each user's actual default sign-in method - there is no
# v1.0/GA equivalent, this data is beta-only in Microsoft Graph.
$requiredBetaModule = 'Microsoft.Graph.Beta.Identity.SignIns'
if (-not (Get-Module -ListAvailable -Name $requiredBetaModule)) {
throw "Required module '$requiredBetaModule' is not installed. It's needed to read each user's actual default sign-in method. Install it with: Install-Module $requiredBetaModule -Scope CurrentUser"
# Fail fast and loudly if the Microsoft.Graph modules this script
# depends on aren't installed OR can't actually be loaded (e.g. an
# assembly-version conflict between mismatched module versions),
# rather than letting every per-user call into them fail silently later
# and produce an incomplete/misleading report. If a problem is found,
# offer to fix it (install what's missing, remove mismatched versions)
# rather than just describing the fix and stopping.
$requiredGraphModuleNames = @(
'Microsoft.Graph.Authentication'
'Microsoft.Graph.Beta.Identity.SignIns'
'Microsoft.Graph.Identity.DirectoryManagement'
'Microsoft.Graph.Identity.SignIns'
'Microsoft.Graph.Reports'
'Microsoft.Graph.Users'
)
if (-not (Test-GraphModuleEnvironment -RequiredModuleNames $requiredGraphModuleNames)) {
Repair-GraphModuleEnvironment -RequiredModuleNames $requiredGraphModuleNames
if (-not (Test-GraphModuleEnvironment -RequiredModuleNames $requiredGraphModuleNames)) {
throw 'Microsoft.Graph module environment still has a problem after the repair attempt - see the warnings above. Close this terminal window, open a new one, and try again; if it still fails, some of the old versions may be installed system-wide (Scope AllUsers) and need to be removed from an elevated PowerShell session.'
}
Write-Host 'Microsoft.Graph module environment repaired.' -ForegroundColor Green
}
$requiredBetaModule = 'Microsoft.Graph.Beta.Identity.SignIns'
try {
Import-Module -Name $requiredBetaModule -ErrorAction Stop
}
catch {
throw "Required module '$requiredBetaModule' is installed but failed to load - this is usually caused by multiple mismatched Microsoft.Graph module versions installed side by side (an assembly-version conflict), not a missing module. Run: Get-Module -ListAvailable 'Microsoft.Graph*' | Group-Object Name | Where-Object { (`$_.Group.Version | Sort-Object -Unique).Count -gt 1 } to find the mismatched ones, update them all to the same version, and remove the old ones. Underlying error: $_"
throw "Required module '$requiredBetaModule' failed to load even though the version check passed - see the underlying error for details: $_"
}
Write-Verbose 'Connecting to Microsoft Graph...'
@@ -624,7 +801,13 @@ catch {
exit 1
}
finally {
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
# -ErrorAction SilentlyContinue does not suppress "command not found" -
# that's a terminating parse/discovery-time error, not a stream event -
# so guard explicitly for the case where the script failed before
# Microsoft.Graph.Authentication was ever loaded (e.g. missing modules).
if (Get-Command -Name Disconnect-MgGraph -ErrorAction SilentlyContinue) {
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
}
}
exit 0