|
| 1 | +function New-CIPPAzRestRequest { |
| 2 | + <# |
| 3 | + .SYNOPSIS |
| 4 | + Create and send a REST request to Azure APIs using Managed Identity authentication |
| 5 | + .DESCRIPTION |
| 6 | + Wraps Invoke-RestMethod with automatic Azure Managed Identity token authentication. |
| 7 | + Automatically adds the Authorization header using Get-CIPPAzIdentityToken. |
| 8 | + Supports all Invoke-RestMethod parameters. |
| 9 | + .PARAMETER Uri |
| 10 | + The URI of the Azure REST API endpoint |
| 11 | + .PARAMETER Method |
| 12 | + The HTTP method (GET, POST, PUT, PATCH, DELETE, etc.). Defaults to GET. |
| 13 | + .PARAMETER ResourceUrl |
| 14 | + The Azure resource URL to get a token for. Defaults to 'https://management.azure.com/' for Azure Resource Manager. |
| 15 | + Use 'https://vault.azure.net' for Key Vault, 'https://api.loganalytics.io' for Log Analytics, etc. |
| 16 | + .PARAMETER Body |
| 17 | + The request body (can be string, hashtable, or PSCustomObject) |
| 18 | + .PARAMETER Headers |
| 19 | + Additional headers to include in the request. Authorization header is automatically added. |
| 20 | + .PARAMETER ContentType |
| 21 | + The content type of the request body. Defaults to 'application/json' if Body is provided and ContentType is not specified. |
| 22 | + .PARAMETER SkipHttpErrorCheck |
| 23 | + Skip checking HTTP error status codes |
| 24 | + .PARAMETER ResponseHeadersVariable |
| 25 | + Variable name to store response headers |
| 26 | + .PARAMETER StatusCodeVariable |
| 27 | + Variable name to store HTTP status code |
| 28 | + .PARAMETER MaximumRetryCount |
| 29 | + Maximum number of retry attempts |
| 30 | + .PARAMETER RetryIntervalSec |
| 31 | + Interval between retries in seconds |
| 32 | + .PARAMETER TimeoutSec |
| 33 | + Request timeout in seconds |
| 34 | + .PARAMETER UseBasicParsing |
| 35 | + Use basic parsing (for older PowerShell versions) |
| 36 | + .PARAMETER WebSession |
| 37 | + Web session object for maintaining cookies/state |
| 38 | + .EXAMPLE |
| 39 | + New-CIPPAzRestRequest -Uri 'https://management.azure.com/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/{name}?api-version=2020-06-01' |
| 40 | + Gets Azure Resource Manager resource using managed identity |
| 41 | + .EXAMPLE |
| 42 | + New-CIPPAzRestRequest -Uri 'https://management.azure.com/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/{name}/config/authsettingsV2/list?api-version=2020-06-01' -Method POST |
| 43 | + POST request to Azure Resource Manager API |
| 44 | + .EXAMPLE |
| 45 | + New-CIPPAzRestRequest -Uri 'https://{vault}.vault.azure.net/secrets/{secret}?api-version=7.4' -ResourceUrl 'https://vault.azure.net' |
| 46 | + Gets a Key Vault secret using managed identity |
| 47 | + .EXAMPLE |
| 48 | + New-CIPPAzRestRequest -Uri 'https://management.azure.com/...' -Method PUT -Body @{ property = 'value' } -ContentType 'application/json' |
| 49 | + PUT request with JSON body |
| 50 | + #> |
| 51 | + [CmdletBinding()] |
| 52 | + param( |
| 53 | + [Parameter(Mandatory = $true, Position = 0)] |
| 54 | + [Alias('Url')] |
| 55 | + [uri]$Uri, |
| 56 | + |
| 57 | + [Parameter(Mandatory = $false)] |
| 58 | + [ValidateSet('GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE')] |
| 59 | + [string]$Method = 'GET', |
| 60 | + |
| 61 | + [Parameter(Mandatory = $false)] |
| 62 | + [string]$ResourceUrl = 'https://management.azure.com/', |
| 63 | + |
| 64 | + [Parameter(Mandatory = $false)] |
| 65 | + [object]$Body, |
| 66 | + |
| 67 | + [Parameter(Mandatory = $false)] |
| 68 | + [hashtable]$Headers = @{}, |
| 69 | + |
| 70 | + [Parameter(Mandatory = $false)] |
| 71 | + [string]$ContentType, |
| 72 | + |
| 73 | + [Parameter(Mandatory = $false)] |
| 74 | + [switch]$SkipHttpErrorCheck, |
| 75 | + |
| 76 | + [Parameter(Mandatory = $false)] |
| 77 | + [string]$ResponseHeadersVariable, |
| 78 | + |
| 79 | + [Parameter(Mandatory = $false)] |
| 80 | + [string]$StatusCodeVariable, |
| 81 | + |
| 82 | + [Parameter(Mandatory = $false)] |
| 83 | + [int]$MaximumRetryCount, |
| 84 | + |
| 85 | + [Parameter(Mandatory = $false)] |
| 86 | + [int]$RetryIntervalSec, |
| 87 | + |
| 88 | + [Parameter(Mandatory = $false)] |
| 89 | + [int]$TimeoutSec, |
| 90 | + |
| 91 | + [Parameter(Mandatory = $false)] |
| 92 | + [switch]$UseBasicParsing, |
| 93 | + |
| 94 | + [Parameter(Mandatory = $false)] |
| 95 | + [Microsoft.PowerShell.Commands.WebRequestSession]$WebSession |
| 96 | + ) |
| 97 | + |
| 98 | + # Get Azure Managed Identity token |
| 99 | + try { |
| 100 | + $Token = Get-CIPPAzIdentityToken -ResourceUrl $ResourceUrl |
| 101 | + } catch { |
| 102 | + $errorMessage = "Failed to get Azure Managed Identity token: $($_.Exception.Message)" |
| 103 | + Write-Error -Message $errorMessage -ErrorAction $ErrorActionPreference |
| 104 | + return |
| 105 | + } |
| 106 | + |
| 107 | + # Build headers - add Authorization, merge with user-provided headers |
| 108 | + $RequestHeaders = @{ |
| 109 | + 'Authorization' = "Bearer $Token" |
| 110 | + } |
| 111 | + |
| 112 | + # Merge user-provided headers (user headers take precedence) |
| 113 | + foreach ($key in $Headers.Keys) { |
| 114 | + $RequestHeaders[$key] = $Headers[$key] |
| 115 | + } |
| 116 | + |
| 117 | + # Handle Content-Type |
| 118 | + if ($Body -and -not $ContentType) { |
| 119 | + $ContentType = 'application/json' |
| 120 | + } |
| 121 | + |
| 122 | + # Convert Body to JSON if it's an object and ContentType is JSON |
| 123 | + $RequestBody = $Body |
| 124 | + if ($Body -and $ContentType -eq 'application/json' -and $Body -isnot [string]) { |
| 125 | + try { |
| 126 | + $RequestBody = $Body | ConvertTo-Json -Depth 10 -Compress |
| 127 | + } catch { |
| 128 | + Write-Warning "Failed to convert Body to JSON: $($_.Exception.Message). Sending as-is." |
| 129 | + $RequestBody = $Body |
| 130 | + } |
| 131 | + } |
| 132 | + |
| 133 | + # Build Invoke-RestMethod parameters |
| 134 | + $RestMethodParams = @{ |
| 135 | + Uri = $Uri |
| 136 | + Method = $Method |
| 137 | + Headers = $RequestHeaders |
| 138 | + ErrorAction = $ErrorActionPreference |
| 139 | + } |
| 140 | + |
| 141 | + if ($Body) { |
| 142 | + $RestMethodParams['Body'] = $RequestBody |
| 143 | + } |
| 144 | + |
| 145 | + if ($ContentType) { |
| 146 | + $RestMethodParams['ContentType'] = $ContentType |
| 147 | + } |
| 148 | + |
| 149 | + if ($SkipHttpErrorCheck) { |
| 150 | + $RestMethodParams['SkipHttpErrorCheck'] = $true |
| 151 | + } |
| 152 | + |
| 153 | + if ($ResponseHeadersVariable) { |
| 154 | + $RestMethodParams['ResponseHeadersVariable'] = $ResponseHeadersVariable |
| 155 | + } |
| 156 | + |
| 157 | + if ($StatusCodeVariable) { |
| 158 | + $RestMethodParams['StatusCodeVariable'] = $StatusCodeVariable |
| 159 | + } |
| 160 | + |
| 161 | + if ($MaximumRetryCount) { |
| 162 | + $RestMethodParams['MaximumRetryCount'] = $MaximumRetryCount |
| 163 | + } |
| 164 | + |
| 165 | + if ($RetryIntervalSec) { |
| 166 | + $RestMethodParams['RetryIntervalSec'] = $RetryIntervalSec |
| 167 | + } |
| 168 | + |
| 169 | + if ($TimeoutSec) { |
| 170 | + $RestMethodParams['TimeoutSec'] = $TimeoutSec |
| 171 | + } |
| 172 | + |
| 173 | + if ($UseBasicParsing) { |
| 174 | + $RestMethodParams['UseBasicParsing'] = $true |
| 175 | + } |
| 176 | + |
| 177 | + if ($WebSession) { |
| 178 | + $RestMethodParams['WebSession'] = $WebSession |
| 179 | + } |
| 180 | + |
| 181 | + # Invoke the REST method |
| 182 | + try { |
| 183 | + $Response = Invoke-RestMethod @RestMethodParams |
| 184 | + |
| 185 | + # For compatibility with Invoke-AzRestMethod behavior, return object with Content property if response is a string |
| 186 | + # Otherwise return the parsed object directly |
| 187 | + if ($Response -is [string]) { |
| 188 | + return [PSCustomObject]@{ |
| 189 | + Content = $Response |
| 190 | + } |
| 191 | + } |
| 192 | + |
| 193 | + return $Response |
| 194 | + } catch { |
| 195 | + $errorMessage = "Azure REST API call failed: $($_.Exception.Message)" |
| 196 | + if ($_.Exception.Response) { |
| 197 | + try { |
| 198 | + $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) |
| 199 | + $responseBody = $reader.ReadToEnd() |
| 200 | + $reader.Close() |
| 201 | + $errorMessage += "`nResponse: $responseBody" |
| 202 | + } catch { |
| 203 | + # Ignore errors reading response stream |
| 204 | + } |
| 205 | + } |
| 206 | + |
| 207 | + Write-Error -Message $errorMessage -ErrorAction $ErrorActionPreference |
| 208 | + return |
| 209 | + } |
| 210 | +} |
0 commit comments