Public/Invoke-MgGraphCommunityRequest.ps1
|
function Invoke-MgGraphCommunityRequest { <# .SYNOPSIS Calls a Microsoft Graph endpoint using the current MgGraphCommunity session. .DESCRIPTION A pure-PowerShell drop-in for Invoke-MgGraphRequest. Sends a request to Microsoft Graph using the access token from the most recent Connect-MgGraphCommunity call. Features: - Auto-prepends the active environment's Graph host for relative URIs ('/me' becomes 'https://graph.microsoft.com/v1.0/me'). - -Beta swaps /v1.0 -> /beta for relative URIs. - Proactive refresh: if the access token expires within 5 minutes, the cached refresh token is used to silently re-acquire BEFORE the call. - Reactive refresh: on HTTP 401, attempts one silent refresh and retries. - On HTTP 429, honors the Retry-After header and retries. - On HTTP 504 Gateway Timeout, sleeps 60 seconds and retries once. - -FollowPagination walks @odata.nextLink and returns combined values. - Default headers added via Add-MgGraphCommunityDefaultHeader are merged automatically. Per-call -Headers override defaults. - Surfaces Microsoft Graph error.code / error.message as PowerShell errors. Requires Connect-MgGraphCommunity to have established a session first. .PARAMETER Method HTTP method. Defaults to GET. .PARAMETER Uri Full URL (https://graph.microsoft.com/...) or a relative path ('/me', 'users?$top=5'). .PARAMETER Body Request body. Objects are serialized as JSON; strings are sent as-is. .PARAMETER Headers Per-call HTTP headers. Merged on top of any default headers; per-call wins. .PARAMETER OutputType 'PSObject' (default) parses JSON into PowerShell objects. 'Hashtable' returns ConvertFrom-Json -AsHashtable. 'HttpResponse' returns the raw response object. .PARAMETER FollowPagination Auto-follow @odata.nextLink and merge .value arrays across pages. .PARAMETER Beta Use the /beta endpoint instead of /v1.0 when expanding a relative URI. .EXAMPLE Invoke-MgGraphCommunityRequest -Uri '/me' .EXAMPLE Invoke-MgGraphCommunityRequest -Method GET -Uri 'https://graph.microsoft.com/beta/deviceManagement/managedDevices' .EXAMPLE Invoke-MgGraphCommunityRequest -Uri '/users?$top=5' -Beta .EXAMPLE Invoke-MgGraphCommunityRequest -Uri '/users' -FollowPagination .EXAMPLE Invoke-MgGraphCommunityRequest -Method POST -Uri '/groups' -Body @{ displayName = 'Marketing' mailEnabled = $false mailNickname = 'marketing' securityEnabled = $true } #> [CmdletBinding()] [Alias('Invoke-MgcRequest')] param( [ValidateSet('GET','POST','PUT','PATCH','DELETE')] [string]$Method = 'GET', [Parameter(Mandatory, Position = 0)] [string]$Uri, [object]$Body, [hashtable]$Headers, [ValidateSet('PSObject','Hashtable','HttpResponse')] [string]$OutputType = 'PSObject', [switch]$FollowPagination, [switch]$Beta ) if (-not $script:MgcActiveSession) { throw "Not connected. Run Connect-MgGraphCommunity first." } # ---- Proactive token refresh ---- # If the access token expires within 5 minutes, refresh before the call. if ($script:MgcActiveSession.ExpiresOn -and $script:MgcActiveSession.Tokens.refresh_token) { $remainingMin = ($script:MgcActiveSession.ExpiresOn - (Get-Date).ToUniversalTime()).TotalMinutes if ($remainingMin -le 5) { Write-Verbose ("Access token expires in {0:N1} min - refreshing proactively." -f $remainingMin) try { $newTokens = Invoke-MgcRefreshTokenAuth ` -LoginEndpoint $script:MgcActiveSession.Authority.Login ` -TenantSegment $script:MgcActiveSession.TenantSegment ` -ClientId $script:MgcActiveSession.ClientId ` -RefreshToken $script:MgcActiveSession.Tokens.refresh_token ` -Scopes $script:MgcActiveSession.Scopes $script:MgcActiveSession.Tokens = $newTokens $script:MgcActiveSession.ExpiresOn = Get-MgcTokenExpiry -Tokens $newTokens Save-MgcTokenCache -Key $script:MgcActiveSession.CacheKey -Tokens $newTokens -Persist:$script:MgcActiveSession.Persist } catch { Write-Verbose "Proactive refresh failed (will rely on reactive 401 retry): $_" } } } # Resolve relative URIs against the active environment $resolvedUri = if ($Uri -match '^https?://') { $Uri } else { $apiVer = if ($Beta) { 'beta' } else { 'v1.0' } $cleaned = $Uri.TrimStart('/') "{0}/{1}/{2}" -f $script:MgcActiveSession.Authority.GraphResource, $apiVer, $cleaned } # Build the request closure (so we can call it multiple times for retries) $sendRequest = { param([string]$accessToken) $mergedHeaders = @{ 'Authorization' = "Bearer $accessToken" 'Content-Type' = 'application/json' } # Session-wide default headers (set via Add-MgGraphCommunityDefaultHeader) if ($script:MgcDefaultHeaders) { foreach ($k in $script:MgcDefaultHeaders.Keys) { $mergedHeaders[$k] = $script:MgcDefaultHeaders[$k] } } # Per-call -Headers override defaults if ($Headers) { foreach ($k in $Headers.Keys) { $mergedHeaders[$k] = $Headers[$k] } } $params = @{ Uri = $resolvedUri Method = $Method Headers = $mergedHeaders } if ($PSBoundParameters.ContainsKey('Body') -and $null -ne $Body) { $params['Body'] = if ($Body -is [string]) { $Body } else { $Body | ConvertTo-Json -Depth 20 -Compress } } # Invoke-MgcHttpRequest abstracts -SkipHttpErrorCheck differences across # PS 5.1 vs 7.x and returns a uniform { StatusCode; Headers; Content } object. Invoke-MgcHttpRequest -Parameters $params } # ---- First attempt ---- $attempt = & $sendRequest -accessToken $script:MgcActiveSession.Tokens.access_token # ---- Retry on 401: reactive refresh once if possible ---- if ($attempt.StatusCode -eq 401 -and $script:MgcActiveSession.Tokens.refresh_token) { Write-Verbose "401 from Graph - attempting silent token refresh." try { $newTokens = Invoke-MgcRefreshTokenAuth ` -LoginEndpoint $script:MgcActiveSession.Authority.Login ` -TenantSegment $script:MgcActiveSession.TenantSegment ` -ClientId $script:MgcActiveSession.ClientId ` -RefreshToken $script:MgcActiveSession.Tokens.refresh_token ` -Scopes $script:MgcActiveSession.Scopes $script:MgcActiveSession.Tokens = $newTokens $script:MgcActiveSession.ExpiresOn = Get-MgcTokenExpiry -Tokens $newTokens Save-MgcTokenCache -Key $script:MgcActiveSession.CacheKey -Tokens $newTokens -Persist:$script:MgcActiveSession.Persist $attempt = & $sendRequest -accessToken $newTokens.access_token } catch { Write-Verbose "Reactive refresh failed: $_" } } # ---- Retry on 429: respect Retry-After ---- if ($attempt.StatusCode -eq 429) { $wait = 5 if ($attempt.Headers -and $attempt.Headers['Retry-After']) { try { $wait = [int]([array]$attempt.Headers['Retry-After'])[0] } catch { } } Write-Verbose "429 throttled by Graph - sleeping $wait seconds before retry." Start-Sleep -Seconds $wait $attempt = & $sendRequest -accessToken $script:MgcActiveSession.Tokens.access_token } # ---- Retry on 504: Graph gateway timeout ---- if ($attempt.StatusCode -eq 504) { Write-Verbose "504 Gateway Timeout from Graph - sleeping 60 seconds and retrying once." Start-Sleep -Seconds 60 $attempt = & $sendRequest -accessToken $script:MgcActiveSession.Tokens.access_token } # ---- Error handling ---- if ($attempt.StatusCode -ge 400) { $msg = "HTTP $($attempt.StatusCode) from $resolvedUri" try { if ($attempt.Content) { $errBody = $attempt.Content | ConvertFrom-Json -ErrorAction Stop if ($errBody.error) { $msg = "Graph error $($attempt.StatusCode) [$($errBody.error.code)]: $($errBody.error.message)" } } } catch { } throw $msg } if ($OutputType -eq 'HttpResponse') { return $attempt } # Parse JSON response $contentType = $null if ($attempt.Headers) { $contentType = $attempt.Headers['Content-Type'] } $isJson = $contentType -and ($contentType -join ' ') -match 'json' $raw = $attempt.Content if (-not $raw) { return $null } if (-not $isJson) { return $raw } # ConvertFrom-Json -AsHashtable is PS 6+; on PS 5.1 we convert PSObject -> Hashtable manually. $parsed = if ($OutputType -eq 'Hashtable') { if ($PSVersionTable.PSVersion.Major -ge 6) { $raw | ConvertFrom-Json -AsHashtable } else { $raw | ConvertFrom-Json | ConvertTo-MgcHashtable } } else { $raw | ConvertFrom-Json } # ---- Pagination ---- # Works whether $parsed is a Hashtable (PS 5.1 fallback) or a PSCustomObject. if ($FollowPagination -and $parsed.value -and $parsed.'@odata.nextLink') { $all = @($parsed.value) $next = $parsed.'@odata.nextLink' while ($next) { $page = Invoke-MgGraphCommunityRequest -Method GET -Uri $next -OutputType $OutputType if ($page.value) { $all += $page.value } $next = $page.'@odata.nextLink' } return $all } return $parsed } |