internal/Get-MsGraphResults.ps1
<# .SYNOPSIS Query Microsoft Graph API .PARAMETER EnableInFilter 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 This filter is currently experimental and will be subject to future change (move to DisableInFilterBatching). .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", # Correlation Id (client-request-id). [Parameter(Mandatory = $false)] [guid] $CorrelationId = (New-Guid), # 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 { if ($EnableInFilter) { Write-Verbose "EnableInFilter switch used: this filter is currently experimental and will be subject to future change (move to DisableInFilterBatching)" } [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 } $ResponseDetail = Get-MsGraphResponseDetail $_ Write-Debug -Message (ConvertTo-Json ([PSCustomObject]$ResponseDetail) -Depth 3) if ($ResponseDetail['ContentParsed']) { ## Terminating errors with specific codes if ($ResponseDetail['ContentParsed'].error.code -in ('Authentication_ExpiredToken', 'Service_ServiceUnavailable', 'Request_UnsupportedQuery')) { #Write-AppInsightsException -ErrorRecord $_ -OrderedProperties $ResponseDetail # Not needed when calling function has try finally to write terminating errors Write-Error -Exception $_.Exception -Message $ResponseDetail['ContentParsed'].error.message -ErrorId $ResponseDetail['ContentParsed'].error.code -Category $_.CategoryInfo.Category -CategoryActivity $_.CategoryInfo.Activity -CategoryReason $_.CategoryInfo.Reason -CategoryTargetName $_.CategoryInfo.TargetName -CategoryTargetType $_.CategoryInfo.TargetType -TargetObject $_.TargetObject -ErrorAction Stop } else { ## Ignore errors with specific codes else display non-terminating error if ($ResponseDetail['ContentParsed'].error.code -eq 'Request_ResourceNotFound') { #Write-Error -Exception $_.Exception -Message $ResponseDetail['ContentParsed'].error.message -ErrorId $ResponseDetail['ContentParsed'].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 $ResponseDetail['ContentParsed'].error.message } else { Write-Error -Exception $_.Exception -Message $ResponseDetail['ContentParsed'].error.message -ErrorId $ResponseDetail['ContentParsed'].error.code -Category $_.CategoryInfo.Category -CategoryActivity $_.CategoryInfo.Activity -CategoryReason $_.CategoryInfo.Reason -CategoryTargetName $_.CategoryInfo.TargetName -CategoryTargetType $_.CategoryInfo.TargetType -TargetObject $_.TargetObject -ErrorVariable cmdError } if ($ResponseDetail.Contains('ContentParsed')) { $ResponseDetail.Remove('ContentParsed') } Write-AppInsightsException -ErrorRecord $cmdError -OrderedProperties $ResponseDetail } } else { throw $ErrorRecord } } function Test-MsGraphBatchError ($BatchResponse, $BatchRequest) { if ($BatchResponse.status -ne '200') { Write-Debug -Message (ConvertTo-Json $BatchResponse -Depth 3) ## Terminating errors with specific codes if ($BatchResponse.body.error.code -in ('Authentication_ExpiredToken','Service_ServiceUnavailable','Request_UnsupportedQuery')) { Write-Error -Message $BatchResponse.body.error.message -ErrorId $BatchResponse.body.error.code -ErrorAction Stop } else { ## Ignore errors with specific codes else display non-terminating error 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 } # generate extra properties for the exception $ResponseDetail = Get-MsGraphResponseDetail $BatchResponse if ($BatchRequest) { $ResponseDetail["Request"] = "{0} {1}" -f $BatchRequest.method, $BatchRequest.url } if ($ResponseDetail.Contains('ContentParsed')) { $ResponseDetail.Remove('ContentParsed') } Write-AppInsightsException -ErrorRecord $cmdError -OrderedProperties $ResponseDetail } 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 } foreach ($results in ($resultsBatch)) { # check if batch result failed and call the endpoint or throw if ($results.status -eq "429") { [int] $RetryAfter = Get-ObjectPropertyValue $results headers 'Retry-After' [double] $SecondsRemaining = $RetryAfter $Date = Get-ObjectPropertyValue $results body error innerError 'date' if ($Date) { if ($PSVersionTable.PSVersion -ge [version]'7.1') { $CurrentTime = Get-Date -AsUTC } else { $CurrentTime = [datetime]::UtcNow } $SecondsRemaining = $(([datetime]$Date).AddSeconds($RetryAfter) - $CurrentTime).TotalSeconds } if ($SecondsRemaining -gt 0) { Write-Warning ("Request from batch was throttled and will attempt retry after {0:0}s." -f $SecondsRemaining) Start-Sleep -Seconds $SecondsRemaining } else { Write-Warning "Request from batch was throttled and will attempt retry." } Invoke-MsGraphRequest $request -NoAppInsights -GraphBaseUri $GraphBaseUri continue } $currentRequest = $BatchRequests[$iRequest] | Where-Object {$_.id -eq $results.id} if (!(Test-MsGraphBatchError $results $currentRequest)) { 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 (!$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, # Default Retry-After value [Parameter(Mandatory = $false)] [int] $RetryAfter = 2 ) 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 -le $MaxRetries; $Retries++) { try { $results = Invoke-RestMethod -WebSession $MsGraphSession -UseBasicParsing @paramInvokeRestMethod -ErrorAction Stop break # break the loop if no error was raised } catch { ## Retry request if response returns error or indicates throttling # Windows PowerShell WebException Example: $_.Exception.Status -eq 'Timeout' # Response is also null because there was no response. # Example: $_.Exception.Response.StatusCode.value__ -eq 429 # Throttling # Example: $_.Exception.Response.StatusCode.value__ -eq 503 # ServiceUnavailable #if ($Retries -lt $MaxRetries -and ((Get-ObjectPropertyValue $_ Exception Response StatusCode value__) -eq 429 -or (Get-ObjectPropertyValue $_ Exception Response StatusCode value__) -eq 503 -or !(Get-ObjectPropertyValue $_ Exception Response))) { if ($Retries -lt $MaxRetries -and (Get-ObjectPropertyValue $_ Exception Response StatusCode value__) -notin 400,403) { $ResponseDetail = Get-MsGraphResponseDetail $_ if ($ResponseDetail.Contains('ContentParsed')) { $ResponseDetail.Remove('ContentParsed') } Write-AppInsightsException -ErrorRecord $_ -OrderedProperties $ResponseDetail # Get the retry after header try { $RetryAfter = $_.Exception.Response.Headers.GetValues('Retry-After')[0] } catch { if ($Retries -gt 0) { $RetryAfter *= 2 } } if ((Get-ObjectPropertyValue $_ Exception Response StatusCode value__) -eq 429) { Write-Warning "Request was throttled and will attempt retry $($Retries+1) of $MaxRetries after $($RetryAfter)s." } else { Write-Warning "Request returned error and will attempt retry $($Retries+1) of $MaxRetries after $($RetryAfter)s." } Start-Sleep -Seconds $RetryAfter } else { # catch error if it was the last try Catch-MsGraphError $_ break # break the loop if error was not due to throttling } } } 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 -MaxRetries $MaxRetries -RetryAfter $RetryAfter } } else { Complete-MsGraphResult $results -DisablePaging:$DisablePaging -KeepODataContext:$KeepODataContext -AddODataType:$AddODataType -GroupOutputByRequest:$GroupOutputByRequest -Request $Request -GraphBaseUri $GraphBaseUri -MaxRetries $MaxRetries -RetryAfter $RetryAfter } } } 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/', # Number of retries in case of throttling [Parameter(Mandatory = $false)] [int] $MaxRetries = 5, # Default Retry-After value [Parameter(Mandatory = $false)] [int] $RetryAfter = 2 ) 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 = ('{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 for ($Retries = 0; $Retries -le $MaxRetries; $Retries++) { try { $Result = Invoke-RestMethod -WebSession $MsGraphSession -UseBasicParsing -Method Get -Uri $nextLink -Headers $Request.headers -ErrorAction Stop break # break the loop if no error was raised } catch { ## Retry request if response returns error or indicates throttling #if ($Retries -lt $MaxRetries -and ((Get-ObjectPropertyValue $_ Exception Response StatusCode value__) -eq 429 -or (Get-ObjectPropertyValue $_ Exception Response StatusCode value__) -eq 503 -or !(Get-ObjectPropertyValue $_ Exception Response))) { if ($Retries -lt $MaxRetries -and (Get-ObjectPropertyValue $_ Exception Response StatusCode value__) -notin 400, 403) { $ResponseDetail = Get-MsGraphResponseDetail $_ if ($ResponseDetail.Contains('ContentParsed')) { $ResponseDetail.Remove('ContentParsed') } Write-AppInsightsException -ErrorRecord $_ -OrderedProperties $ResponseDetail # Get the retry after header try { $RetryAfter = $_.Exception.Response.Headers.GetValues('Retry-After')[0] } catch { if ($Retries -gt 0) { $RetryAfter *= 2 } } if ((Get-ObjectPropertyValue $_ Exception Response StatusCode value__) -eq 429) { Write-Warning "Request was throttled and will attempt retry $($Retries+1) of $MaxRetries after $($RetryAfter)s." } else { Write-Warning "Request returned error and will attempt retry $($Retries+1) of $MaxRetries after $($RetryAfter)s." } Start-Sleep -Seconds $RetryAfter } else { # catch error if it was the last try Catch-MsGraphError $_ break # break the loop if error was not due to throttling } } } 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 @{ 'client-request-id' = $CorrelationId; 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 @{ 'client-request-id' = $CorrelationId; ConsistencyLevel = $ConsistencyLevel } | Add-MsGraphRequest -GraphBaseUri $BaseUri } } elseif ($ProgressState) { $ProgressState.Total -= 1 } } elseif ($ProgressState) { $ProgressState.Total -= 1 } } } else { New-MsGraphRequest $uriQueryEndpoint.Uri -Headers @{ 'client-request-id' = $CorrelationId; 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, # API Version. [Parameter(Mandatory = $false)] [ValidateSet('v1.0', 'beta')] [string] $ApiVersion = 'v1.0', # Base URL for Microsoft Graph API. [Parameter(Mandatory = $false)] [uri] $GraphBaseUri = $script:mapMgEnvironmentToMgEndpoint[$script:ConnectState.CloudEnvironment] ) process { if ($Uri.IsAbsoluteUri) { $uriEndpointCount = New-Object System.UriBuilder -ArgumentList $Uri -ErrorAction Stop } else { $uriEndpointCount = New-Object System.UriBuilder -ArgumentList $GraphBaseUri -ErrorAction Stop $uriEndpointCount.Path = ([IO.Path]::Combine($uriEndpointCount.Path, $ApiVersion, $Uri)) } ## 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 } } function Get-MsGraphResponseDetail { [CmdletBinding()] param ( # ErrorRecord from exception or batch response object [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [object] $InputObject ) process { $ResponseDetail = [ordered]@{} if ($InputObject -is [System.Management.Automation.ErrorRecord]) { if ($InputObject.Exception.psobject.Properties.Name.Contains('Response')) { ## Get Response Body if ($InputObject.ErrorDetails) { $ResponseDetail['Response'] = '{0} {1} HTTP/{2}' -f $InputObject.Exception.Response.StatusCode.value__, $InputObject.Exception.Response.ReasonPhrase, $InputObject.Exception.Response.Version $ResponseDetail['Content-Type'] = $InputObject.Exception.Response.Content.Headers.ContentType.ToString() $ResponseDetail['Content'] = $InputObject.ErrorDetails.Message } elseif ($InputObject.Exception -is [System.Net.WebException]) { if ($InputObject.Exception.Response) { $ResponseDetail['Response'] = '{0} {1} HTTP/{2}' -f $InputObject.Exception.Response.StatusCode.value__, $InputObject.Exception.Response.StatusDescription, $InputObject.Exception.Response.ProtocolVersion $ResponseDetail['Content-Type'] = $InputObject.Exception.Response.Headers.GetValues('Content-Type') -join '; ' $StreamReader = New-Object System.IO.StreamReader -ArgumentList $InputObject.Exception.Response.GetResponseStream() try { $ResponseDetail['Content'] = $StreamReader.ReadToEnd() } finally { $StreamReader.Close() } } } $ResponseDetail['ContentParsed'] = $null if ($ResponseDetail['Content-Type'] -eq 'application/json') { $ResponseDetail['ContentParsed'] = ConvertFrom-Json $ResponseDetail['Content'] } $ResponseDetail['error-message'] = Get-ObjectPropertyValue $ResponseDetail['ContentParsed'] error message $ResponseDetail['Request'] = '{0} {1}' -f $InputObject.TargetObject.Method, $InputObject.TargetObject.RequestUri.AbsoluteUri if ($InputObject.Exception.Response) { $ResponseDetail['Date'] = $InputObject.Exception.Response.Headers.GetValues('Date')[0] $ResponseDetail['request-id'] = $InputObject.Exception.Response.Headers.GetValues('request-id')[0] $ResponseDetail['client-request-id'] = $InputObject.Exception.Response.Headers.GetValues('client-request-id')[0] try { if ($InputObject.Exception.Response.Headers.GetValues('Retry-After')[0]) { $ResponseDetail['Retry-After'] = $InputObject.Exception.Response.Headers.GetValues('Retry-After')[0] } } catch {} } } } else { $ResponseDetail['Response'] = '{0} {1}' -f $InputObject.status, (Get-ObjectPropertyValue $InputObject body error code) $ResponseDetail['Content-Type'] = Get-ObjectPropertyValue $InputObject headers 'Content-Type' $ResponseDetail['ContentParsed'] = Get-ObjectPropertyValue $InputObject body $ResponseDetail['Content'] = ConvertTo-Json $ResponseDetail['ContentParsed'] -Depth 5 -Compress $ResponseDetail['error-message'] = Get-ObjectPropertyValue $InputObject body error message $ResponseDetail['Date'] = Get-ObjectPropertyValue $InputObject body error innerError 'date' $ResponseDetail['request-id'] = Get-ObjectPropertyValue $InputObject body error innerError 'request-id' $ResponseDetail['client-request-id'] = Get-ObjectPropertyValue $InputObject body error innerError 'client-request-id' if (Get-ObjectPropertyValue $InputObject headers 'Retry-After') { $ResponseDetail['Retry-After'] = Get-ObjectPropertyValue $InputObject headers 'Retry-After' } } Write-Output $ResponseDetail } } # SIG # Begin signature block # MIInugYJKoZIhvcNAQcCoIInqzCCJ6cCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB9T1km0muffvC6 # QKRo/OrVY08c+XD3BKjYgEQbHfFbuKCCDYUwggYDMIID66ADAgECAhMzAAACzfNk # v/jUTF1RAAAAAALNMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjIwNTEyMjA0NjAyWhcNMjMwNTExMjA0NjAyWjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDrIzsY62MmKrzergm7Ucnu+DuSHdgzRZVCIGi9CalFrhwtiK+3FIDzlOYbs/zz # HwuLC3hir55wVgHoaC4liQwQ60wVyR17EZPa4BQ28C5ARlxqftdp3H8RrXWbVyvQ # aUnBQVZM73XDyGV1oUPZGHGWtgdqtBUd60VjnFPICSf8pnFiit6hvSxH5IVWI0iO # nfqdXYoPWUtVUMmVqW1yBX0NtbQlSHIU6hlPvo9/uqKvkjFUFA2LbC9AWQbJmH+1 # uM0l4nDSKfCqccvdI5l3zjEk9yUSUmh1IQhDFn+5SL2JmnCF0jZEZ4f5HE7ykDP+ # oiA3Q+fhKCseg+0aEHi+DRPZAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU0WymH4CP7s1+yQktEwbcLQuR9Zww # VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh # dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzQ3MDUzMDAfBgNVHSMEGDAW # gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v # d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw # MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov # L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx # XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB # AE7LSuuNObCBWYuttxJAgilXJ92GpyV/fTiyXHZ/9LbzXs/MfKnPwRydlmA2ak0r # GWLDFh89zAWHFI8t9JLwpd/VRoVE3+WyzTIskdbBnHbf1yjo/+0tpHlnroFJdcDS # MIsH+T7z3ClY+6WnjSTetpg1Y/pLOLXZpZjYeXQiFwo9G5lzUcSd8YVQNPQAGICl # 2JRSaCNlzAdIFCF5PNKoXbJtEqDcPZ8oDrM9KdO7TqUE5VqeBe6DggY1sZYnQD+/ # LWlz5D0wCriNgGQ/TWWexMwwnEqlIwfkIcNFxo0QND/6Ya9DTAUykk2SKGSPt0kL # tHxNEn2GJvcNtfohVY/b0tuyF05eXE3cdtYZbeGoU1xQixPZAlTdtLmeFNly82uB # VbybAZ4Ut18F//UrugVQ9UUdK1uYmc+2SdRQQCccKwXGOuYgZ1ULW2u5PyfWxzo4 # BR++53OB/tZXQpz4OkgBZeqs9YaYLFfKRlQHVtmQghFHzB5v/WFonxDVlvPxy2go # a0u9Z+ZlIpvooZRvm6OtXxdAjMBcWBAsnBRr/Oj5s356EDdf2l/sLwLFYE61t+ME # iNYdy0pXL6gN3DxTVf2qjJxXFkFfjjTisndudHsguEMk8mEtnvwo9fOSKT6oRHhM # 9sZ4HTg/TTMjUljmN3mBYWAWI5ExdC1inuog0xrKmOWVMIIHejCCBWKgAwIBAgIK # 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/Xmfwb1tbWrJUnMTDXpQzTGCGYswghmHAgEBMIGVMH4x # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p # Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAALN82S/+NRMXVEAAAAA # As0wDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw # HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEINST # cfjYPGAMZi9c1/2Lp6Ut4ySK4bY65hAfhQndio6MMEIGCisGAQQBgjcCAQwxNDAy # oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20wDQYJKoZIhvcNAQEBBQAEggEAjgRJWrGRdF5Y3omLwgQ2EpIhWmUSqG17aBIA # i8RudaNJZ1ZDVnVe1DoDTjNyWlv8/s5zl9BRn/TvNuHeElIixHrkEWwSxMfea3AP # JXlQNdjb+N3XH+1m6Aj+WORZ7CmfDjNUoic/aKr4DjKSc5lrwn/Jp1BIuP4F8Haa # 14tShRul0LFdE+cebAqnMpww+bxe65Y9Y3Jsi5MTfW7ZaExi6dYpah3rKsQGPcGL # trJeomo5SqiTlSqWuXJIhB3Eysv5PRDAVNkG8YGbOWR8FnSj3Z/0WojeXK0pqXT8 # GWnHtiADn3lKyrIv+6WigeinS1bEF6poVfVTFZTfZd5IzMKZLKGCFxUwghcRBgor # BgEEAYI3AwMBMYIXATCCFv0GCSqGSIb3DQEHAqCCFu4wghbqAgEDMQ8wDQYJYIZI # AWUDBAIBBQAwggFYBgsqhkiG9w0BCRABBKCCAUcEggFDMIIBPwIBAQYKKwYBBAGE # WQoDATAxMA0GCWCGSAFlAwQCAQUABCCIA/E7EIPGwjJN3Sh31rsZo4PclH1DejSv # MB+hzM6PSwIGYxIN+ad6GBIyMDIyMDkwNjIxNDAyNy4zMVowBIACAfSggdikgdUw # gdIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS # ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsT # JE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEmMCQGA1UECxMd # VGhhbGVzIFRTUyBFU046ODZERi00QkJDLTkzMzUxJTAjBgNVBAMTHE1pY3Jvc29m # dCBUaW1lLVN0YW1wIFNlcnZpY2WgghFlMIIHFDCCBPygAwIBAgITMwAAAYwBl2JH # NnZmOwABAAABjDANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UE # CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z # b2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQ # Q0EgMjAxMDAeFw0yMTEwMjgxOTI3NDRaFw0yMzAxMjYxOTI3NDRaMIHSMQswCQYD # VQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEe # MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3Nv # ZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBU # U1MgRVNOOjg2REYtNEJCQy05MzM1MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1T # dGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA00ho # TKET+SGsayw+9BFdm+uZ+kvEPGLd5sF8XlT3Uy4YGqT86+Dr8G3k6q/lRagixRKv # n+g2AFRL9VuZqC1uTva7dZN9ChiotHHFmyyQZPalXdJTC8nKIrbgTMXAwh/mbhnm # oaxsI9jGlivYgi5GNOE7u6TV4UOtnVP8iohTUfNMKhZaJdzmWDjhWC7LjPXIham9 # QhRkVzrkxfJKc59AsaGD3PviRkgHoGxfpdWHPPaW8iiEHjc4PDmCKluW3J+IdU38 # H+MkKPmekC7GtRTLXKBCuWKXS8TjZY/wkNczWNEo+l5J3OZdHeVigxpzCneskZfc # HXxrCX2hue7qJvWrksFStkZbOG7IYmafYMQrZGull72PnS1oIdQdYnR5/ngcvSQb # 11GQ0kNMDziKsSd+5ifUaYbJLZ0XExNV4qLXCS65Dj+8FygCjtNvkDiB5Hs9I7K9 # zxZsUb7fKKSGEZ9yA0JgTWbcAPCYPtuAHVJ8UKaT967pJm7+r3hgce38VU39spee # HHgaCS4vXrelTLiUMAl0Otk5ncKQKc2kGnvuwP2RCS3kEEFAxonwLn8pyedyreZT # bBMQBqf1o3kj0ilOJ7/f/P3c1rnaYO01GDJomv7otpb5z+1hrSoIs8u+6eruJKCT # ihd0i/8bc67AKF76wpWuvW9BhbUMTsWkww4r42cCAwEAAaOCATYwggEyMB0GA1Ud # DgQWBBSWzlOGqYIhYIh5Vp0+iMrdQItSIzAfBgNVHSMEGDAWgBSfpxVdAF5iXYP0 # 5dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29m # dC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIw # MjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8v # d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUt # U3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQM # MAoGCCsGAQUFBwMIMA0GCSqGSIb3DQEBCwUAA4ICAQDXaMVFWMIJqdblQZK6oks7 # cdCUwePAmmEIedsyusgUMIQlQqajfCP9iG58yOFSRx2k59j2hABSZBxFmbkVjwhY # EC1yJPQm9464gUz5G+uOW51i8ueeeB3h2i+DmoWNKNSulINyfSGgW6PCDCiRqO3q # n8KYVzLzoemfPir/UVx5CAgVcEDAMtxbRrTHXBABXyCa6aQ3+jukWB5aQzLw6qhH # hz7HIOU9q/Q9Y2NnVBKPfzIlwPjb2NrQGfQnXTssfFD98OpRHq07ZUx21g4ps8V3 # 3hSSkJ2uDwhtp5VtFGnF+AxzFBlCvc33LPTmXsczly6+yQgARwmNHeNA262WqLLJ # M84Iz8OS1VfE1N6yYCkLjg81+zGXsjvMGmjBliyxZwXWGWJmsovB6T6h1GrfmvMK # udOE92D67SR3zT3DdA5JwL9TAzX8Uhi0aGYtn5uNUDFbxIozIRMpLVpP/YOLng+r # 2v8s8lyWv0afjwZYHBJ64MWVNxHcaNtjzkYtQjdZ5bhyka6dX+DtQD9bh3zji0Sl # rfVDILxEb6OjyqtfGj7iWZvJrb4AqIVgHQaDzguixES9ietFikHff6p97C5qobTT # bKwN0AEP3q5teyI9NIOVlJl0gi5Ibd58Hif3JLO6vp+5yHXjoSL/MlhFmvGtaYmQ # wD7KzTm9uADF4BzP/mx2vzCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAA # ABUwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo # aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y # cG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1 # dGhvcml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDEL # MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v # bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWlj # cm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4IC # DwAwggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/ # bErg4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNE # t6aORmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2 # Nr41JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOOR # j7I5LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnop # N6zL64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0g # z3N9QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/ # BQOj0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8Qmgu # EOqEUUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFd # XzB0kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y # 7435UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQAB # o4IB3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS # /mTEmr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1Gely # MFwGA1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6 # Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTAT # BgNVHSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL # BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+ii # XGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jv # c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0y # My5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1p # Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNy # dDANBgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5O # R2R4sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6 # th542DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhT # dSRXud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnD # vBewVIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvf # SaN0DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+ # CljdQDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5a # GZFrDZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+ # fKFhbHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmN # cP7ntdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGe # Pu1+oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNY # s6FwZvKhggLUMIICPQIBATCCAQChgdikgdUwgdIxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9w # ZXJhdGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046ODZERi00 # QkJDLTkzMzUxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wi # IwoBATAHBgUrDgMCGgMVADSi8hTrq/Q8oppweGyuZLNEJq/VoIGDMIGApH4wfDEL # MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v # bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWlj # cm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJKoZIhvcNAQEFBQACBQDmwdJX # MCIYDzIwMjIwOTA2MjIwNjE1WhgPMjAyMjA5MDcyMjA2MTVaMHQwOgYKKwYBBAGE # WQoEATEsMCowCgIFAObB0lcCAQAwBwIBAAICDL0wBwIBAAICEX8wCgIFAObDI9cC # AQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgCAQACAwehIKEK # MAgCAQACAwGGoDANBgkqhkiG9w0BAQUFAAOBgQBvZOyf9CJtI8FWiYilvAB1+PXW # bTGdgFeSd7MLiMPErX28t1Jd64cqMdtTNEqyop1R5SGPjUvJjlSMVH40hyXNvvAE # ye5UkRUvwKxMOHIMeA0xQrSm7soPS2rwqz2hZmMK4w7Ad2EIeusZLTohJgYXiL+E # 9eeiagSFfgbIiZUIfTGCBA0wggQJAgEBMIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w # IFBDQSAyMDEwAhMzAAABjAGXYkc2dmY7AAEAAAGMMA0GCWCGSAFlAwQCAQUAoIIB # SjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIEIJyp # /DDIP0m6UifpkG5sD5JZOy4dEdt3OLNWiwNiZs+DMIH6BgsqhkiG9w0BCRACLzGB # 6jCB5zCB5DCBvQQg1a2L+BUqkM8Gf8TmIQWdgeKTTrYXIwOofOuJiBiYaZ4wgZgw # gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD # VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAYwBl2JHNnZm # OwABAAABjDAiBCB0Psu/yN03HWFV14l9L4G+lUNnTR9JmibVUJCQ5OeqEzANBgkq # hkiG9w0BAQsFAASCAgBc7/CrWGMdGakrIbyHjZQDyvCiT1KW3e92Z1uluZNeQbBR # ewOvQjDPrp4KQez7J47RfF6cP9PwhC0KjnVlJkV+2e6hi+C8RHJuGWMV07eN6A5l # yceM9/gX7PCcVcWMqIh8w2dN08bKuV+GZn3PhEjGvzn16jwvKUF/0dNU4Vj39lBb # ti3MaJOAc91r2En1OH1dSUXF+QVWrn+4alSlTJgLz64OE/l9bw+xhRIzNkFAL/yL # KV/JLVAaTAG840sJqWHXv/Zu4t07UGPHtcMFu6yXcO5x0LuWK1kr2xki4GIzeFYW # YGtvw07tvclk9zhG3ukyLEfEHgtRiBN8fjg15yE6ltnlMmpNRZtchd4lOcB2R2tU # MOVCK2oyahe3GYsQNVpW0M/xiysVZdedqEnaGUfTgoTzC5YGEcEUkdM4leUBiC0a # 8AVGmaUiafKko/TDpcr00sbOFueAFIy2Bsk2dYAC9Hu4nsLOJa1XGrCnJ8Cr84Ok # EVeyg/SgenEBB2V7EQ89i5id2ux9OrHRUAAm+k2ez+FR9iKVqvOSk705i0VdqdcV # ySbt4+mtGbABvIztT5P98+YuOWnkqqdNSj6qTptNVjYy3VQKcqvjyTt7EwPHa3a0 # j6HMxpJlhsIr3KNuzu2uaJqzB8ayHUq1R5o3rK28wHnCBbZT9oI05zxF9yXgAQ== # SIG # End signature block |