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, # 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 ) ## 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 } foreach ($results in ($resultsBatch)) { 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 (!$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/' ) 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 { # [hashtable] $results = Invoke-MgGraphRequest -Method $Request.method -Uri $uriEndpoint.AbsoluteUri -Headers $Request.headers $results = Invoke-RestMethod -WebSession $MsGraphSession -UseBasicParsing @paramInvokeRestMethod -ErrorAction Stop 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 } } catch { Catch-MsGraphError $_ } 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 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 try { $Result = Invoke-RestMethod -WebSession $MsGraphSession -UseBasicParsing -Method Get -Uri $nextLink -Headers $Request.headers -ErrorAction Stop } catch { Catch-MsGraphError $_ } #$Request.url = $Result.'@odata.nextLink' #$Result = Invoke-MsGraphRequest $Request -NoAppInsights -GraphBaseUri $GraphBaseUri $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 (!$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 # MIInoQYJKoZIhvcNAQcCoIInkjCCJ44CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBBzv/lQuU5NXe2 # t21JPP9zXLznE6FNptyncoSxRmvQgKCCDYEwggX/MIID56ADAgECAhMzAAACUosz # qviV8znbAAAAAAJSMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjEwOTAyMTgzMjU5WhcNMjIwOTAxMTgzMjU5WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDQ5M+Ps/X7BNuv5B/0I6uoDwj0NJOo1KrVQqO7ggRXccklyTrWL4xMShjIou2I # sbYnF67wXzVAq5Om4oe+LfzSDOzjcb6ms00gBo0OQaqwQ1BijyJ7NvDf80I1fW9O # L76Kt0Wpc2zrGhzcHdb7upPrvxvSNNUvxK3sgw7YTt31410vpEp8yfBEl/hd8ZzA # v47DCgJ5j1zm295s1RVZHNp6MoiQFVOECm4AwK2l28i+YER1JO4IplTH44uvzX9o # RnJHaMvWzZEpozPy4jNO2DDqbcNs4zh7AWMhE1PWFVA+CHI/En5nASvCvLmuR/t8 # q4bc8XR8QIZJQSp+2U6m2ldNAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUNZJaEUGL2Guwt7ZOAu4efEYXedEw # UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1 # ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDY3NTk3MB8GA1UdIwQYMBaAFEhu # ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu # bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w # Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3 # Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx # MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAFkk3 # uSxkTEBh1NtAl7BivIEsAWdgX1qZ+EdZMYbQKasY6IhSLXRMxF1B3OKdR9K/kccp # kvNcGl8D7YyYS4mhCUMBR+VLrg3f8PUj38A9V5aiY2/Jok7WZFOAmjPRNNGnyeg7 # l0lTiThFqE+2aOs6+heegqAdelGgNJKRHLWRuhGKuLIw5lkgx9Ky+QvZrn/Ddi8u # TIgWKp+MGG8xY6PBvvjgt9jQShlnPrZ3UY8Bvwy6rynhXBaV0V0TTL0gEx7eh/K1 # o8Miaru6s/7FyqOLeUS4vTHh9TgBL5DtxCYurXbSBVtL1Fj44+Od/6cmC9mmvrti # yG709Y3Rd3YdJj2f3GJq7Y7KdWq0QYhatKhBeg4fxjhg0yut2g6aM1mxjNPrE48z # 6HWCNGu9gMK5ZudldRw4a45Z06Aoktof0CqOyTErvq0YjoE4Xpa0+87T/PVUXNqf # 7Y+qSU7+9LtLQuMYR4w3cSPjuNusvLf9gBnch5RqM7kaDtYWDgLyB42EfsxeMqwK # WwA+TVi0HrWRqfSx2olbE56hJcEkMjOSKz3sRuupFCX3UroyYf52L+2iVTrda8XW # esPG62Mnn3T8AuLfzeJFuAbfOSERx7IFZO92UPoXE1uEjL5skl1yTZB3MubgOA4F # 8KoRNhviFAEST+nG8c8uIsbZeb08SeYQMqjVEmkwggd6MIIFYqADAgECAgphDpDS # AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK # V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0 # IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0 # ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla # MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS # ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT # H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB # AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG # OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S # 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz # y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7 # 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u # M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33 # X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl # XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP # 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB # l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF # RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM # CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ # BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud # DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO # 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0 # LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y # Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p # Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y # Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB # FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw # cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA # XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY # 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj # 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd # d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ # Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf # wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ # aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j # NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B # xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96 # eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7 # r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I # RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIZdjCCGXICAQEwgZUwfjELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z # b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAlKLM6r4lfM52wAAAAACUjAN # BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor # BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgYGLl+NN+ # +7/OjRGdYPi3W4Lj7b4uv4CKr2NwYYlt5HwwQgYKKwYBBAGCNwIBDDE0MDKgFIAS # AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN # BgkqhkiG9w0BAQEFAASCAQAV04gQxF1zOg687B78WUo4i8LBTMtNaBOrJ/q7rZEW # n1S/p0Qb9eFfR8NIWsTshWZ4f/CPl/7lUKL/KQlwkLdFFheYDWFka2Sz4gUC4EV0 # gbhI4VpFRXXyvHR7tCqgqH9oLHFLmN9iomDgL7fLEpgs+2Aox4j3kMbrJ+gg0/JD # 5C+S562Y2NxcZVcK3UnwiraVQ7AH7XuBIeme6n4o3/eb3cshBG7G+vyx4/kTMO0F # bIaOf/GbiHdW5o7dq5KCoPNU9hUQi4ZuP7160ZVbzgOzoQ9m39396iP2PYhgUPTx # Idgg/5a0g+SCuqx7OCmfNumzDLyHMuOZVZ/08UMEHFJMoYIXADCCFvwGCisGAQQB # gjcDAwExghbsMIIW6AYJKoZIhvcNAQcCoIIW2TCCFtUCAQMxDzANBglghkgBZQME # AgEFADCCAVEGCyqGSIb3DQEJEAEEoIIBQASCATwwggE4AgEBBgorBgEEAYRZCgMB # MDEwDQYJYIZIAWUDBAIBBQAEIEuZ4ealL3nxoGGWCawxGO7mr15xFABuXzp+FwWy # OEHpAgZiFmCaDPcYEzIwMjIwMzI0MDAwMTU2LjgzNVowBIACAfSggdCkgc0wgcox # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1p # Y3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJjAkBgNVBAsTHVRoYWxlcyBUU1Mg # RVNOOjhBODItRTM0Ri05RERBMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFt # cCBTZXJ2aWNloIIRVzCCBwwwggT0oAMCAQICEzMAAAGZyI+vrbZ9vosAAQAAAZkw # DQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0 # b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh # dGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcN # MjExMjAyMTkwNTE2WhcNMjMwMjI4MTkwNTE2WjCByjELMAkGA1UEBhMCVVMxEzAR # BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p # Y3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2Eg # T3BlcmF0aW9uczEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046OEE4Mi1FMzRGLTlE # REExJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0G # CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC4E/lXXKMsy9rVa2a8bRb0Ar/Pj4+b # KiAgMgKayvCMFn3ddGof8eWFgJWp5JdKjWjrnmW1r9tHpcP2kFpjXp2Udrj55jt5 # NYi1MERcoIo+E29XuCwFAMJftGdvsWea/OTQPIFsZEWqEteXdRncyVwct5xFzBIC # 1JWCdmfc7R59RMIyvgWjIz8356mweowkOstN1fe53KIJ8flrYILIQWsNRMOT3znA # GwIb9kyL54C6jZjFxOSusGYmVQ+Gr/qZQELw1ipx9s5jNP1LSpOpfTEBFu+y9KLN # BmMBARkSPpTFkGEyGSwGGgSdOi6BU6FPK+6urZ830jrRemK4JkIJ9tQhlGcIhAjh # cqZStn+38lRjVvrfbBI5EpI2NwlVIK2ibGW7sWeTAz/yNPNISUbQhGAJse/OgGj/ # 1qz/Ha9mqfYZ8BHchNxn08nWkqyrjrKicQyxuD8mCatTrVSbOJYfQyZdHR9a4vgy # GeZEXBYQNAlIuB37QCOAgs/VeDU8M4dc/IlrTyC0uV1SS4Gk8zV+5X5eRu+XORN8 # FWqzI6k/9y6cWwOWMK6aUN1XqLcaF/sm9rX84eKW2lhDc3C31WLjp8UOfOHZfPuy # y54xfilnhhCPy4QKJ9jggoqqeeEhCEfgDYjy+PByV/e5HDB2xHdtlL93wltAkI3a # Cxo84kVPBCa0OwIDAQABo4IBNjCCATIwHQYDVR0OBBYEFI26Vrg+nGWvrvIh0dQP # EonENR0QMB8GA1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRY # MFYwVKBSoFCGTmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01p # Y3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEF # BQcBAQRgMF4wXAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9w # a2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAo # MSkuY3J0MAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwgwDQYJKoZI # hvcNAQELBQADggIBAHGzWh29ibBNro3ns8E3EOHGsLB1Gzk90SFYUKBilIu4jDbR # 7qbvXNd8nnl/z5D9LKgw3T81jqy5tMiWp+p4jYBBk3PRx1ySqLUfhF5ZMWolRzW+ # cQZGXV38iSmdAUG0CpR5x1rMdPIrTczVUFsOYGqmkoUQ/dRiVL4iAXJLCNTj4x3Y # wIQcCPt0ijJVinPIMAYzA8f99BbeiskyI0BHGAd0kGUX2I2/puYnlyS8toBnANjh # 21xgvEuaZ2dvRqvWk/i1XIlO67au/XCeMTvXhPOIUmq80U32Tifw3SSiBKTyir7m # oWH1i7H2q5QAnrBxuyy//ZsDfARDV/Atmj5jr6ATfRHDdUanQpeoBS+iylNU6RAR # u8g+TMCu/ZndZmrs9w+8galUIGg+GmlNk07fXJ58Oc+qFqgNAsNkMi+dSzKkWGA4 # /klJFn0XichXL8+t7KOayXKGzQja6CdtCjisnyS8hbv4PKhaeMtf68wJWKKOs0tt # 2AJfYC5vSbH9ck8BGj2e/yQXEZEu88L5/fHK5XUk/IKXx3zaLkxXTSZ43Ea/WKXV # BzMasHZ3Pmny0moEekAXx1UhLNNYv4Vum33VirxSB6r/GKQxFSHu7yFfrWQpYyyD # H119TmhAedS8T1VabqdtO5ZP2E14TK82Vyxy3xEPelOo4dRIlhm7XY6k9B68MIIH # cTCCBVmgAwIBAgITMwAAABXF52ueAptJmQAAAAAAFTANBgkqhkiG9w0BAQsFADCB # iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl # ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMp # TWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEw # OTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UE # CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z # b2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQ # Q0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOThpkzntHIh # C3miy9ckeb0O1YLT/e6cBwfSqWxOdcjKNVf2AX9sSuDivbk+F2Az/1xPx2b3lVNx # WuJ+Slr+uDZnhUYjDLWNE893MsAQGOhgfWpSg0S3po5GawcU88V29YZQ3MFEyHFc # UTE3oAo4bo3t1w/YJlN8OWECesSq/XJprx2rrPY2vjUmZNqYO7oaezOtgFt+jBAc # nVL+tuhiJdxqD89d9P6OU8/W7IVWTe/dvI2k45GPsjksUZzpcGkNyjYtcI4xyDUo # veO0hyTD4MmPfrVUj9z6BVWYbWg7mka97aSueik3rMvrg0XnRm7KMtXAhjBcTyzi # YrLNueKNiOSWrAFKu75xqRdbZ2De+JKRHh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9 # fvzZnkXftnIv231fgLrbqn427DZM9ituqBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdH # GO2n6Jl8P0zbr17C89XYcz1DTsEzOUyOArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7X # KHYC4jMYctenIPDC+hIK12NvDMk2ZItboKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiE # R9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/ # eKtFtvUeh17aj54WcmnGrnu3tz5q4i6tAgMBAAGjggHdMIIB2TASBgkrBgEEAYI3 # FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQWBBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAd # BgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEE # AYI3TIN9AQEwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29t # L3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMI # MBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMB # Af8EBTADAQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1Ud # HwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3By # b2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEFBQcBAQRO # MEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2Vy # dHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0MA0GCSqGSIb3DQEBCwUAA4IC # AQCdVX38Kq3hLB9nATEkW+Geckv8qW/qXBS2Pk5HZHixBpOXPTEztTnXwnE2P9pk # bHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gng # ugnue99qb74py27YP0h1AdkY3m2CDPVtI1TkeFN1JFe53Z/zjj3G82jfZfakVqr3 # lbYoVSfQJL1AoL8ZthISEV09J+BAljis9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHC # gRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTpkbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6 # MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEU # BHG/ZPkkvnNtyo4JvbMBV0lUZNlz138eW0QBjloZkWsNn6Qo3GcZKCS6OEuabvsh # VGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJsWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+ # fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrp # NPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0dFtq0Z4+7X6gMTN9vMvpe784cETRkPHI # qzqKOghif9lwY1NNje6CbaUFEMFxBmoQtB1VM1izoXBm8qGCAs4wggI3AgEBMIH4 # oYHQpIHNMIHKMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G # A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUw # IwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMSYwJAYDVQQLEx1U # aGFsZXMgVFNTIEVTTjo4QTgyLUUzNEYtOUREQTElMCMGA1UEAxMcTWljcm9zb2Z0 # IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIaAxUAku/zYujnqapN6BJ9 # MJ5jtgDrlOuggYMwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu # Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv # cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAN # BgkqhkiG9w0BAQUFAAIFAOXlxyswIhgPMjAyMjAzMjQwMDE5NTVaGA8yMDIyMDMy # NTAwMTk1NVowdzA9BgorBgEEAYRZCgQBMS8wLTAKAgUA5eXHKwIBADAKAgEAAgIL # sgIB/zAHAgEAAgIRUjAKAgUA5ecYqwIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgor # BgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUA # A4GBAHWiYlUQ/3cqxc41HrLMIE4HBYM/9Dyc489ODKRHFFfD9Gem7N8kRkjqaN79 # Q+8m8vf0gAPuKSJBEvDQzJNBO6/ySNlrNE0vyO/ok3Y8N8ZoOLSzUGKRhG9MRvi0 # lBxChmVQy9pOj6b1w/UD9XFGnVeYIRcyhLeD5kflg+T5uoDPMYIEDTCCBAkCAQEw # gZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT # B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE # AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAGZyI+vrbZ9vosA # AQAAAZkwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0B # CRABBDAvBgkqhkiG9w0BCQQxIgQgUzpGMgv+0xjDhTKVlJH8oEq514/tgajrKyTa # emVpqRowgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCBmfWn58qKN7WWpBTYO # rUO1BSCSnKPLC/G7wCOIc2JsfjCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w # IFBDQSAyMDEwAhMzAAABmciPr622fb6LAAEAAAGZMCIEIMnSc6nGGvSBG18c1N0q # JcPoJSYwwKHX4i7NOow+2SKhMA0GCSqGSIb3DQEBCwUABIICAAEmaL7d1f8eEekC # w6sFyr4hm7jiGv2m3bodwGLKjN4yRCP4jTYUWTs2zZdnW3YUqz0wZSKpJyfVOO2a # Xm9CP5rzXpN7ZREI3a5x+7FMOgs/iti2iUm2Fm9roUrUZrZJO9+0dqCZH/Fm4cwK # SoiupE1Wi1uJLCRQ+wSS6RK6RY2XIewhu3lJf3dkcQ1k0CVx3+qDmnDA9dT8si42 # kF9tZOAr6bc4nWjnIPnfNmt+mSOI//l+AdFu7fhAGsmpsgXnSyE4yNyly9rX+Pnc # JZSBHGvk5ch69Y/B9tnRSbJazwtS4N7qV8kA+VdtvrrtH0RYrksGJkmUf20XncOw # t7lIr42LLywuFP44s6Um7NE0VwF42x8FasXtcCiJGgjpOJFZ+VRnW9b4TEpFEtmZ # vJrAUiyNQuGsJdKtRjteJyPMF7PWPTF3x/V6FOCJr3EIc9Leu4+T22hPb/faAjfH # obsLJyoS6Wp3920VkU865fULDOty/BE5E0JMiup18Z4u4eVzT7uXZ0P5uZzUb8hi # xy8MMjMOzA6slVqUbvoIJYEFVVnaRnv7700pwgOov/jjhrLMvswipDr19bOPhja1 # MvpL+EWlqyq/TnS2xJ97AmxQomu9WWsaza5UETKVtJHTFqiKuV5Do0E13Je0WKsI # My64xwi3sXkAztDN7MUsxbvmHkIh # SIG # End signature block |