Currently, the connector uses “Get-EXOMailbox” to query the Shared mailboxes for the permissions.
However, the command “Get-EXORecipient” is at least twice as fast and provides the same data. At least, the same data HelloID requires, it doesn't provide the UserPrincipalName, but this isn't required for usage in HelloID.
With “Get-EXOMailbox” we experience the error "timeout occurred" a lot, sometimes even all the time (depends on the amount of data), with “Get-EXORecipient” the data is available in about 10~15 seconds (this of course may vary based on the amount of data).
The updated script would be something like this:
#####################################################
# HelloID-Conn-Prov-Target-ExchangeOnline-Permissions-SharedMailboxes
#
# Version: 2.0.0
#####################################################
# Initialize default values
$c = $configuration | ConvertFrom-Json
# Set TLS to accept TLS, TLS 1.1 and TLS 1.2
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12
# Set debug logging
switch ($($c.isDebug)) {
$true { $VerbosePreference = "Continue" }
$false { $VerbosePreference = "SilentlyContinue" }
}
$InformationPreference = "Continue"
$WarningPreference = "Continue"
# Define configuration properties as required
$requiredConfigurationFields = @("AzureADOrganization", "AzureADTenantId", "AzureADAppId", "AzureADAppSecret")
# Used to connect to Exchange Online in an unattended scripting scenario using an App ID and App Secret to create an Access Token.
$AADOrganization = $c.AzureADOrganization
$AADTenantId = $c.AzureADTenantId
$AADAppID = $c.AzureADAppId
$AADAppSecret = $c.AzureADAppSecret
# PowerShell commands to import
$commands = @(
"Get-User"
, "Get-EXORecipient"
)
#region functions
function Resolve-HTTPError {
[CmdletBinding()]
param (
[Parameter(Mandatory,
ValueFromPipeline
)]
[object]$ErrorObject
)
process {
$httpErrorObj = [PSCustomObject]@{
FullyQualifiedErrorId = $ErrorObject.FullyQualifiedErrorId
MyCommand = $ErrorObject.InvocationInfo.MyCommand
RequestUri = $ErrorObject.TargetObject.RequestUri
ScriptStackTrace = $ErrorObject.ScriptStackTrace
ErrorMessage = ""
}
if ($ErrorObject.Exception.GetType().FullName -eq "Microsoft.PowerShell.Commands.HttpResponseException") {
$httpErrorObj.ErrorMessage = $ErrorObject.ErrorDetails.Message
}
elseif ($ErrorObject.Exception.GetType().FullName -eq "System.Net.WebException") {
$httpErrorObj.ErrorMessage = [System.IO.StreamReader]::new($ErrorObject.Exception.Response.GetResponseStream()).ReadToEnd()
}
Write-Output $httpErrorObj
}
}
function Get-ErrorMessage {
[CmdletBinding()]
param (
[Parameter(Mandatory,
ValueFromPipeline
)]
[object]$ErrorObject
)
process {
$errorMessage = [PSCustomObject]@{
VerboseErrorMessage = $null
AuditErrorMessage = $null
}
if ( $($ErrorObject.Exception.GetType().FullName -eq "Microsoft.PowerShell.Commands.HttpResponseException") -or $($ErrorObject.Exception.GetType().FullName -eq "System.Net.WebException")) {
$httpErrorObject = Resolve-HTTPError -Error $ErrorObject
$errorMessage.VerboseErrorMessage = $httpErrorObject.ErrorMessage
$errorMessage.AuditErrorMessage = $httpErrorObject.ErrorMessage
}
# If error message empty, fall back on $ex.Exception.Message
if ([String]::IsNullOrEmpty($errorMessage.VerboseErrorMessage)) {
$errorMessage.VerboseErrorMessage = $ErrorObject.Exception.Message
}
if ([String]::IsNullOrEmpty($errorMessage.AuditErrorMessage)) {
$errorMessage.AuditErrorMessage = $ErrorObject.Exception.Message
}
Write-Output $errorMessage
}
}
#endregion functions
try {
# Check if required fields are available in configuration object
$incompleteConfiguration = $false
foreach ($requiredConfigurationField in $requiredConfigurationFields) {
if ($requiredConfigurationField -notin $c.PsObject.Properties.Name) {
$incompleteConfiguration = $true
Write-Warning "Required configuration object field [$requiredConfigurationField] is missing"
}
elseif ([String]::IsNullOrEmpty($c.$requiredConfigurationField)) {
$incompleteConfiguration = $true
Write-Warning "Required configuration object field [$requiredConfigurationField] has a null or empty value"
}
}
if ($incompleteConfiguration -eq $true) {
throw "Configuration object incomplete, cannot continue."
}
try {
# Import module
$moduleName = "ExchangeOnlineManagement"
# If module is imported say that and do nothing
if (Get-Module -Verbose:$false | Where-Object { $_.Name -eq $ModuleName }) {
Write-Verbose "Module [$ModuleName] is already imported."
}
else {
# If module is not imported, but available on disk then import
if (Get-Module -ListAvailable -Verbose:$false | Where-Object { $_.Name -eq $ModuleName }) {
$module = Import-Module $ModuleName -Cmdlet $commands -Verbose:$false
Write-Verbose "Imported module [$ModuleName]"
}
else {
# If the module is not imported, not available and not in the online gallery then abort
throw "Module [$ModuleName] is not available. Please install the module using: Install-Module -Name [$ModuleName] -Force"
}
}
}
catch {
$ex = $PSItem
$errorMessage = Get-ErrorMessage -ErrorObject $ex
Write-Verbose "Error at Line [$($ex.InvocationInfo.ScriptLineNumber)]: $($ex.InvocationInfo.Line). Error: $($errorMessage.VerboseErrorMessage)"
throw "Error importing module [$ModuleName]. Error Message: $($errorMessage.AuditErrorMessage)"
}
# Connect to Exchange
try {
# Create access token
Write-Verbose "Creating Access Token"
$baseUri = "https://login.microsoftonline.com/"
$authUri = $baseUri + "$AADTenantId/oauth2/token"
$body = @{
grant_type = "client_credentials"
client_id = "$AADAppID"
client_secret = "$AADAppSecret"
resource = "https://outlook.office365.com"
}
$Response = Invoke-RestMethod -Method POST -Uri $authUri -Body $body -ContentType "application/x-www-form-urlencoded" -UseBasicParsing:$true
$accessToken = $Response.access_token
# Connect to Exchange Online in an unattended scripting scenario using an access token.
Write-Verbose "Connecting to Exchange Online"
$exchangeSessionParams = @{
Organization = $AADOrganization
AppID = $AADAppID
AccessToken = $accessToken
CommandName = $commands
ShowBanner = $false
ShowProgress = $false
TrackPerformance = $false
ErrorAction = "Stop"
}
$exchangeSession = Connect-ExchangeOnline @exchangeSessionParams
Write-Information "Successfully connected to Exchange Online"
}
catch {
$ex = $PSItem
$errorMessage = Get-ErrorMessage -ErrorObject $ex
Write-Verbose "Error at Line [$($ex.InvocationInfo.ScriptLineNumber)]: $($ex.InvocationInfo.Line). Error: $($errorMessage.VerboseErrorMessage)"
throw "Error connecting to Exchange Online. Error Message: $($errorMessage.AuditErrorMessage)"
}
# Get Exchange Online Shared Mailboxes
try {
Write-Verbose "Querying EXO Shared Mailboxes"
# Only get Exchange Shared Mailboxes (can be changed easily to get all mailboxes)
$properties = @("Guid", "DisplayName")
$mailboxes = Get-EXORecipient -RecipientTypeDetails SharedMailbox -Properties $properties -resultSize unlimited
Write-Information "Successfully queried EXO Shared Mailboxes. Result count: $(($mailboxes | Measure-Object).Count)"
}
catch {
$ex = $PSItem
$errorMessage = Get-ErrorMessage -ErrorObject $ex
Write-Verbose "Error at Line [$($ex.InvocationInfo.ScriptLineNumber)]: $($ex.InvocationInfo.Line). Error: $($errorMessage.VerboseErrorMessage)"
throw "Error querying EXO Shared Mailboxes. Error Message: $($errorMessage.AuditErrorMessage)"
}
}
finally {
# Send results
foreach ($mailbox in $mailboxes) {
# Shorten DisplayName to max. 100 chars
$displayName = "Shared Mailbox - $($mailbox.DisplayName)"
$displayName = $displayName.substring(0, [System.Math]::Min(100, $displayName.Length))
$permission = @{
DisplayName = $displayName
Identification = @{
Id = $mailbox.Guid
Name = $displayName
Permissions = @("Full Access", "Send As") # Options: Full Access,Send As, Send on Behalf
}
}
Write-Output ($permission | ConvertTo-Json -Depth 10)
}
}
Currently, the connector uses “Get-EXOMailbox” to query the Shared mailboxes for the permissions. However, the command “Get-EXORecipient” is at least twice as fast and provides the same data. At least, the same data HelloID requires, it doesn't provide the UserPrincipalName, but this isn't required for usage in HelloID. With “Get-EXOMailbox” we experience the error "timeout occurred" a lot, sometimes even all the time (depends on the amount of data), with “Get-EXORecipient” the data is available in about 10~15 seconds (this of course may vary based on the amount of data).
The updated script would be something like this: