internal/Get-MsGraphResults.ps1
<# .SYNOPSIS Query Microsoft Graph API .EXAMPLE PS C:\>Get-MsGraphResults 'users' Return query results for first page of users. .EXAMPLE PS C:\>Get-MsGraphResults 'users' -ApiVersion beta Return query results for all users using the beta API. .EXAMPLE PS C:\>Get-MsGraphResults 'users' -UniqueId 'user1@domain.com','user2@domain.com' -Select id,userPrincipalName,displayName Return id, userPrincipalName, and displayName for user1@domain.com and user2@domain.com. #> function Get-MsGraphResults { [CmdletBinding()] [OutputType([PSCustomObject])] param ( # Graph endpoint such as "users". [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [uri[]] $RelativeUri, # Specifies unique Id(s) for the URI endpoint. For example, users endpoint accepts Id or UPN. [Parameter(Mandatory = $false, Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('Id')] #[ValidateNotNullOrEmpty()] [string[]] $UniqueId, # Filters properties (columns). [Parameter(Mandatory = $false)] [string[]] $Select, # Filters results (rows). https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter [Parameter(Mandatory = $false)] [string] $Filter, # Specifies the page size of the result set. [Parameter(Mandatory = $false)] [int] $Top, # Include a count of the total number of items in a collection [Parameter(Mandatory = $false)] [switch] $Count, # Parameters such as "$orderby". [Parameter(Mandatory = $false)] [hashtable] $QueryParameters, # API Version. [Parameter(Mandatory = $false)] [ValidateSet('v1.0', 'beta')] [string] $ApiVersion = 'v1.0', # Specifies consistency level. [Parameter(Mandatory = $false)] [string] $ConsistencyLevel = "eventual", # Total requests to calcuate progress bar when using pipeline. [Parameter(Mandatory = $false)] [int] $TotalRequests, # Copy OData Context to each result value. [Parameter(Mandatory = $false)] [switch] $KeepODataContext, # Add OData Type to each result value. [Parameter(Mandatory = $false)] [switch] $AddODataType, # Incapsulate member and owner reference calls with a parent object. [Parameter(Mandatory = $false)] [switch] $IncapsulateReferenceListInParentObject, # Group results in array by request. [Parameter(Mandatory = $false)] [switch] $GroupOutputByRequest, # Disable deduplication of UniqueId values. [Parameter(Mandatory = $false)] [switch] $DisableUniqueIdDeduplication, # Only return first page of results. [Parameter(Mandatory = $false)] [switch] $DisablePaging, # Disable consolidating uniqueIds using getByIds endpoint [Parameter(Mandatory = $false)] [switch] $DisableGetByIdsBatching, # Specify GetByIds Batch size. [Parameter(Mandatory = $false)] [int] $GetByIdsBatchSize = 1000, # Enables in filter by in on ids for requried uniqueIds; $filter={previous filter} and id in ({csv with ids}) # Should be more flexible than GetByIds, scalability to be tested to eventually replace getbyids [Parameter(Mandatory = $false)] [switch] $EnableInFilter, [Parameter(Mandatory = $false)] [int] $InFilterBatchSize = 15, # Force individual requests to MS Graph. [Parameter(Mandatory = $false)] [switch] $DisableBatching, # Specify Batch size. [Parameter(Mandatory = $false)] [int] $BatchSize = 20, # Base URL for Microsoft Graph API. [Parameter(Mandatory = $false)] [uri] $GraphBaseUri = $script:mapMgEnvironmentToMgEndpoint[$script:ConnectState.CloudEnvironment] ) begin { [uri] $uriGraphVersionBase = [IO.Path]::Combine($GraphBaseUri.AbsoluteUri, $ApiVersion) $listRequests = New-Object 'System.Collections.Generic.Dictionary[string,System.Collections.Generic.List[pscustomobject]]' $listRequests.Add($uriGraphVersionBase.AbsoluteUri, (New-Object 'System.Collections.Generic.List[pscustomobject]')) [System.Collections.Generic.List[guid]] $listIds = New-Object 'System.Collections.Generic.List[guid]' [System.Collections.Generic.HashSet[uri]] $hashUri = New-Object 'System.Collections.Generic.HashSet[uri]' $ProgressState = Start-Progress -Activity 'Microsoft Graph Requests' -Total $TotalRequests function Catch-MsGraphError { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Management.Automation.ErrorRecord] $ErrorRecord ) # throw error record directly if no response is found on the exception if (!$_.Exception.psobject.Properties.Name.Contains('Response')) { throw $ErrorRecord } ## Get Response Body if ($_.ErrorDetails) { $Response = '{0} {1} HTTP/{2}' -f $_.Exception.Response.StatusCode.value__, $_.Exception.Response.ReasonPhrase, $_.Exception.Response.Version $ContentType = $_.Exception.Response.Content.Headers.ContentType.ToString() $ResponseContent = ConvertFrom-Json $_.ErrorDetails.Message } elseif ($_.Exception -is [System.Net.WebException]) { if ($_.Exception.Response) { $Response = '{0} {1} HTTP/{2}' -f $_.Exception.Response.StatusCode.value__, $_.Exception.Response.StatusDescription, $_.Exception.Response.ProtocolVersion $ContentType = $_.Exception.Response.Headers.GetValues('Content-Type') -join '; ' $StreamReader = New-Object System.IO.StreamReader -ArgumentList $_.Exception.Response.GetResponseStream() try { $ResponseContent = ConvertFrom-Json $StreamReader.ReadToEnd() } finally { $StreamReader.Close() } } } Write-Debug -Message (ConvertTo-Json ([PSCustomObject]@{ 'Request' = '{0} {1}' -f $_.TargetObject.Method, $_.TargetObject.RequestUri.AbsoluteUri 'Response' = $Response 'Response.Content-Type' = $ContentType 'Response.Content' = $ResponseContent 'Response.Header.Date' = $_.Exception.Response.Headers.GetValues('Date')[0] 'Response.Header.request-id' = $_.Exception.Response.Headers.GetValues('request-id')[0] 'Response.Header.client-request-id' = $_.Exception.Response.Headers.GetValues('client-request-id')[0] 'Response.Header.x-ms-ags-diagnostic' = $_.Exception.Response.Headers.GetValues('x-ms-ags-diagnostic')[0] | ConvertFrom-Json }) -Depth 3) if ($ResponseContent) { ## Write Custom Error if ($ResponseContent.error.code -eq 'Authentication_ExpiredToken' -or $ResponseContent.error.code -eq 'Service_ServiceUnavailable' -or $ResponseContent.error.code -eq 'Request_UnsupportedQuery') { #Write-AppInsightsException $_.Exception Write-Error -Exception $_.Exception -Message $ResponseContent.error.message -ErrorId $ResponseContent.error.code -Category $_.CategoryInfo.Category -CategoryActivity $_.CategoryInfo.Activity -CategoryReason $_.CategoryInfo.Reason -CategoryTargetName $_.CategoryInfo.TargetName -CategoryTargetType $_.CategoryInfo.TargetType -TargetObject $_.TargetObject -ErrorAction Stop } else { if ($ResponseContent.error.code -eq 'Request_ResourceNotFound') { Write-Error -Exception $_.Exception -Message $ResponseContent.error.message -ErrorId $ResponseContent.error.code -Category $_.CategoryInfo.Category -CategoryActivity $_.CategoryInfo.Activity -CategoryReason $_.CategoryInfo.Reason -CategoryTargetName $_.CategoryInfo.TargetName -CategoryTargetType $_.CategoryInfo.TargetType -TargetObject $_.TargetObject -ErrorVariable cmdError -ErrorAction SilentlyContinue Write-Warning $ResponseContent.error.message } else { Write-Error -Exception $_.Exception -Message $ResponseContent.error.message -ErrorId $ResponseContent.error.code -Category $_.CategoryInfo.Category -CategoryActivity $_.CategoryInfo.Activity -CategoryReason $_.CategoryInfo.Reason -CategoryTargetName $_.CategoryInfo.TargetName -CategoryTargetType $_.CategoryInfo.TargetType -TargetObject $_.TargetObject -ErrorVariable cmdError } Write-AppInsightsException $cmdError.Exception } } else { throw $ErrorRecord } } function Test-MsGraphBatchError ($BatchResponse) { if ($BatchResponse.status -ne '200') { Write-Debug -Message (ConvertTo-Json $BatchResponse -Depth 3) if ($BatchResponse.body.error.code -eq 'Authentication_ExpiredToken' -or $BatchResponse.body.error.code -eq 'Service_ServiceUnavailable' -or $BatchResponse.body.error.code -eq 'Request_UnsupportedQuery') { Write-Error -Message $BatchResponse.body.error.message -ErrorId $BatchResponse.body.error.code -ErrorAction Stop } else { if ($BatchResponse.body.error.code -eq 'Request_ResourceNotFound') { Write-Error -Message $BatchResponse.body.error.message -ErrorId $BatchResponse.body.error.code -ErrorVariable cmdError -ErrorAction SilentlyContinue Write-Warning $BatchResponse.body.error.message } else { Write-Error -Message $BatchResponse.body.error.message -ErrorId $BatchResponse.body.error.code -ErrorVariable cmdError } Write-AppInsightsException $cmdError.Exception } return $true } return $false } function Add-MsGraphRequest { param ( # A collection of request objects. [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [object[]] $Requests, # Base URL for Microsoft Graph API. [Parameter(Mandatory = $false)] [uri] $GraphBaseUri = 'https://graph.microsoft.com/' ) process { foreach ($Request in $Requests) { if ($DisableBatching) { if ($ProgressState) { Update-Progress $ProgressState -CurrentOperation ('{0} {1}' -f $Request.method.ToUpper(), $Request.url) -IncrementBy 1 } Invoke-MsGraphRequest $Request -GraphBaseUri $GraphBaseUri } else { $listRequests[$GraphBaseUri].Add($Request) ## Invoke when there are enough for a batch while ($listRequests[$GraphBaseUri].Count -ge $BatchSize) { Invoke-MsGraphBatchRequest $listRequests[$GraphBaseUri][0..($BatchSize - 1)] -BatchSize $BatchSize -ProgressState $ProgressState -GraphBaseUri $GraphBaseUri $listRequests[$GraphBaseUri].RemoveRange(0, $BatchSize) } } } } } function Invoke-MsGraphBatchRequest { param ( # A collection of request objects. [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [object[]] $Requests, # Specify Batch size. [Parameter(Mandatory = $false)] [int] $BatchSize = 20, # Use external progress object. [Parameter(Mandatory = $false)] [psobject] $ProgressState, # Base URL for Microsoft Graph API. [Parameter(Mandatory = $false)] [uri] $GraphBaseUri = 'https://graph.microsoft.com/' ) begin { [bool] $ExternalProgress = $false if ($ProgressState) { $ExternalProgress = $true } else { $ProgressState = Start-Progress -Activity 'Microsoft Graph Requests - Batched' -Total $Requests.Count $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew() } [uri] $uriEndpoint = [IO.Path]::Combine($GraphBaseUri.AbsoluteUri, '$batch') $listRequests = New-Object 'System.Collections.Generic.List[pscustomobject]' } process { foreach ($Request in $Requests) { $listRequests.Add($Request) } } end { [array] $BatchRequests = New-MsGraphBatchRequest $listRequests -BatchSize $BatchSize for ($iRequest = 0; $iRequest -lt $BatchRequests.Count; $iRequest++) { if ($ProgressState.Total -gt $BatchSize) { Update-Progress $ProgressState -CurrentOperation ('{0} {1}' -f $BatchRequests[$iRequest].method.ToUpper(), $BatchRequests[$iRequest].url) -IncrementBy $BatchRequests[$iRequest].body.requests.Count } $resultsBatch = Invoke-MsGraphRequest $BatchRequests[$iRequest] -NoAppInsights -GraphBaseUri $GraphBaseUri [array] $resultsBatch = $resultsBatch.responses | Sort-Object -Property { [int]$_.id } $throttledRequests = @() [double]$maxRetryAfter = 1.0 foreach ($results in ($resultsBatch)) { # check if batch result failed and call the endpoint or throw if ($results.status -eq "429") { # request was throttled $throttledRequests += $listRequests[$results.id] # check if a retry after was recieved if ($results.psobject.Properties.Name.Contains('headers')) { if ($results.headers.psobject.Properties.Name.Contains('Retry-After')) { $RetryAfter = [double]$results.headers.'Retry-After' if ($RetryAfter -gt $maxRetryAfter) { $maxRetryAfter = $RetryAfter } } } continue } if (!(Test-MsGraphBatchError $results)) { if ($IncapsulateReferenceListInParentObject -and $listRequests[$results.id].url -match '.*/(.+)/(.+)/((?:transitive)?members|owners)') { [PSCustomObject]@{ id = $Matches[2] '@odata.type' = '#{0}' -f (Get-MsGraphEntityType $GraphBaseUri.AbsoluteUri -EntityName $Matches[1]) $Matches[3] = Complete-MsGraphResult $results.body -DisablePaging:$DisablePaging -KeepODataContext:$KeepODataContext -AddODataType:$AddODataType -GroupOutputByRequest -Request $listRequests[$results.id] -GraphBaseUri $GraphBaseUri } } else { Complete-MsGraphResult $results.body -DisablePaging:$DisablePaging -KeepODataContext:$KeepODataContext -AddODataType:$AddODataType -GroupOutputByRequest:$GroupOutputByRequest -Request $listRequests[$results.id] -GraphBaseUri $GraphBaseUri } } } if ($throttledRequests.Count -gt 0) { Write-Warning "$($throttledRequests.Count) requests have been throttled; Retrying after $($maxRetryAfter)s" Start-Sleep -Seconds $maxRetryAfter foreach($request in $throttledRequests) { Invoke-MsGraphRequest $request -NoAppInsights -GraphBaseUri $GraphBaseUri -RetryAfter $RetryAfter } } } if (!$ExternalProgress) { $Stopwatch.Stop() Write-AppInsightsDependency ('{0} {1}' -f 'POST', $uriEndpoint.AbsolutePath) -Type 'MS Graph' -Data ("{0} {1}`r`n`r`n{2}" -f 'POST', $uriEndpoint.AbsoluteUri, ('{{"requests":[...{0}...]}}' -f $listRequests.Count)) -Duration $Stopwatch.Elapsed -Success ($null -ne $resultsBatch) Stop-Progress $ProgressState } } } function Invoke-MsGraphRequest { param ( # A collection of request objects. [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [psobject] $Request, # Do not write application insights dependency. [Parameter(Mandatory = $false)] [switch] $NoAppInsights, # Base URL for Microsoft Graph API. [Parameter(Mandatory = $false)] [uri] $GraphBaseUri = 'https://graph.microsoft.com/', # Number of retries in case of throttling [Parameter(Mandatory = $false)] [int] $MaxRetries = 5, [Parameter(Mandatory = $false)] [double] $RetryAfter = 1 ) process { [uri] $uriEndpoint = $Request.url if (!$uriEndpoint.IsAbsoluteUri) { $uriEndpoint = [IO.Path]::Combine($GraphBaseUri.AbsoluteUri, $Request.url.TrimStart('/')) } #if ($uriEndpoint.Segments -contains 'directoryObjects/') { $NoAppInsights = $true } [hashtable] $paramInvokeRestMethod = @{ Method = $Request.method Uri = $uriEndpoint } if ($Request.psobject.Properties.Name -contains 'headers') { $paramInvokeRestMethod.Add('Headers', $Request.headers) } if ($Request.psobject.Properties.Name -contains 'body') { $paramInvokeRestMethod.Add('Body', ($Request.body | ConvertTo-Json -Depth 10 -Compress)) $paramInvokeRestMethod.Add('ContentType', 'application/json') } ## Get results $results = $null $MsGraphSession = Confirm-ModuleAuthentication -MsGraphSession -ErrorAction Stop if (!$NoAppInsights) { $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew() } try { for($Retries = 0; $Retries -lt $MaxRetries; $Retries++) { try { $results = Invoke-RestMethod -WebSession $MsGraphSession -UseBasicParsing @paramInvokeRestMethod -ErrorAction Stop # break the loop if no error was raised break } catch { ## error while invoking graph if ($Retries -eq $MaxRetries-1) { # catch error if it was the last try Catch-MsGraphError $_ } ## check if throttling happened if ($_.Exception.PSobject.Properties.Name.Contains("Response") -and $_.Exception.Response.StatusCode.value__ -eq 429) { # Get the retry after header try { $RetryAfter = [double]($_.Exception.Response.Headers.GetValues('Retry-After')[0]) } catch { Write-Verbose "Request throttled but Retry-After not provided ($(Request.url)) using exponential backoff ($(RetryAfter)s)" } } # request had an error and has not reached maximum retries Write-Warning "$($paramInvokeRestMethod['Method']) $($paramInvokeRestMethod['Uri']); error $($_.Exception.Message); attempt $($Retries+1) out of $MaxRetries. Retrying after $($RetryAfter)s" Start-Sleep -Seconds $RetryAfter $RetryAfter = $RetryAfter * 2 } } if ($results) { if ($IncapsulateReferenceListInParentObject -and $Request.url -match '.*/(.+)/(.+)/((?:transitive)?members|owners)') { [PSCustomObject]@{ id = $Matches[2] '@odata.type' = '#{0}' -f (Get-MsGraphEntityType $GraphBaseUri.AbsoluteUri -EntityName $Matches[1]) $Matches[3] = Complete-MsGraphResult $results -DisablePaging:$DisablePaging -KeepODataContext:$KeepODataContext -AddODataType:$AddODataType -GroupOutputByRequest -Request $Request -GraphBaseUri $GraphBaseUri } } else { Complete-MsGraphResult $results -DisablePaging:$DisablePaging -KeepODataContext:$KeepODataContext -AddODataType:$AddODataType -GroupOutputByRequest:$GroupOutputByRequest -Request $Request -GraphBaseUri $GraphBaseUri } } } finally { if (!$NoAppInsights) { $Stopwatch.Stop() Write-AppInsightsDependency ('{0} {1}' -f $Request.method.ToUpper(), $uriEndpoint.AbsolutePath) -Type 'MS Graph' -Data ('{0} {1}' -f $Request.method.ToUpper(), $uriEndpoint.AbsoluteUri) -Duration $Stopwatch.Elapsed -Success ($null -ne $results) } } } } function Complete-MsGraphResult { param ( # Results from MS Graph API. [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [object[]] $Results, # Only return first page of results. [Parameter(Mandatory = $false)] [switch] $DisablePaging, # Copy ODataContext to each result value. [Parameter(Mandatory = $false)] [switch] $KeepODataContext, # Add ODataType to each result value. [Parameter(Mandatory = $false)] [switch] $AddODataType, # Group results in array by request. [Parameter(Mandatory = $false)] [switch] $GroupOutputByRequest, # MS Graph request object. [Parameter(Mandatory = $false)] [psobject] $Request, # Base URL for Microsoft Graph API. [Parameter(Mandatory = $false)] [uri] $GraphBaseUri = 'https://graph.microsoft.com/' ) begin { [System.Collections.Generic.List[object]] $listOutput = New-Object 'System.Collections.Generic.List[object]' } process { foreach ($Result in $Results) { $Output = Expand-MsGraphResult $Result -RawOutput:$DisablePaging -KeepODataContext:$KeepODataContext -AddODataType:$AddODataType if ($GroupOutputByRequest -and $Output) { $listOutput.AddRange([array]$Output) } else { $Output } if (!$DisablePaging -and $Result) { if (Get-ObjectPropertyValue $Result '@odata.nextLink') { [uri] $uriEndpoint = [IO.Path]::Combine($GraphBaseUri.AbsoluteUri, $Request.url.TrimStart('/')) [int] $Total = Get-MsGraphResultsCount $uriEndpoint -GraphBaseUri $GraphBaseUri $Activity = ('Microsoft Graph Request - {0} {1}' -f $Request.method.ToUpper(), $uriEndpoint.AbsolutePath) $ProgressState = Start-Progress -Activity $Activity -Total $Total $ProgressState.CurrentIteration = $Result.value.Count $MaxRetries = 5 try { while (Get-ObjectPropertyValue $Result '@odata.nextLink') { Update-Progress $ProgressState -IncrementBy $Result.value.Count $nextLink = $Result.'@odata.nextLink' $MsGraphSession = Confirm-ModuleAuthentication -MsGraphSession -ErrorAction Stop $Result = $null [double]$RetryAfter = 1.0 for($Retries = 0; $Retries -lt $MaxRetries; $Retries++) { try { $Result = Invoke-RestMethod -WebSession $MsGraphSession -UseBasicParsing -Method Get -Uri $nextLink -Headers $Request.headers -ErrorAction Stop # break the loop if no error was raised break } catch { ## error while invoking graph if ($Retries -eq $MaxRetries-1) { # catch error if it was the last try Catch-MsGraphError $_ } # update retry after if throttling if ($_.Exception.PSobject.Properties.Name.Contains("Response") -and $_.Exception.Response.StatusCode.value__ -eq 429) { # Get the retry after header try { $RetryAfter = [double]($_.Exception.Response.Headers.GetValues('Retry-After')[0]) } catch { Write-Verbose "Request throttled but Retry-After not provided ($nextLink) using exponential backoff ($(RetryAfter)s)" } } # request has encountered and error and has not hit the maximum retires Write-Warning "GET $nextLink; error '$($_.Exception.Message); attempt $($Retries+1) out of $MaxRetries. Retrying after $($RetryAfter)s" Start-Sleep -Seconds $RetryAfter $RetryAfter = $RetryAfter * 2 } } if ($Result) { $Output = Expand-MsGraphResult $Result -RawOutput:$DisablePaging -KeepODataContext:$KeepODataContext -AddODataType:$AddODataType if ($GroupOutputByRequest -and $Output) { $listOutput.AddRange([array]$Output) } else { $Output } } } } finally { Stop-Progress $ProgressState } } } } } end { if ($GroupOutputByRequest) { Write-Output $listOutput.ToArray() -NoEnumerate } } } } process { ## Initialize if ($PSBoundParameters.ContainsKey('UniqueId') -and !$UniqueId) { return } if ($RelativeUri.OriginalString -eq $UniqueId) { $UniqueId = $null } # Pipeline string/uri input binds to both parameters so default to just uri ## Process Each RelativeUri foreach ($uri in $RelativeUri) { [string] $BaseUri = $uriGraphVersionBase.AbsoluteUri if ($uri.IsAbsoluteUri) { if ($uri.AbsoluteUri -match '^https://(.+?)/(v1.0|beta)?') { $BaseUri = $Matches[0] } if (!$listRequests.ContainsKey($BaseUri)) { $listRequests.Add($BaseUri, (New-Object 'System.Collections.Generic.List[pscustomobject]')) } $uriQueryEndpoint = New-Object System.UriBuilder -ArgumentList $uri } else { $uriQueryEndpoint = New-Object System.UriBuilder -ArgumentList ([IO.Path]::Combine($BaseUri, $uri)) } ## Combine query parameters from URI and cmdlet parameters [hashtable] $QueryParametersFinal = @{ } if ($uriQueryEndpoint.Query) { $QueryParametersFinal = ConvertFrom-QueryString $uriQueryEndpoint.Query -AsHashtable if ($QueryParameters) { foreach ($ParameterName in $QueryParameters.Keys) { $QueryParametersFinal[$ParameterName] = $QueryParameters[$ParameterName] } } } elseif ($QueryParameters) { $QueryParametersFinal = $QueryParameters } if ($Select) { $QueryParametersFinal['$select'] = $Select -join ',' } if ($Filter) { $QueryParametersFinal['$filter'] = $Filter } if ($Top) { $QueryParametersFinal['$top'] = $Top } if ($PSBoundParameters.ContainsKey('Count')) { $QueryParametersFinal['$count'] = ([string]$Count).ToLower() } $uriQueryEndpoint.Query = ConvertTo-QueryString $QueryParametersFinal ## Expand with UniqueIds if ($UniqueId) { foreach ($id in $UniqueId) { if ($id) { ## If the URI contains '{0}', then replace it with Unique Id. if ($uriQueryEndpoint.Uri.AbsoluteUri.Contains('%7B0%7D')) { $uriQueryEndpointUniqueId = New-Object System.UriBuilder -ArgumentList ([System.Net.WebUtility]::UrlDecode($uriQueryEndpoint.Uri.AbsoluteUri) -f $id) } else { $uriQueryEndpointUniqueId = New-Object System.UriBuilder -ArgumentList $uriQueryEndpoint.Uri $uriQueryEndpointUniqueId.Path = ([IO.Path]::Combine($uriQueryEndpointUniqueId.Path, $id)) } if ($DisableUniqueIdDeduplication -or $hashUri.Add($uriQueryEndpointUniqueId.Uri)) { if ($EnableInFilter -and $id -match '^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$') { $listIds.Add($id) while($listIds.Count -ge $InFilterBatchSize) { # go back to initial uri (without appending id) $uriQueryEndpointUniqueId = New-Object System.UriBuilder -ArgumentList $uriQueryEndpoint.Uri # get the query parameters $QueryParametersInIds = ConvertFrom-QueryString $uriQueryEndpoint.Query -AsHashtable # get the ids to query $filterids = $listIds[0..($InFilterBatchSize - 1)] # append them to "$filter" if ($QueryParametersInIds.ContainsKey('$filter')) { $QueryParametersInIds['$filter'] = "($($QueryParametersInIds['$filter'])) and id in ('$($filterids -join "','")')" } else { $QueryParametersInIds['$filter'] = "id in ('$($filterids -join "','")')" } # update query $uriQueryEndpointUniqueId.Query = ConvertTo-QueryString $QueryParametersInIds # add new batch request New-MsGraphRequest $uriQueryEndpointUniqueId.Uri -Headers @{ ConsistencyLevel = $ConsistencyLevel } | Add-MsGraphRequest -GraphBaseUri $BaseUri # remove ids from ids to request $listIds.RemoveRange(0, $InFilterBatchSize) # update progress if ($ProgressState) { $ProgressState.CurrentIteration += $InFilterBatchSize - 1 } } } elseif (!$DisableGetByIdsBatching -and $id -match '^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$' -and $uriQueryEndpoint.Uri.Segments.Count -eq 3 -and $uriQueryEndpoint.Uri.Segments[2] -in ('directoryObjects', 'users', 'groups', 'devices', 'servicePrincipals', 'applications') -and ($QueryParametersFinal.Count -eq 0 -or ($QueryParametersFinal.Count -eq 1 -and $QueryParametersFinal.ContainsKey('$select')))) { $listIds.Add($id) while ($listIds.Count -ge $GetByIdsBatchSize) { New-MsGraphGetByIdsRequest $listIds[0..($GetByIdsBatchSize - 1)] -Types $uriQueryEndpoint.Uri.Segments[2].TrimEnd('s') -Select $QueryParametersFinal['$select'] -BatchSize $GetByIdsBatchSize | Add-MsGraphRequest -GraphBaseUri $BaseUri $listIds.RemoveRange(0, $GetByIdsBatchSize) if ($ProgressState) { $ProgressState.CurrentIteration += $GetByIdsBatchSize - 1 } } } else { New-MsGraphRequest $uriQueryEndpointUniqueId.Uri -Headers @{ ConsistencyLevel = $ConsistencyLevel } | Add-MsGraphRequest -GraphBaseUri $BaseUri } } elseif ($ProgressState) { $ProgressState.Total -= 1 } } elseif ($ProgressState) { $ProgressState.Total -= 1 } } } else { New-MsGraphRequest $uriQueryEndpoint.Uri -Headers @{ ConsistencyLevel = $ConsistencyLevel } | Add-MsGraphRequest -GraphBaseUri $BaseUri } } } end { ## Complete Remaining Ids if ($listIds.Count -gt 0) { New-MsGraphGetByIdsRequest $listIds -Types $uriQueryEndpoint.Uri.Segments[2].TrimEnd('s') -Select $QueryParametersFinal['$select'] -BatchSize $GetByIdsBatchSize | Add-MsGraphRequest -GraphBaseUri $BaseUri if ($ProgressState) { $ProgressState.CurrentIteration += $listIds.Count - 1 } } ## Finish requests foreach ($BaseUri in $listRequests.Keys) { if ($listRequests[$BaseUri].Count -eq 1) { Invoke-MSGraphRequest $listRequests[$BaseUri][0] -GraphBaseUri $BaseUri } elseif ($listRequests[$BaseUri].Count -gt 0) { Invoke-MsGraphBatchRequest $listRequests[$BaseUri] -BatchSize $BatchSize -ProgressState $ProgressState -GraphBaseUri $BaseUri } if (!$DisableBatching -and $ProgressState -and $ProgressState.CurrentIteration -gt 1) { [uri] $uriEndpoint = [IO.Path]::Combine($BaseUri, '$batch') Write-AppInsightsDependency ('{0} {1}' -f 'POST', $uriEndpoint.AbsolutePath) -Type 'MS Graph' -Data ("{0} {1}`r`n`r`n{2}" -f 'POST', $uriEndpoint.AbsoluteUri, ('{{"requests":[...{0}...]}}' -f $ProgressState.CurrentIteration)) -Duration $ProgressState.Stopwatch.Elapsed -Success $? } } ## Clean-up if ($ProgressState) { Stop-Progress $ProgressState } } } <# .SYNOPSIS New request object containing Microsoft Graph API details. .EXAMPLE PS C:\>New-MsGraphRequest 'users' Return request object for GET /users. .EXAMPLE PS C:\>New-MsGraphRequest -Method Get -Uri 'https://graph.microsoft.com/v1.0/users' Return request object for GET /users. .EXAMPLE PS C:\>New-MsGraphRequest -Method Patch -Uri 'users/{id}' -Body ([PsCustomObject]{ displayName = "Joe Cool" } Return request object for PATCH /users/{id} with a body payload to update the displayName. #> function New-MsGraphRequest { [CmdletBinding()] param ( # Specifies the method used for the web request. [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [Alias('Id')] [int] $RequestId = 0, # Specifies the method used for the web request. [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [ValidateSet('Get', 'Head', 'Post', 'Put', 'Delete', 'Trace', 'Options', 'Merge', 'Patch')] [string] $Method = 'Get', # Specifies the Uniform Resource Identifier (URI) of the Internet resource to which the web request is sent. [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [uri[]] $Uri, # Specifies the headers of the web request. [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [hashtable] $Headers, # Specifies the body of the request. [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [pscustomobject] $Body ) process { if (!$Headers) { $Headers = @{} } for ($iRequest = 0; $iRequest -lt $Uri.Count; $iRequest++) { if ($Body) { if (!$Headers.ContainsKey('Content-Type')) { $Headers.Add('Content-Type', 'application/json') } } [string] $url = $Uri[$iRequest].PathAndQuery if (!$url) { $url = $Uri[$iRequest].ToString() } [pscustomobject]@{ id = $RequestId + $iRequest method = $Method.ToUpper() url = $url -replace '^(https://.+?/)?/?(v1.0/|beta/)?', '/' headers = $Headers body = $Body } } } } function New-MsGraphGetByIdsRequest { [CmdletBinding()] param ( # A collection of IDs for which to return objects. [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [guid[]] $Ids, # A collection of resource types that specifies the set of resource collections to search. [Parameter(Mandatory = $false)] [string[]] $Types, # Filters properties (columns). [Parameter(Mandatory = $false)] [string[]] $Select, # Specify Batch size. [Parameter(Mandatory = $false)] [int] $BatchSize = 1000 ) begin { $Types = $Types | Where-Object { $_ -ne 'directoryObject' } if (!$Select) { $Select = "*" } $listIds = New-Object 'System.Collections.Generic.List[guid]' } process { foreach ($Id in $Ids) { $listIds.Add($Id) ## Process IDs when a full batch is reached while ($listIds.Count -ge $BatchSize) { New-MsGraphRequest ('/directoryObjects/getByIds?$select={0}' -f ($Select -join ',')) -Method Post -Headers @{ 'Content-Type' = 'application/json' } -Body ([PSCustomObject]@{ ids = $listIds[0..($BatchSize - 1)] types = $Types }) $listIds.RemoveRange(0, $BatchSize) } } } end { ## Process any remaining IDs if ($listIds.Count -gt 0) { New-MsGraphRequest ('/directoryObjects/getByIds?$select={0}' -f ($Select -join ',')) -Method Post -Headers @{ 'Content-Type' = 'application/json' } -Body ([PSCustomObject]@{ ids = $listIds types = $Types }) } } } function New-MsGraphBatchRequest { [CmdletBinding()] param ( # A collection of request objects. [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [object[]] $Requests, # Specify Batch size. [Parameter(Mandatory = $false)] [int] $BatchSize = 20, # Specify depth of nested batches. MS Graph does not currently support batch nesting. [Parameter(Mandatory = $false)] [int] $Depth = 1 ) process { for ($iRequest = 0; $iRequest -lt $Requests.Count; $iRequest += [System.Math]::Pow($BatchSize, $Depth)) { $indexEnd = [System.Math]::Min($iRequest + [System.Math]::Pow($BatchSize, $Depth) - 1, $Requests.Count - 1) ## Reset ID Order for ($iId = $iRequest; $iId -le $indexEnd; $iId++) { $Requests[$iId].id = $iId } ## Generate Batch Request if ($Depth -gt 1) { $BatchRequest = New-MsGraphBatchRequest $Requests[$iRequest..$indexEnd] -Depth ($Depth - 1) } else { $BatchRequest = $Requests[$iRequest..$indexEnd] } New-MsGraphRequest -RequestId $iRequest -Method Post -Uri '/$batch' -Headers @{ 'Content-Type' = 'application/json' } -Body ([PSCustomObject]@{ requests = $BatchRequest }) } } } function Get-MsGraphMetadata { param ( # Metadata URL for Microsoft Graph API. [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true)] [uri] $Uri = 'https://graph.microsoft.com/v1.0/$metadata', # Force a refresh of metadata. [Parameter(Mandatory = $false)] [switch] $ForceRefresh ) if (!(Get-Variable MsGraphMetadataCache -Scope Script -ErrorAction SilentlyContinue)) { New-Variable -Name MsGraphMetadataCache -Scope Script -Value (New-Object 'System.Collections.Generic.Dictionary[string,xml]') } if (!$Uri.AbsolutePath.EndsWith('$metadata')) { $Uri = ([IO.Path]::Combine($Uri.AbsoluteUri, '$metadata')) } [string] $BaseUri = $Uri.AbsoluteUri if ($Uri.AbsoluteUri -match ('^.+{0}' -f ([regex]::Escape($Uri.AbsolutePath)))) { $BaseUri = $Matches[0] } if ($ForceRefresh -or !$script:MsGraphMetadataCache.ContainsKey($BaseUri)) { #$MsGraphSession = Confirm-ModuleAuthentication -MsGraphSession -ErrorAction Stop try { $script:MsGraphMetadataCache[$BaseUri] = Invoke-RestMethod -UseBasicParsing -Method Get -Uri $Uri -ErrorAction Ignore } catch {} } return $script:MsGraphMetadataCache[$BaseUri] } function Get-MsGraphEntityType { param ( # Metadata URL for Microsoft Graph API. [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true)] [uri] $Uri = 'https://graph.microsoft.com/v1.0/$metadata', # Name of endpoint. [Parameter(Mandatory = $false)] [string] $EntityName ) process { $MsGraphMetadata = Get-MSGraphMetadata $Uri if (!$EntityName -and $Uri.Fragment -match '^#(.+?)(\(.+\))?(/\$entity)?$') { $EntityName = $Matches[1] } foreach ($Schema in $MsGraphMetadata.Edmx.DataServices.Schema) { foreach ($EntitySet in $Schema.EntityContainer.EntitySet) { if ($EntitySet.Name -eq $EntityName) { return $EntitySet.EntityType } } } } } function Expand-MsGraphResult { param ( # Results from MS Graph API. [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [object[]] $Results, # Do not expand result values [Parameter(Mandatory = $false)] [switch] $RawOutput, # Copy ODataContext to each result value [Parameter(Mandatory = $false)] [switch] $KeepODataContext, # Add ODataType to each result value [Parameter(Mandatory = $false)] [switch] $AddODataType ) process { foreach ($Result in $Results) { if (!$RawOutput -and (Get-ObjectPropertyValue $Result.psobject.Properties 'Name') -contains 'value') { foreach ($ResultValue in $Result.value) { if ($AddODataType) { $ODataType = Get-ObjectPropertyValue $Result '@odata.context' | Get-MsGraphEntityType if ($ODataType) { $ODataType = '#' + $ODataType } if ($ResultValue -is [hashtable] -and !$ResultValue.ContainsKey('@odata.type')) { $ResultValue.Add('@odata.type', $ODataType) } elseif ($ResultValue.psobject.Properties.Name -notcontains '@odata.type') { $ResultValue | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value $ODataType } } if ($KeepODataContext) { if ($ResultValue -is [hashtable]) { $ResultValue.Add('@odata.context', ('{0}/$entity' -f $Result.'@odata.context')) } else { $ResultValue | Add-Member -MemberType NoteProperty -Name '@odata.context' -Value ('{0}/$entity' -f $Result.'@odata.context') } } Write-Output $ResultValue } } else { Write-Output $Result } } } } function Get-MsGraphResultsCount { [CmdletBinding()] param ( # Graph endpoint such as "users". [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [uri] $Uri, # Base URL for Microsoft Graph API. [Parameter(Mandatory = $false)] [uri] $GraphBaseUri = 'https://graph.microsoft.com/' ) process { if ($Uri.IsAbsoluteUri) { $uriEndpointCount = New-Object System.UriBuilder -ArgumentList $Uri -ErrorAction Stop } else { $uriEndpointCount = New-Object System.UriBuilder -ArgumentList $GraphBaseUri -ErrorAction Stop } ## Remove $ref from path $uriEndpointCount.Path = $uriEndpointCount.Path -replace '/\$ref$', '' ## Add $count segment to path $uriEndpointCount.Path = ([IO.Path]::Combine($uriEndpointCount.Path, '$count')) ## $count is not supported with $expand parameter so remove it. [hashtable] $QueryParametersUpdated = ConvertFrom-QueryString $uriEndpointCount.Query -AsHashtable if ($QueryParametersUpdated.ContainsKey('$expand')) { $QueryParametersUpdated.Remove('$expand') } $uriEndpointCount.Query = ConvertTo-QueryString $QueryParametersUpdated $MsGraphSession = Confirm-ModuleAuthentication -MsGraphSession -ErrorAction Stop [int] $Count = $null try { $Count = Invoke-RestMethod -WebSession $MsGraphSession -UseBasicParsing -Method Get -Uri $uriEndpointCount.Uri -Headers @{ ConsistencyLevel = 'eventual' } -ErrorAction Ignore } catch {} return $Count } } # SIG # Begin signature block # MIInuwYJKoZIhvcNAQcCoIInrDCCJ6gCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCVyQ2Dabavt36b # WvieSObiQq63mBqxyLhMr3ZTboUBT6CCDYUwggYDMIID66ADAgECAhMzAAACU+OD # 3pbexW7MAAAAAAJTMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjEwOTAyMTgzMzAwWhcNMjIwOTAxMTgzMzAwWjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDLhxHwq3OhH+4J+SX4qS/VQG8HybccH7tnG+BUqrXubfGuDFYPZ29uCuHfQlO1 # lygLgMpJ4Geh6/6poQ5VkDKfVssn6aA1PCzIh8iOPMQ9Mju3sLF9Sn+Pzuaie4BN # rp0MuZLDEXgVYx2WNjmzqcxC7dY9SC3znOh5qUy2vnmWygC7b9kj0d3JrGtjc5q5 # 0WfV3WLXAQHkeRROsJFBZfXFGoSvRljFFUAjU/zdhP92P+1JiRRRikVy/sqIhMDY # +7tVdzlE2fwnKOv9LShgKeyEevgMl0B1Fq7E2YeBZKF6KlhmYi9CE1350cnTUoU4 # YpQSnZo0YAnaenREDLfFGKTdAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUlZpLWIccXoxessA/DRbe26glhEMw # VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh # dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzQ2NzU5ODAfBgNVHSMEGDAW # gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v # d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw # MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov # L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx # XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB # AKVY+yKcJVVxf9W2vNkL5ufjOpqcvVOOOdVyjy1dmsO4O8khWhqrecdVZp09adOZ # 8kcMtQ0U+oKx484Jg11cc4Ck0FyOBnp+YIFbOxYCqzaqMcaRAgy48n1tbz/EFYiF # zJmMiGnlgWFCStONPvQOBD2y/Ej3qBRnGy9EZS1EDlRN/8l5Rs3HX2lZhd9WuukR # bUk83U99TPJyo12cU0Mb3n1HJv/JZpwSyqb3O0o4HExVJSkwN1m42fSVIVtXVVSa # YZiVpv32GoD/dyAS/gyplfR6FI3RnCOomzlycSqoz0zBCPFiCMhVhQ6qn+J0GhgR # BJvGKizw+5lTfnBFoqKZJDROz+uGDl9tw6JvnVqAZKGrWv/CsYaegaPePFrAVSxA # yUwOFTkAqtNC8uAee+rv2V5xLw8FfpKJ5yKiMKnCKrIaFQDr5AZ7f2ejGGDf+8Tz # OiK1AgBvOW3iTEEa/at8Z4+s1CmnEAkAi0cLjB72CJedU1LAswdOCWM2MDIZVo9j # 0T74OkJLTjPd3WNEyw0rBXTyhlbYQsYt7ElT2l2TTlF5EmpVixGtj4ChNjWoKr9y # TAqtadd2Ym5FNB792GzwNwa631BPCgBJmcRpFKXt0VEQq7UXVNYBiBRd+x4yvjqq # 5aF7XC5nXCgjbCk7IXwmOphNuNDNiRq83Ejjnc7mxrJGMIIHejCCBWKgAwIBAgIK # YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm # aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw # OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD # VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG # 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la # UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc # 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D # dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+ # lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk # kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6 # A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd # X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL # 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd # sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3 # T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS # 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI # bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL # BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD # uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv # c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf # MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf # MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF # BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h # cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA # YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn # 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7 # v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b # pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/ # KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy # CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp # mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi # hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb # BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS # oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL # gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX # cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGYwwghmIAgEBMIGVMH4x # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p # Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAAJT44Pelt7FbswAAAAA # AlMwDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw # HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIIgk # NkjfUUwAio6o8/Cziuom+Ag7dUxfQJYrcbFLDF/rMEIGCisGAQQBgjcCAQwxNDAy # oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20wDQYJKoZIhvcNAQEBBQAEggEAGF0ZDbd9KesmaqioesU5tzPn02CTYc++XkZW # TP+WpbkML18upUPMZ/iXKzU7dZaKh4WbbwtBERHm7ogpwlBxjOXyprrGS4R+VM32 # M98MMmrNuWfYqei85QLtvJGivSD6ERc/ETTFEy6yjsGCFI581SGORoYdcDKFT2fd # kSgs4nYJ0G0znPd18Odlvreu8PLozGhlCpgEPLUxulcnlD5/TmIzK+CkpGkNzUGZ # R8eSOHvWOhs50laUvN8mzliMbaVfUp+2XZNxHICKCkN4lwBxyfdbzApeDBOO5IYx # XoR3qK2sUMxO7qi6w9mKqX0MYfxbZV1FtDsjZyQ0odtxZqx2FqGCFxYwghcSBgor # BgEEAYI3AwMBMYIXAjCCFv4GCSqGSIb3DQEHAqCCFu8wghbrAgEDMQ8wDQYJYIZI # AWUDBAIBBQAwggFZBgsqhkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGE # WQoDATAxMA0GCWCGSAFlAwQCAQUABCAlTJ/9vS9O+skYfzmfRbFlOjunRvXWfiCG # FGr5GPTJKwIGYgivzlZcGBMyMDIyMDQyMTEyMDM0Ny4zMThaMASAAgH0oIHYpIHV # MIHSMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQL # EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsT # HVRoYWxlcyBUU1MgRVNOOjg2REYtNEJCQy05MzM1MSUwIwYDVQQDExxNaWNyb3Nv # ZnQgVGltZS1TdGFtcCBTZXJ2aWNloIIRZTCCBxQwggT8oAMCAQICEzMAAAGMAZdi # RzZ2ZjsAAQAAAYwwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAg # UENBIDIwMTAwHhcNMjExMDI4MTkyNzQ0WhcNMjMwMTI2MTkyNzQ0WjCB0jELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9z # b2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMg # VFNTIEVTTjo4NkRGLTRCQkMtOTMzNTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt # U3RhbXAgU2VydmljZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANNI # aEyhE/khrGssPvQRXZvrmfpLxDxi3ebBfF5U91MuGBqk/Ovg6/Bt5Oqv5UWoIsUS # r5/oNgBUS/Vbmagtbk72u3WTfQoYqLRxxZsskGT2pV3SUwvJyiK24EzFwMIf5m4Z # 5qGsbCPYxpYr2IIuRjThO7uk1eFDrZ1T/IqIU1HzTCoWWiXc5lg44Vguy4z1yIWp # vUIUZFc65MXySnOfQLGhg9z74kZIB6BsX6XVhzz2lvIohB43ODw5gipbltyfiHVN # /B/jJCj5npAuxrUUy1ygQrlil0vE42WP8JDXM1jRKPpeSdzmXR3lYoMacwp3rJGX # 3B18awl9obnu6ib1q5LBUrZGWzhuyGJmn2DEK2RrpZe9j50taCHUHWJ0ef54HL0k # G9dRkNJDTA84irEnfuYn1GmGyS2dFxMTVeKi1wkuuQ4/vBcoAo7Tb5A4geR7PSOy # vc8WbFG+3yikhhGfcgNCYE1m3ADwmD7bgB1SfFCmk/eu6SZu/q94YHHt/FVN/bKX # nhx4GgkuL163pUy4lDAJdDrZOZ3CkCnNpBp77sD9kQkt5BBBQMaJ8C5/Kcnncq3m # U2wTEAan9aN5I9IpTie/3/z93Na52mDtNRgyaJr+6LaW+c/tYa0qCLPLvunq7iSg # k4oXdIv/G3OuwChe+sKVrr1vQYW1DE7FpMMOK+NnAgMBAAGjggE2MIIBMjAdBgNV # HQ4EFgQUls5ThqmCIWCIeVadPojK3UCLUiMwHwYDVR0jBBgwFoAUn6cVXQBeYl2D # 9OXSZacbUzUZ6XIwXwYDVR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3Nv # ZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUy # MDIwMTAoMSkuY3JsMGwGCCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDov # L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1l # LVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUE # DDAKBggrBgEFBQcDCDANBgkqhkiG9w0BAQsFAAOCAgEA12jFRVjCCanW5UGSuqJL # O3HQlMHjwJphCHnbMrrIFDCEJUKmo3wj/YhufMjhUkcdpOfY9oQAUmQcRZm5FY8I # WBAtciT0JveOuIFM+RvrjludYvLnnngd4dovg5qFjSjUrpSDcn0hoFujwgwokajt # 6p/CmFcy86Hpnz4q/1FceQgIFXBAwDLcW0a0x1wQAV8gmumkN/o7pFgeWkMy8Oqo # R4c+xyDlPav0PWNjZ1QSj38yJcD429ja0Bn0J107LHxQ/fDqUR6tO2VMdtYOKbPF # d94UkpCdrg8IbaeVbRRpxfgMcxQZQr3N9yz05l7HM5cuvskIAEcJjR3jQNutlqiy # yTPOCM/DktVXxNTesmApC44PNfsxl7I7zBpowZYssWcF1hliZrKLwek+odRq35rz # CrnThPdg+u0kd809w3QOScC/UwM1/FIYtGhmLZ+bjVAxW8SKMyETKS1aT/2Di54P # q9r/LPJclr9Gn48GWBwSeuDFlTcR3GjbY85GLUI3WeW4cpGunV/g7UA/W4d844tE # pa31QyC8RG+jo8qrXxo+4lmbya2+AKiFYB0Gg84LosREvYnrRYpB33+qfewuaqG0 # 02ysDdABD96ubXsiPTSDlZSZdIIuSG3efB4n9ySzur6fuch146Ei/zJYRZrxrWmJ # kMA+ys05vbgAxeAcz/5sdr8wggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAA # AAAVMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz # aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv # cnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBB # dXRob3JpdHkgMjAxMDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwx # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1p # Y3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOC # Ag8AMIICCgKCAgEA5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YB # f2xK4OK9uT4XYDP/XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKD # RLemjkZrBxTzxXb1hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus # 9ja+NSZk2pg7uhp7M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTj # kY+yOSxRnOlwaQ3KNi1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56 # KTesy+uDRedGbsoy1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39 # IM9zfUGaRnXNxF803RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHo # vwUDo9Fzpk03dJQcNIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJo # LhDqhFFG4tG9ahhaYQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMh # XV8wdJGUlNi5UPkLiWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREd # cu+N+VLEhReTwDwV2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEA # AaOCAd0wggHZMBIGCSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqn # Uv5kxJq+gpE8RjUpzxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnp # cjBcBgNVHSAEVTBTMFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRw # Oi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0w # EwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEw # CwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/o # olxiaNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNy # b3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYt # MjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5t # aWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5j # cnQwDQYJKoZIhvcNAQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+ # TkdkeLEGk5c9MTO1OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2Y # urYeeNg2LpypglYAA7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4 # U3UkV7ndn/OOPcbzaN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJ # w7wXsFSFQrP8DJ6LGYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb # 30mjdAy87JGA0j3mSj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ # /gpY3UA8x1RtnWN0SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGO # WhmRaw2fpCjcZxkoJLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFE # fnyhYWxz/gq77EFmPWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJ # jXD+57XQKBqJC4822rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rR # nj7tfqAxM328y+l7vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUz # WLOhcGbyoYIC1DCCAj0CAQEwggEAoYHYpIHVMIHSMQswCQYDVQQGEwJVUzETMBEG # A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj # cm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBP # cGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOjg2REYt # NEJCQy05MzM1MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNl # oiMKAQEwBwYFKw4DAhoDFQA0ovIU66v0PKKacHhsrmSzRCav1aCBgzCBgKR+MHwx # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1p # Y3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqGSIb3DQEBBQUAAgUA5gt/ # DjAiGA8yMDIyMDQyMTE0NTgyMloYDzIwMjIwNDIyMTQ1ODIyWjB0MDoGCisGAQQB # hFkKBAExLDAqMAoCBQDmC38OAgEAMAcCAQACAgn9MAcCAQACAhFpMAoCBQDmDNCO # AgEAMDYGCisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSCh # CjAIAgEAAgMBhqAwDQYJKoZIhvcNAQEFBQADgYEAks/ZgMX+jGfC1EWyNdx2nvj0 # hW8iwNY2zO5y7sLaW4jyqxuWxF42PsUWszU+5OrGI3jAHq8n8ft3f2Nyi2FfOdtG # UtEotOBOpUXkk9WmyNrULUF7ftdB0vHn9UwSim+FAI+5WuhYJgLW1+uo9DUTyh2z # sBxiCu4p+g3qr9iRnAsxggQNMIIECQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEG # A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj # cm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFt # cCBQQ0EgMjAxMAITMwAAAYwBl2JHNnZmOwABAAABjDANBglghkgBZQMEAgEFAKCC # AUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCCI # obnCQFtgRn3CT2omdcI6iDgRrJ/ijxeO5YhDhcL1CjCB+gYLKoZIhvcNAQkQAi8x # geowgecwgeQwgb0EINWti/gVKpDPBn/E5iEFnYHik062FyMDqHzriYgYmGmeMIGY # MIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV # BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQG # A1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAGMAZdiRzZ2 # ZjsAAQAAAYwwIgQgpdF2N4mEhZ6yisryWdFie0WYGW0rlwyPw2hqo1TwPeAwDQYJ # KoZIhvcNAQELBQAEggIAbdOtrazbTDZTSlzeUOR2AxhbQFkG/hnIWnQ0IRKTzdxB # xXnFs7aJFswAsGghuBM8w0EtiyK3mzJIOV6upVV/RgOQpEjqtOZoXNWdNrJ13uNA # TudCSqi95psyHyPXoReWQTaeYtY47H76QOEdtBuD7dPokHvARy80GCaGvqEiaodT # tmJK2dLeYCJQopzytl9/iwo1UXB+wEp2NonSbLNPF6H45V9MyNv7l0zhdTDIs/bU # HHSAcfzVE2iPvuiCeunpr4Jgdkjv/D0Kj7n1KNt2fkHSjhDRZGCVozTs+rslKS6C # jz0ujV2C1s1pl1ngOnj+NPBvSN9Llia1ynVdFNtiMOGnhKjb2gJ/xJrsBkG4HrM0 # UUqKRppi2gBGimeGezAMPqGPigcax+v0f0ei5q51D0lvMUzvioSTx6nv/jaoFMMD # 574kifc89Dpf+cQiHPt2y23XUaV8CyOC3P2ZNpB397sPHC2mOI8ccLVCEzaGKcEi # 7zKO4rhBESztLGgoeI3iALdG70Mo4/KzXWWHeIGdPRC1BHrNL5I9MGxu4ZFjC7KI # JjO5TV77fwNxoj9X9Ue78ibSBlM165A5OhmapT8UecRuPUSJr6pdTO/pHUYHKPtQ # nSV0YkDVC6i8WOBqTDHHmDs7CHvjt6gHcMkjP7lSjQOZQchQ80nubmzinQaGvjI= # SIG # End signature block |