Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added M365 connections to shared #2090

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions Shared/M365/EXOConnection.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

. $PSScriptRoot\..\ModuleHandle.ps1

iserrano76 marked this conversation as resolved.
Show resolved Hide resolved
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 = $true, ParameterSetName = 'AllowMultipleSessions')]
[string]$Prefix = $null
)

#Validate EXO 3.0 is installed and loaded
$requestModule = $false
$requestModule = Request-Module -Modules "ExchangeOnlineManagement" -MinModuleVersion 3.0.0
iserrano76 marked this conversation as resolved.
Show resolved Hide resolved

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.ModulePrefix -contains $Prefix) {
Write-Host "You already have a session with the prefix $Prefix" -ForegroundColor Red
return $null
} 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
}
dpaulson45 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
} 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 please use just one session. 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we returning a connection here? Trying to understand this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big reason why is trying to understand this section of code:

    if ($null -eq $connections -or $AllowMultipleSessions) {
        if ($connections.ModulePrefix -contains $Prefix) {
            Write-Host "You already have a session with the prefix $Prefix" -ForegroundColor Red
            return $null
        } else {

Copy link
Contributor Author

@iserrano76 iserrano76 Nov 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to return the connection because you could use different connections and you can control which one is under your responsibility, which one you created and which one is not your connection.

On that part of the code, I want to create a connection only if you do not have a connection and you do not use allow multiplesessions.
In case that you use allowmultiplesession, we will verify that we do not have a duplicated prefix.

It could be useful in future in case that we want to close a created connection or the one that you are going to use.

For example, in MDO we get information from EXO connection and compare with Graph connection to confirm that we are connected to the same tenant.

}

function Show-EXOConnection {
param (
[Parameter(Mandatory = $true)]
[Microsoft.Exchange.Management.ExoPowershellSnapin.ConnectionInformation]$Connection
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this correctly work when you have multiple sessions used with $AllowMultipleSessions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, you are sending by parameter the session that you created, just one, and that is the one that we are showing.
If you create a second one or a third one, is going to have a new call to the function that shows that information.
I think I test it when I created, but I will test it and paste some screenshots.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, $newConnection should only have 1 object in the array by the time we get here, so it should work as expected.

)
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)"
}
}
137 changes: 137 additions & 0 deletions Shared/M365/GraphConnection.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

. $PSScriptRoot\..\ModuleHandle.ps1

iserrano76 marked this conversation as resolved.
Show resolved Hide resolved
function Connect-GraphAdvanced {
param (
[Parameter(Mandatory = $true)]
[string[]]$Scopes,
[Parameter(Mandatory = $true)]
[string[]]$Modules,
[Parameter(Mandatory = $false)]
[string[]]$TenantId = $null,
[Parameter(Mandatory = $false)]
[switch]$DoNotShowConnectionDetails
)

#Validate Graph is installed and loaded
$requestModule = $false
$requestModule = Request-Module -Modules $Modules
iserrano76 marked this conversation as resolved.
Show resolved Hide resolved
if (-not $requestModule) {
Write-Host "We cannot continue without $Modules 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 -Scopes $Scopes
} else {
Write-Verbose "You have a Graph sessions"
Write-Verbose "Checking scopes"
if (-not (Test-GraphContext -Scopes $connection.Scopes -ExpectedScopes $Scopes)) {
Write-Host "Not connected to Graph with expected scopes" -ForegroundColor Yellow
$connection = Add-GraphConnection -Scopes $Scopes
} 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"
}
}

if (-not $DoNotShowConnectionDetails) {
$connection.PSObject.Properties | ForEach-Object { Write-Verbose "$($_.Name): $($_.Value)" }
iserrano76 marked this conversation as resolved.
Show resolved Hide resolved
Show-GraphContext -Context $connection
}
}
return $connection
}

function Add-GraphConnection {
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")]
param (
[Parameter(Mandatory = $true)]
[string[]]$Scopes
)

if ($PSCmdlet.ShouldProcess("Do you want to connect?", "We need a Graph connection with scopes $Scopes")) {
Write-Verbose "Connecting to Microsoft Graph API using scopes $Scopes"
try {
Connect-MgGraph -Scopes $Scopes -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-GraphContext -Scopes $connection.Scopes -ExpectedScopes $Scopes)) {
Write-Host "We cannot continue without Graph Powershell session without Expected Scopes" -ForegroundColor Red
return $null
}
return $connection
}
}

function Test-GraphContext {
iserrano76 marked this conversation as resolved.
Show resolved Hide resolved
[OutputType([bool])]
param (
[Parameter(Mandatory = $true)]
[string[]]$ExpectedScopes,
[Parameter(Mandatory = $true)]
[string[]]$Scopes
)

$foundError = $false
foreach ($expectedScope in $ExpectedScopes) {
if ($Scopes -notcontains $expectedScope) {
Write-Host "The following scope is missing: $expectedScope" -ForegroundColor Red
$foundError = $true
}
}

if ($foundError) {
return $false
} else {
Write-Verbose "All expected scopes are present."
return $true
}
iserrano76 marked this conversation as resolved.
Show resolved Hide resolved
}

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)"
Write-Host "Account: $($Context.Account)"
}
56 changes: 56 additions & 0 deletions Shared/ModuleHandle.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

iserrano76 marked this conversation as resolved.
Show resolved Hide resolved
function Request-Module {
[OutputType([bool])]
param (
[Parameter(Mandatory = $true)]
[string[]]$Modules,
[Parameter(Mandatory = $false)]
[System.Version]$MinModuleVersion = $null
Comment on lines +33 to +34
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a parameter validation here for if multiple $Modules are provided, that we should not be trying to find a $MinModuleVersion available.

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting I did not see that documented.
Maybe could be better to check one by one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also address this prior to merging.

)
dpaulson45 marked this conversation as resolved.
Show resolved Hide resolved

$installed = $null
iserrano76 marked this conversation as resolved.
Show resolved Hide resolved
Write-Verbose "Checking $Modules PowerShell Module"
if ($MinModuleVersion) {
Write-Verbose "with minimum version $minModuleVersion"
$installed = Get-InstalledModule -Name $Modules -MinimumVersion $MinModuleVersion -ErrorAction SilentlyContinue
} else {
Write-Verbose "without minimum version"
$installed = Get-InstalledModule -Name $Modules -ErrorAction SilentlyContinue
}

$foundError = $false
foreach ($module in $Modules) {
if ($null -eq $installed -or $installed.Name -notcontains $module) {
Write-Host "The following module is missing: $module" -ForegroundColor Yellow
$confirmed = $null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't required if we are setting $confirmed prior to testing it in an if statement.

try {
if ($MinModuleVersion) {
Write-Verbose "Installing $module with minimum version $minModuleVersion"
Install-Module -Name $module -Scope CurrentUser -MinimumVersion $MinModuleVersion
$confirmed = Get-InstalledModule -Name $module -MinimumVersion $MinModuleVersion -ErrorAction Stop
if (-not $confirmed) {
Write-Host "We could not install module: $module with minimum version $minModuleVersion" -ForegroundColor Red
$foundError = $true
}
} else {
Write-Verbose "Installing $module"
Install-Module -Name $module -Scope CurrentUser
$confirmed = Get-InstalledModule -Name $module -ErrorAction Stop
if (-not $confirmed) {
Write-Host "We could not install module: $module" -ForegroundColor Red
$confirmed = $true
iserrano76 marked this conversation as resolved.
Show resolved Hide resolved
}
}
} catch {
Write-Host "Installation process fails. Error: `n$_" -ForegroundColor Red
return $false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could leave, or this an option as well:

Write-Host "Installation process fails. Error: `n$_" -ForegroundColor Red
$foundError = $true
break

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not break the flow in any function, I return null and leave the decision to the one that use the function to stop if they do not get what they expect.
What do you think?

}
}
}
if ($foundError) {
return $false
}
return $true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could change to

return (-not $foundError) 

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think we should exit if we find an error. If we are using this function, we should just straight up exit if we run into an issue as the request of the script shouldn't work. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the first comment, agree, it is easier. I will simplify it.

About second one, similar to previous one, if works I return true if fails I return false and leave the decision on the one that use the function.
I think maybe you can try to request another version and it is more flexible.
What do you think?

}