src/cmdlets/Invoke-GraphRequest.ps1
# Copyright 2018, Adam Edwards # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. . (import-script New-GraphConnection) . (import-script ../common/GraphUtilities) . (import-script ../common/GraphAccessDeniedException) . (import-script common/QueryHelper) . (import-script ../REST/GraphRequest) . (import-script ../REST/GraphErrorRecorder) function Invoke-GraphRequest { [cmdletbinding(positionalbinding=$false, supportspaging=$true, supportsshouldprocess=$true)] param( [parameter(position=0, mandatory=$true)] [Uri[]] $RelativeUri, [parameter(position=1)] [String] $Verb = 'GET', [parameter(position=2)] $Payload = $null, [String] $Query = $null, [String] $ODataFilter = $null, [String[]] $Select = $null, [String[]] $Expand = $null, [Alias('Sort')] $OrderBy = $null, [Switch] $Descending, [parameter(parametersetname='MSGraphNewConnection')] [String[]] $ScopeNames = $null, [String] $Version = $null, [switch] $RawContent, [switch] $AbsoluteUri, [HashTable] $Headers = $null, [parameter(parametersetname='AADGraphNewConnection', mandatory=$true)] [switch] $AADGraph, [parameter(parametersetname='MSGraphNewConnection')] [GraphCloud] $Cloud = [GraphCloud]::Public, [parameter(parametersetname='ExistingConnection', mandatory=$true)] [PSCustomObject] $Connection = $null ) $::.GraphErrorRecorder |=> StartRecording if ( $Query ) { if ( $ODataFilter -or $Select -or $OrderBy ) { throw [ArgumentException]::new("'ODataFilter' and 'Select' options may not specified with 'Query'") } } if ( $Descending.IsPresent -and ! $OrderBy ) { throw [ArgumentException]::new("'Descending' option was specified without 'OrderBy'") } $orderQuery = if ( $OrderBy ) { try { $::.QueryHelper |=> GetOrderQueryFromOrderByParameters $OrderBy $Descending.IsPresent } catch { throw } } if ( $AbsoluteUri.IsPresent ) { if ( $RelativeUri.length -gt 1 ) { throw "More than one Uri was specified when AbsoluteUri was specified -- only one Uri is allowed when AbsoluteUri is configured" } } elseif ( $RelativeUri[0].IsAbsoluteUri -and ! (! $RelativeUri[0].Host) ) { throw "An absolute URI was specified -- specify a URI relative to the graph host and version, or specify -AbsoluteUri" } $defaultVersion = $null $graphType = if ($Connection -ne $null ) { $Connection.GraphEndpoint.Type } elseif ( $AADGraph.ispresent ) { ([GraphType]::AADGraph) } else { ([GraphType]::MSGraph) } $MSGraphScopeNames = if ( $ScopeNames -ne $null ) { if ( $Connection -ne $null ) { throw "Scopes may not be specified via -ScopeNames if an existing connection is supplied with -Connection" } $ScopeNames } else { @('User.Read') } $requestQuery = if ( $Query ) { @($Query) } else { $queryParameters = [string[]] @() if ( $Select ) { $queryParameters += @('$select={0}') -f ($Select -join ',') } if ( $Expand ) { $queryParameters += @('$expand={0}') -f ($Expand -join ',') } if ( $ODataFilter ) { $queryParameters += @('$filter={0}' -f $ODataFilter) } if ( $orderQuery ) { $queryParameters += @('$orderBy={0}' -f $orderQuery) } if ( $queryParameters.length -gt 0 ) { $queryParameters } } if ( $pscmdlet.pagingparameters.includetotalcount.ispresent -eq $true ) { write-verbose 'Including the total count of results' $requestQuery += '$count' } # Cast it in case this is a deserialized object -- # workaround for a defect in ScriptClass switch ([GraphType] $graphType) { ([GraphType]::AADGraph) { $defaultVersion = '1.6' } ([GraphType]::MSGraph) { $defaultVersion = 'GraphContext' |::> GetDefaultVersion } default { throw "Unexpected identity type '$graphType'" } } $currentContext = $null $graphConnection = if ( $Connection -eq $null ) { if ( $graphType -eq ([GraphType]::AADGraph) ) { $::.GraphConnection |=> NewSimpleConnection ([GraphType]::AADGraph) $cloud $MSGraphScopeNames } else { 'GraphContext' |::> GetConnection $null $null $cloud $ScopeNames } } else { $Connection } $uriInfo = if ( $AbsoluteUri.ispresent ) { write-verbose "Caller specified AbsoluteUri -- interpreting uri as absolute" $specificContext = new-so GraphContext $graphConnection $version 'local' $info = $::.GraphUtilities |=> ParseGraphUri $RelativeUri[0] $specificContext write-verbose "Absolute uri parsed as relative '$($info.GraphRelativeUri)' and version $($info.GraphVersion)" if ( ! $info.IsAbsolute ) { throw "Absolute Uri was specified, but given Uri was not absolute: '$($RelativeUri[0])'" } if ( ! $info.IsContextCompatible ) { throw "The version '$($info.Graphversion)' and connection endpoint '$($specificcontext.Connection.GraphEndpoint.Graph)' is not compatible with the uri '$RelativeUri'" } $info } elseif ( $graphType -ne ([GraphType]::AADGraph) ) { # We only parse URI's relative to context for MS Graph -- AADGraph # context is not tracked, so don't try to construct a context relative # AAD Graph path if ( ($::.GraphContext |=> GetCurrent).location ) { $info = $::.GraphUtilities |=> ParseGraphRelativeLocation $RelativeUri[0] @{ GraphRelativeUri = $info.GraphRelativeUri GraphVersion = $info.context.version } } else { @{ GraphRelativeUri = $RelativeUri[0] GraphVersion = ($::.GraphContext |=> GetCurrent).version } } } $apiVersion = if ( $Version -ne $null -and $version.length -ne 0 ) { write-verbose "Using version specified by caller: '$Version'" $Version } elseif ( $uriInfo -and $uriInfo.GraphVersion -and $uriInfo ) { write-verbose "Using version from implied relative uri: '$($uriInfo.GraphVersion)'" $uriInfo.GraphVersion } else { if ( $currentContext ) { write-verbose "Using context Graph version '$($currentContext.Version)'" $currentContext.Version } else { write-verbose "Using default Graph version '$defaultVersion'" $defaultVersion } } $tenantQualifiedVersionSegment = if ( $graphType -eq ([GraphType]::AADGraph) ) { $graphConnection |=> Connect $graphConnection.Identity.Token.TenantId } else { $apiVersion } $firstIndex = if ( $pscmdlet.pagingparameters.Skip -ne $null -and $pscmdlet.pagingparameters.skip -ne 0 ) { write-verbose "Skipping the first '$($pscmdlet.pagingparameters.skip)' parameters" $pscmdlet.pagingparameters.Skip } $maxReturnedResults = $null $maxResultCount = if ( $pscmdlet.pagingparameters.first -ne $null -and $pscmdlet.pagingparameters.first -lt [Uint64]::MaxValue ) { $pscmdlet.pagingparameters.First | tee -variable maxReturnedResults } else { 10 } $skipCount = $firstIndex $results = @() $inputUriRelative = if ( ! $uriInfo ) { $RelativeUri[0] } else { $uriInfo.GraphRelativeUri } $contextUri = if ( ($::.GraphContext |=> GetCurrent).location ) { $::.GraphUtilities |=> ToGraphRelativeUri $inputUriRelative } else { $inputUriRelative } $graphRelativeUri = $::.GraphUtilities |=> JoinRelativeUri $tenantQualifiedVersionSegment $contextUri $countError = $false $optionalCountResult = $null while ( $graphRelativeUri -ne $null -and ($graphRelativeUri.tostring().length -gt 0) -and ($maxResultCount -eq $null -or $results.length -lt $maxResultCount) ) { if ( $graphType -eq ([GraphType]::AADGraph) ) { $graphRelativeUri = $graphRelativeUri, "api-version=$apiVersion" -join '?' } $graphResponse = if ( $graphConnection.status -ne ([GraphConnectionStatus]::Offline) ) { $request = new-so GraphRequest $graphConnection $graphRelativeUri $Verb $Headers $requestQuery $request |=> SetBody $Payload try { $request |=> Invoke $skipCount } catch [System.Net.WebException] { $statusCode = if ( $_.exception.response | gm statuscode -erroraction silentlycontinue ) { $_.exception.response.statuscode } if ( $statusCode -eq 'Unauthorized' -or $statusCode -eq 'Forbidden' ) { throw [GraphAccessDeniedException]::new($_.exception) } # Note that there may be other errors, such as 'BadRequest' that deserve a warning rather than failure, # so we should consider adding others if the cases can be narrowed sufficiently to avoid other # undesirable side effects of continuing on an error. An even better workaround may be command-completion, # which would (and should!) be scoped to purely local operations -- this would give visibility as to # the next segments without a request to Graph that could fail. throw } } $skipCount = $null $content = if ( $graphResponse -and $graphResponse.Entities -ne $null ) { $graphRelativeUri = $graphResponse.Nextlink if (! $RawContent.ispresent) { $entities = if ( $graphResponse.entities -is [Object[]] -and $graphResponse.entities.length -eq 1 ) { @([PSCustomObject] $graphResponse.entities) } elseif ($graphResponse.entities -is [HashTable]) { @([PSCustomObject] $graphResponse.Entities) } else { $graphResponse.Entities } if ( $pscmdlet.pagingparameters.includetotalcount.ispresent -eq $true -and $results.length -eq 0 ) { try { $optionalCountResult = $graphResponse.RestResponse.value.count } catch { $countError = $true } } $entities } else { $graphResponse |=> Content } } else { $graphRelativeUri = $null if ( $graphResponse ) { $graphResponse |=> Content } } if ( $graphResponse -and ( ! $RawContent.ispresent ) ) { # Add __ItemContext to decorate the object with its source uri. # Do this as a script method to prevent deserialization $requestUriNoQuery = $request.Uri.GetLeftPart([System.UriPartial]::Path) $ItemContextScript = [ScriptBlock]::Create("[PSCustomObject] @{RequestUri=`"$requestUriNoQuery`"}") $content | foreach { $_ | add-member -membertype scriptmethod -name __ItemContext -value $ItemContextScript } } $results += $content } if ($pscmdlet.pagingparameters.includetotalcount.ispresent -eq $true) { $accuracy = [double] 1.0 $count = if ( $optionalCountResult -eq $null ) { $accuracy = [double] .1 $results.length } else { if ( $countError ) { $accuracy = [double] .5 } $optionalCountResult } $PSCmdlet.PagingParameters.NewTotalCount($count, $accuracy) } if ( $maxReturnedResults ) { $results | select -first $maxReturnedResults } else { $results } } |