diff --git a/Shared/M365/EXOConnection.ps1 b/Shared/M365/EXOConnection.ps1 new file mode 100644 index 0000000000..9562faf9ae --- /dev/null +++ b/Shared/M365/EXOConnection.ps1 @@ -0,0 +1,150 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# +.SYNOPSIS +This script defines a function `Connect-EXOAdvanced` that establishes a connection to Exchange Online. +It ensures that the required ExchangeOnlineManagement module (version 3.0.0 or higher by default) is installed and loaded. +The function supports single and multiple session connections, with optional parameters to control the connection details display and session prefix. +If the required module is not found, the script attempts to install it. +The function returns the connection information or null if the connection fails. + +.PARAMETER DoNotShowConnectionDetails + Optional switch to hide connection details. +.PARAMETER AllowMultipleSessions + Optional switch to allow multiple sessions. +.PARAMETER Prefix + Optional string to specify a prefix for the session. +.PARAMETER MinModuleVersion + Optional parameter to specify the minimum version of the ExchangeOnlineManagement module (default and minimum supported version is 3.0.0). + +.OUTPUTS +Microsoft.Exchange.Management.ExoPowershellSnapin.ConnectionInformation. The connection information object for the Exchange Online session. + +.EXAMPLE +$exoConnection = Connect-EXOAdvanced +This example establishes a connection to Exchange Online using the default settings. + +.EXAMPLE +$exoConnection = Connect-EXOAdvanced -AllowMultipleSessions +This example establishes a connection to Exchange Online and allows multiple sessions. + +.EXAMPLE +$exoConnection = Connect-EXOAdvanced -AllowMultipleSessions -Prefix Con2 +This example establishes a connection to Exchange Online, allows multiple sessions, and specifies a prefix "Con2" for the session. + +.EXAMPLE +$exoConnection2 = Connect-EXOAdvanced -MinModuleVersion 3.5.0 +This example establishes a connection to Exchange Online and specifies a minimum module version of 3.5.0. +#> + +. $PSScriptRoot\..\ModuleHandle.ps1 + +function Connect-EXOAdvanced { + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [Parameter(Mandatory = $false, ParameterSetName = 'SingleSession')] + [Parameter(Mandatory = $false, ParameterSetName = 'AllowMultipleSessions')] + [switch]$DoNotShowConnectionDetails, + [Parameter(Mandatory = $true, ParameterSetName = 'AllowMultipleSessions')] + [switch]$AllowMultipleSessions, + [Parameter(Mandatory = $false, ParameterSetName = 'AllowMultipleSessions')] + [string]$Prefix = $null, + [ValidateScript({ + if ($_ -lt [System.Version]'3.0.0.0') { + throw "Minimum supported version: 3.0.0.0" + } + $true + })] + [Parameter(Mandatory = $false, ParameterSetName = 'SingleSession')] + [Parameter(Mandatory = $false, ParameterSetName = 'AllowMultipleSessions')] + [System.Version]$MinModuleVersion = '3.0.0.0' + ) + + #Validate EXO 3.0 is installed and loaded + $requestModule = Request-Module -Module "ExchangeOnlineManagement" -MinModuleVersion $MinModuleVersion + + if (-not $requestModule) { + Write-Host "We cannot continue without ExchangeOnlineManagement Powershell module" -ForegroundColor Red + return $null + } + + #Validate EXO is connected or try to connect + $connections = $null + $newConnection = $null + try { + $connections = Get-ConnectionInformation -ErrorAction Stop | Where-Object { $_.State -eq 'Connected' } + } catch { + Write-Host "We cannot check connections. Error:`n$_" -ForegroundColor Red + return $null + } + + if ($null -eq $connections -or $AllowMultipleSessions) { + if ($connections | Where-Object { $_.ModulePrefix -eq $Prefix }) { + Write-Host "You already have a session" -ForegroundColor Yellow -NoNewline + if ($Prefix) { + Write-Host " with the prefix $Prefix." -ForegroundColor Yellow + } else { + Write-Host " without prefix." -ForegroundColor Yellow + } + $newConnection = $connections | Where-Object { $_.ModulePrefix -eq $Prefix } | Select-Object -First 1 + } else { + $prefixString = "." + if ($Prefix) { $prefixString = " with Prefix $Prefix." } + Write-Host "Not connected to Exchange Online$prefixString" -ForegroundColor Yellow + + if ($PSCmdlet.ShouldProcess("Do you want to add it?", "Adding an Exchange Online Session")) { + Write-Verbose "Connecting to Exchange Online session" + try { + Connect-ExchangeOnline -ShowBanner:$false -Prefix $Prefix -ErrorAction Stop + } catch { + Write-Host "We cannot connect to Exchange Online. Error:`n$_" -ForegroundColor Red + return $null + } + try { + $newConnections = Get-ConnectionInformation -ErrorAction Stop + } catch { + Write-Host "We cannot check connections. Error:`n$_" -ForegroundColor Red + return $null + } + foreach ($testConnection in $newConnections) { + if ($connections -notcontains $testConnection) { + $newConnection = $testConnection + } + } + } + } + if (@($newConnection).Count -gt 1) { + Write-Host "You have more than one Exchange Online sessions with Prefix $Prefix.`nPlease use just one session with same Prefix." -ForegroundColor Red + return $null + } + } else { + Write-Verbose "You already have an Exchange Online session" + if (@($connections).Count -gt 1) { + Write-Host "You have more than one Exchange Online sessions.`nPlease use just one session as you are not using AllowMultipleSessions" -ForegroundColor Red + return $null + } + $newConnection = $connections + } + + Write-Verbose "Connected session to Exchange Online" + $newConnection.PSObject.Properties | ForEach-Object { Write-Verbose "$($_.Name): $($_.Value)" } + if (-not $DoNotShowConnectionDetails) { + Show-EXOConnection -Connection $newConnection + } + return $newConnection +} + +function Show-EXOConnection { + param ( + [Parameter(Mandatory = $true)] + [Microsoft.Exchange.Management.ExoPowershellSnapin.ConnectionInformation]$Connection + ) + Write-Host "`nConnected to Exchange Online" + Write-Host "Session details" + Write-Host "Tenant Id: $($Connection.TenantId)" + Write-Host "User: $($Connection.UserPrincipalName)" + if ($($Connection.ModulePrefix)) { + Write-Host "Prefix: $($Connection.ModulePrefix)" + } +} diff --git a/Shared/M365/GraphConnection.ps1 b/Shared/M365/GraphConnection.ps1 new file mode 100644 index 0000000000..da133e720b --- /dev/null +++ b/Shared/M365/GraphConnection.ps1 @@ -0,0 +1,183 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# +.SYNOPSIS +This script defines a function `Connect-GraphAdvanced` that establishes a connection to Microsoft Graph. +It ensures that the required modules are installed and loaded. +The function accepts a list of scopes and modules, with optional parameters for tenant ID and connection details display. +If the required modules are not found, the script attempts to install them. +The function returns the connection information or null if the connection fails. + +.PARAMETER Scope + Mandatory array of strings specifying the scopes for the connection. +.PARAMETER Module + Mandatory array of strings specifying the modules required for the connection. +.PARAMETER TenantId + Optional array of strings specifying the tenant ID(s) for the connection. +.PARAMETER DoNotShowConnectionDetails + Optional switch to hide connection details. +.PARAMETER MinModuleVersion + Optional parameter to specify the minimum version of the Graph modules (default and minimum supported version 2.0.0). + +.OUTPUTS +Microsoft.Graph.PowerShell.Authentication.AuthContext. The connection information object for the Microsoft Graph session. + +.EXAMPLE +$graphConnection = Connect-GraphAdvanced -Scope User.Read, Mail.Read -Module Microsoft.Graph +This example establishes a connection to Microsoft Graph with the scopes "User.Read" and "Mail.Read" using the "Microsoft.Graph" module. + +.EXAMPLE +$graphConnection = Connect-GraphAdvanced -Scope Group.Read.All, User.Read.All -Module Microsoft.Graph.Users, Microsoft.Graph.Groups +This example establishes a connection to Microsoft Graph with the scopes "Group.Read.All" and "User.Read.All" using the "Microsoft.Graph.Users" and "Microsoft.Graph.Groups" modules. + +.EXAMPLE +$graphConnection = Connect-GraphAdvanced -Scope Group.Read.All, User.Read.All -Module Microsoft.Graph.Users, Microsoft.Graph.Groups -MinModuleVersion 2.25.0 +This example establishes a connection to Microsoft Graph with the scopes "Group.Read.All" and "User.Read.All" using the "Microsoft.Graph.Users" and "Microsoft.Graph.Groups" modules, and specifies a minimum module version of 2.25.0. +#> + +. $PSScriptRoot\..\ModuleHandle.ps1 + +function Connect-GraphAdvanced { + param ( + [Parameter(Mandatory = $true)] + [string[]]$Scope, + [Parameter(Mandatory = $true)] + [string[]]$Module, + [Parameter(Mandatory = $false)] + [string]$TenantId = $null, + [Parameter(Mandatory = $false)] + [switch]$DoNotShowConnectionDetails, + [ValidateScript({ + if ($_ -lt [System.Version]'2.0.0.0') { + throw "Minimum supported version: 2.0.0.0" + } + $true + })] + [Parameter(Mandatory = $false)] + [System.Version]$MinModuleVersion = '2.0.0.0' + ) + + #Validate Graph is installed and loaded + foreach ($m in $Module) { + $requestModule = $null + $requestModule = Request-Module -Module $m -MinModuleVersion $MinModuleVersion + if (-not $requestModule) { + Write-Host "We cannot continue without $m Powershell module" -ForegroundColor Red + return $null + } + } + + #Validate Graph is connected or try to connect + $connection = $null + try { + $connection = Get-MgContext -ErrorAction Stop + } catch { + Write-Host "We cannot check context. Error:`n$_" -ForegroundColor Red + return $null + } + + if ($null -eq $connection) { + Write-Host "Not connected to Graph" -ForegroundColor Yellow + $connection = Add-GraphConnection -Scope $Scope + } else { + Write-Verbose "You have a Graph sessions" + Write-Verbose "Checking scopes" + if (-not (Test-GraphScopeContext -Scope $connection.Scopes -ExpectedScope $Scope)) { + Write-Host "Not connected to Graph with expected scopes" -ForegroundColor Yellow + $connection = Add-GraphConnection -Scope $Scope + } else { + Write-Verbose "All scopes are present" + } + } + + if ($connection) { + Write-Verbose "Checking TenantId" + if ($TenantId) { + if ($connection.TenantId -ne $TenantId) { + Write-Host "Connected to $($connection.TenantId). Not expected tenant: $TenantId" -ForegroundColor Red + return $null + } else { + Write-Verbose "TenantId is correct" + } + } + + $connection.PSObject.Properties | ForEach-Object { Write-Verbose "$($_.Name): $($_.Value)" } + if (-not $DoNotShowConnectionDetails) { + Show-GraphContext -Context $connection + } + } + return $connection +} + +function Add-GraphConnection { + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [Parameter(Mandatory = $true)] + [string[]]$Scope + ) + + if ($PSCmdlet.ShouldProcess("Do you want to connect?", "We need a Graph connection with scopes $Scope")) { + Write-Verbose "Connecting to Microsoft Graph API using scopes $Scope" + try { + Connect-MgGraph -Scope $Scope -NoWelcome -ErrorAction Stop + } catch { + Write-Host "We cannot connect to Graph. Error:`n$_" -ForegroundColor Red + return $null + } + $connection = $null + try { + $connection = Get-MgContext -ErrorAction Stop + } catch { + Write-Host "We cannot check context. Error:`n$_" -ForegroundColor Red + return $null + } + Write-Verbose "Checking scopes" + if (-not $connection) { + Write-Host "We cannot continue without Graph Powershell session" -ForegroundColor Red + return $null + } + if (-not (Test-GraphScopeContext -Scope $connection.Scopes -ExpectedScope $Scope)) { + Write-Host "We cannot continue without Graph Powershell session without Expected Scopes" -ForegroundColor Red + return $null + } + return $connection + } +} + +function Test-GraphScopeContext { + [OutputType([bool])] + param ( + [Parameter(Mandatory = $true)] + [string[]]$ExpectedScope, + [Parameter(Mandatory = $true)] + [string[]]$Scope + ) + + $foundError = $false + foreach ($es in $ExpectedScope) { + if ($Scope -notcontains $es) { + Write-Host "The following scope is missing: $es" -ForegroundColor Red + $foundError = $true + } + } + + Write-Verbose "All expected scopes are $(if($foundError){ "NOT "})present." + return (-not $foundError) +} + +function Show-GraphContext { + param ( + [Parameter(Mandatory = $true)] + [Microsoft.Graph.PowerShell.Authentication.AuthContext]$Context + ) + Write-Host "`nConnected to Graph" + Write-Host "Session details" + Write-Host "Tenant Id: $($Context.TenantId)" + if ($Context.AuthType) { + Write-Host "AuthType: $($Context.AuthType)" + } + if ($Context.Account) { + Write-Host "Account: $($Context.Account)" + } +} diff --git a/Shared/ModuleHandle.ps1 b/Shared/ModuleHandle.ps1 new file mode 100644 index 0000000000..daa869b33f --- /dev/null +++ b/Shared/ModuleHandle.ps1 @@ -0,0 +1,105 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# +.SYNOPSIS +This script defines a function `Request-Module` that checks for the presence of specified PowerShell module. +If a module is not found, it attempts to install it on current user scope. +The function accepts a module names and an optional minimum version for the module. +It returns a boolean indicating whether a specified module was added successfully (installed if it is needed). + +.PARAMETER Module + Mandatory string specifying the names of the module to check and install if necessary. +.PARAMETER MinModuleVersion + Optional parameter to specify the minimum version of the module (default is null). + +.OUTPUTS +bool. A boolean indicating whether the specified module was added successfully (installed if it is needed). + +.EXAMPLE +$requestModule = Request-Module -Module "ExchangeOnlineManagement" +This example checks if the "ExchangeOnlineManagement" module is installed. If it is not found, the script attempts to install it. + +.EXAMPLE +$requestModule = Request-Module -Module "ExchangeOnlineManagement" -MinModuleVersion $MinModuleVersion +This example checks if the "ExchangeOnlineManagement" module with a specified minimum version is installed. If it is not found, the script attempts to install it. +#> + +. $PSScriptRoot\..\Shared\Confirm-Administrator.ps1 + +function Request-Module { + [OutputType([bool])] + param ( + [Parameter(Mandatory = $true)] + [string]$Module, + [Parameter(Mandatory = $false)] + [System.Version]$MinModuleVersion = $null, + [Parameter(Mandatory = $false)] + [switch]$InstallAllUsersIfNotAvailable + ) + + $installedModule = $null + $getParams = @{ + Name = $Module + ErrorAction = 'SilentlyContinue' + } + if ($MinModuleVersion) { + $getParams["MinimumVersion"] = $MinModuleVersion + } + + try { + $installedModule = Get-InstalledModule @getParams + } catch { + Write-Host "Get-InstalledModule fails. Error: `n$_" -ForegroundColor Red + return $false + } + + if ($installedModule) { + Write-Verbose "Module $Module is already installed." + return $true + } else { + Write-Host "Module $Module is not installed." + } + + if ($InstallAllUsersIfNotAvailable -and (-not (Confirm-Administrator))) { + Write-Host "Module $Module is not available and cannot be installed for all users because this PowerShell is not running in elevated mode." -ForegroundColor Red + return $false + } else { + Write-Verbose "Installing $Module" + $installParams = @{ + Name = $Module + Scope = "CurrentUser" + } + if ($InstallAllUsersIfNotAvailable) { + $installParams.Scope = "AllUsers" + Write-Verbose "Scope: AllUsers" + } + if ($MinModuleVersion) { + $installParams["MinimumVersion"] = $MinModuleVersion + Write-Verbose "with minimum version $MinModuleVersion" + } else { + Write-Verbose "without minimum version" + } + try { + Write-Host "Installing module $Module..." + Install-Module @installParams -AllowClobber + } catch { + Write-Host "Installation process fails. Error: `n$_" -ForegroundColor Red + return $false + } + $installedModule = $null + try { + $installedModule = Get-InstalledModule @getParams + } catch { + Write-Host "Get-InstalledModule fails. Error: `n$_" -ForegroundColor Red + return $false + } + if ($null -eq $installedModule) { + Write-Host "We could not install module: $Module" -ForegroundColor Red + return $false + } else { + Write-Host "Module $Module correctly installed." -ForegroundColor Green + return $true + } + } +}