Private/Utilities/Install-RequiredModules.ps1
function Install-RequiredModules { <# .SYNOPSIS Installs required PowerShell modules for PIM activation. .DESCRIPTION Validates and installs necessary Microsoft Graph modules and optionally Azure PowerShell modules. Automatically handles NuGet provider setup, repository trust configuration, and module versioning. Falls back to CurrentUser scope if not running as administrator. .PARAMETER RequiredModules Array of hashtables containing module specifications with Name and MinVersion properties. If not provided, defaults to core Microsoft Graph modules required for PIM operations. .PARAMETER IncludeAzureModules Switch to include Azure PowerShell modules (Az.Accounts, Az.Resources) for Azure resource support. .EXAMPLE Install-RequiredModules Installs default Microsoft Graph modules for PIM operations. .EXAMPLE Install-RequiredModules -IncludeAzureModules Installs Microsoft Graph modules plus Azure PowerShell modules. .EXAMPLE $modules = @(@{Name='Microsoft.Graph.Users'; MinVersion='2.0.0'}) Install-RequiredModules -RequiredModules $modules Installs only the specified modules. .OUTPUTS PSCustomObject Returns object with Success (boolean) and Error (string) properties indicating operation status. .NOTES Requires PowerShell 7 or later. Administrative privileges recommended for AllUsers scope installation. #> [CmdletBinding()] param( [Parameter()] [hashtable[]]$RequiredModules, [Parameter()] [switch]$IncludeAzureModules, [Parameter()] [switch]$Force, [Parameter()] [switch]$AutoResolveConflicts ) $result = [PSCustomObject]@{ Success = $true Error = $null } try { # Initialize module list with defaults if not provided if (-not $RequiredModules) { Write-Verbose "Using default Microsoft Graph module set" $moduleList = [System.Collections.ArrayList]::new() $null = $moduleList.AddRange(@( @{Name = 'Microsoft.Graph.Authentication'; MinVersion = '2.29.0'}, @{Name = 'Microsoft.Graph.Users'; MinVersion = '2.29.0'}, @{Name = 'Microsoft.Graph.Identity.DirectoryManagement'; MinVersion = '2.29.0'}, @{Name = 'Microsoft.Graph.Identity.Governance'; MinVersion = '2.29.0'}, @{Name = 'Microsoft.Graph.Groups'; MinVersion = '2.29.0'}, @{Name = 'Microsoft.Graph.Identity.SignIns'; MinVersion = '2.29.0'}, @{Name = 'Az.Accounts'; MinVersion = '5.1.0'} )) if ($IncludeAzureModules) { Write-Verbose "Including Azure PowerShell modules" $null = $moduleList.Add(@{Name = 'Az.Resources'; MinVersion = '6.0.0'}) } $RequiredModules = $moduleList.ToArray() } # Check for version conflicts before proceeding Write-Host "🔍 Checking for module version conflicts..." -ForegroundColor Yellow $moduleVersionMap = @{} foreach ($module in $RequiredModules) { $moduleVersionMap[$module.Name] = $module.MinVersion } if ($AutoResolveConflicts) { Write-Verbose "Checking for module version conflicts..." $conflictResult = Test-ModuleVersionConflicts -RequiredModuleVersions $moduleVersionMap -AutoResolve:$Force -Force:$Force if ($conflictResult.HasConflicts -and -not $conflictResult.AutoResolutionSuccess) { Write-Warning "Some version conflicts could not be resolved automatically." if ($conflictResult.Recommendations.Count -gt 0) { Write-Host "`nRecommendations:" -ForegroundColor Yellow foreach ($recommendation in $conflictResult.Recommendations) { Write-Host " • $recommendation" -ForegroundColor White } } if (-not $Force) { $userChoice = Read-Host "`nContinue with installation anyway? (y/N)" if ($userChoice -ne 'y') { $result.Success = $false $result.Error = "Installation cancelled due to version conflicts" return $result } } } elseif ($conflictResult.AutoResolutionSuccess) { Write-Verbose "Version conflicts resolved automatically" } else { Write-Verbose "No version conflicts detected" } } else { Write-Verbose "Skipping automatic conflict resolution" } $conflictCheck = Test-ModuleVersionConflicts -RequiredModuleVersions $moduleVersionMap if ($conflictCheck.HasConflicts) { Write-Warning "Module version conflicts detected!" foreach ($conflict in $conflictCheck.Conflicts) { if ($conflict.Severity -eq 'High') { Write-Warning "❌ $($conflict.ModuleName): Loaded v$($conflict.LoadedVersion) < Required v$($conflict.RequiredVersion)" } else { Write-Warning "⚠️ $($conflict.ModuleName): Loaded v$($conflict.LoadedVersion) > Required v$($conflict.RequiredVersion) (newer version detected)" } } if (-not $conflictCheck.SafeToProceed) { $result.Success = $false $result.Error = "Incompatible module versions are currently loaded. Please restart PowerShell session and try again." Write-Warning "Resolution steps:" foreach ($recommendation in $conflictCheck.Recommendations) { Write-Warning " • $recommendation" } return $result } else { Write-Warning "Proceeding with newer module versions - monitor for compatibility issues" } } else { Write-Verbose "✓ No module version conflicts detected" } # Determine installation scope based on privileges $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator") $installScope = if ($isAdmin) { 'AllUsers' } else { 'CurrentUser' } Write-Verbose "Installation scope: $installScope" # Process each required module foreach ($module in $RequiredModules) { Write-Verbose "Processing module: $($module.Name) (min version: $($module.MinVersion))" # Check if module is already loaded with sufficient version $loadedModule = Get-Module -Name $module.Name -ErrorAction SilentlyContinue if ($loadedModule -and $loadedModule.Version -ge $module.MinVersion) { Write-Verbose "✓ $($module.Name) v$($loadedModule.Version) already loaded" continue } # Check for suitable installed version $availableModules = Get-Module -ListAvailable -Name $module.Name -ErrorAction SilentlyContinue $suitableModule = $availableModules | Where-Object { $_.Version -ge $module.MinVersion } | Sort-Object Version -Descending | Select-Object -First 1 if ($suitableModule) { Write-Verbose "Found suitable version: $($module.Name) v$($suitableModule.Version)" try { Import-Module -Name $module.Name -MinimumVersion $module.MinVersion -ErrorAction Stop Write-Verbose "✓ $($module.Name) imported successfully" continue } catch { Write-Verbose "Import failed, proceeding with installation: $($_.Exception.Message)" } } # Install module if not available or insufficient version Write-Verbose "Installing $($module.Name)..." try { # Ensure NuGet provider is available $nugetProvider = Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue if (-not $nugetProvider -or $nugetProvider.Version -lt '2.8.5.201') { Write-Verbose "Installing NuGet provider..." Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope $installScope -ErrorAction Stop } # Configure PSGallery as trusted repository $psGallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue if ($psGallery.InstallationPolicy -ne 'Trusted') { Write-Verbose "Configuring PSGallery as trusted repository" Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction Stop } # Install the module $installParams = @{ Name = $module.Name MinimumVersion = $module.MinVersion Scope = $installScope Force = $true AllowClobber = $true Repository = 'PSGallery' ErrorAction = 'Stop' } Install-Module @installParams # Import the newly installed module $importParams = @{ Name = $module.Name MinimumVersion = $module.MinVersion ErrorAction = 'Stop' } Import-Module @importParams Write-Verbose "✓ $($module.Name) installed and imported successfully" } catch { # Fallback: retry with CurrentUser scope only try { Write-Verbose "Retrying installation with CurrentUser scope..." $fallbackParams = @{ Name = $module.Name MinimumVersion = $module.MinVersion Scope = 'CurrentUser' Force = $true AllowClobber = $true ErrorAction = 'Stop' } Install-Module @fallbackParams Import-Module @importParams Write-Verbose "✓ $($module.Name) installed successfully (fallback)" } catch { throw "Failed to install $($module.Name): $($_.Exception.Message)" } } } # Final validation of all required modules Write-Verbose "Validating module installation..." foreach ($module in $RequiredModules) { $loadedModule = Get-Module -Name $module.Name -ErrorAction SilentlyContinue if (-not $loadedModule) { throw "$($module.Name) failed to load after installation" } if ($loadedModule.Version -lt $module.MinVersion) { throw "$($module.Name) v$($loadedModule.Version) loaded but v$($module.MinVersion) required" } } Write-Verbose "All required modules validated successfully" } catch { $result.Success = $false $result.Error = $_.Exception.Message Write-Verbose "Installation failed: $($_.Exception.Message)" } return $result } |