GraphApiHelper.psm1
|
#region Public commands function Add-GraphLargeFile { <# .SYNOPSIS Uploads large files to Microsoft Graph using the resumable upload protocol .DESCRIPTION Uploads large files to Microsoft Graph (OneDrive, SharePoint, etc.) using the resumable upload session API. This function handles files of any size by splitting them into chunks and uploading them sequentially. The upload uses 5MB chunks (320KB * 16) which is optimal for Graph API uploads and supports resumable uploads in case of network interruptions. The function automatically creates an upload session and manages the chunked upload process. .PARAMETER LocalFilePath The full path to the local file to upload. The file must exist and be readable. .PARAMETER GraphFilePath The Microsoft Graph API path where the file should be uploaded, excluding the ':/createUploadSession' suffix. Example: 'https://graph.microsoft.com/v1.0/me/drive/root:/Documents/myfile.pdf' .EXAMPLE Add-GraphLargeFile -LocalFilePath 'C:\Files\presentation.pptx' -GraphFilePath 'https://graph.microsoft.com/v1.0/me/drive/root:/Documents/presentation.pptx' Uploads a PowerPoint file to the current user's OneDrive Documents folder. .EXAMPLE Add-GraphLargeFile -LocalFilePath 'C:\Videos\training.mp4' -GraphFilePath 'https://graph.microsoft.com/v1.0/sites/{site-id}/drive/root:/Videos/training.mp4' -Verbose Uploads a video file to a SharePoint site's Videos folder with verbose output showing upload progress. .OUTPUTS None The function streams upload chunk requests and does not emit the final driveItem object. .INPUTS None This command does not accept pipeline input. .NOTES - Uses 5MB chunks for optimal performance - Automatically handles upload session creation - Supports conflict behavior of 'replace' - existing files will be overwritten - Uses Invoke-GraphWithRetry internally for reliability - Enable -Verbose to see detailed upload progress - Uses the authentication factory configured via Set-GraphAadFactory .LINK https://learn.microsoft.com/en-us/graph/api/driveitem-createuploadsession #> [CmdletBinding()] param ( [Parameter(Mandatory)] $LocalFilePath, [Parameter(Mandatory)] $GraphFilePath ) begin { $chunkSize = 320KB * 16 # 5MB chunks $graphUri = New-GraphUri -Uri "$GraphFilePath" } process { $item = Get-Item -Path $LocalFilePath $fileSize = $item.length $fileStream = [System.IO.File]::OpenRead($item.FullName) Write-Verbose "Filesize: $fileSize" Write-Verbose "Chunksize: $chunkSize" try { $payload = @{ item = @{ '@microsoft.graph.conflictBehavior' = 'replace' } } Write-Verbose "Requesting upload session on $graphUri`:/createUploadSession" $uploadSession = Invoke-GraphWithRetry ` -RequestUri "$graphUri`:/createUploadSession" ` -method Post ` -body ($payload | ConvertTo-Json -Depth 10) ` -ContentType 'application/json' ` -ErrorAction Stop $uploadUrl = $uploadSession.uploadUrl Write-Verbose "UploadUrl: $uploadUrl" $offset = 0 while ($offset -lt $fileSize) { $bytesToRead = [Math]::Min($chunkSize, $fileSize - $offset) $buffer = New-Object byte[] $bytesToRead $bytesRead = $fileStream.Read($buffer, 0, $bytesToRead) if ($bytesRead -gt 0) { $contentRange = "bytes $offset-$($offset + $bytesRead - 1)/$fileSize" Write-Verbose "Writing range: $contentRange" Invoke-GraphWithRetry ` -RequestUri $uploadUrl ` -method Put ` -body $buffer ` -headers @{ 'Content-Range' = $contentRange } ` -ContentType 'application/octet-stream' | out-null $offset += $bytesRead } } } finally { $fileStream.Close() } } } function Get-GraphAuthorizationHeader { <# .SYNOPSIS Retrieves an authorization header for Microsoft Graph API calls .DESCRIPTION Obtains an access token from the configured AAD authentication factory with the Graph API scope and returns it as a hashtable containing the Authorization header. This command can be called directly but is primarily used by other module functions. .PARAMETER FactoryName Optional factory name override used to obtain the token. If omitted, the factory configured by Set-GraphAadFactory is used. .INPUTS None This command does not accept pipeline input. .OUTPUTS System.Collections.Hashtable Returns a hashtable with the Authorization header containing the Bearer token. .EXAMPLE $authHeader = Get-GraphAuthorizationHeader Retrieves the authorization header for Graph API calls. .EXAMPLE $authHeader = Get-GraphAuthorizationHeader -FactoryName 'ManagedIdentityFactory' Retrieves the authorization header by explicitly selecting a token factory. .NOTES This function uses the scopes configured via Set-GraphScopes and the factory configured via Set-GraphAadFactory. #> param ( $FactoryName = $script:graphConnection.FactoryName ) process { Get-AadToken -Factory $FactoryName -Scope $script:graphConnection.GraphScope -AsHashTable } } function Get-GraphData { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] <# .SYNOPSIS Retrieves data from Microsoft Graph API with automatic pagination .DESCRIPTION Executes a Microsoft Graph API GET request and automatically handles pagination by following @odata.nextLink references. This function retrieves all pages of data and returns the complete dataset. It uses Invoke-GraphWithRetry internally, so it inherits automatic retry logic for throttling. The function intelligently handles both single objects and arrays of results from the Graph API. .PARAMETER RequestUri The complete Microsoft Graph API request URL including query parameters. Example: 'https://graph.microsoft.com/v1.0/users' .PARAMETER WithSelect Optional value for the $select query option. Example: 'id,displayName,userPrincipalName' .PARAMETER WithFilter Optional value for the $filter query option. Example: "accountEnabled eq true" .PARAMETER WithCount Adds $count=true to the request. .PARAMETER WithExpand Optional value for the $expand query option. .PARAMETER WithSearch Optional value for the $search query option. .PARAMETER Top Optional value for the $top query option. .PARAMETER Skip Optional value for the $skip query option. .PARAMETER OperationName The operation name to use for Application Insights logging. Default is 'Get-GraphData'. .PARAMETER AdditionalHeaders Additional HTTP headers to include in requests (for example ConsistencyLevel for advanced queries). .PARAMETER NoContinue When specified, retrieves only the first page and does not follow @odata.nextLink. .INPUTS None This command does not accept pipeline input. .OUTPUTS System.Object[] Returns all objects from the Graph API response, automatically handling pagination. .EXAMPLE Get-GraphData -RequestUri 'https://graph.microsoft.com/v1.0/users' Retrieves all users from Microsoft Graph, automatically paginating through all result pages. .EXAMPLE Get-GraphData -RequestUri 'https://graph.microsoft.com/v1.0/groups?$filter=startswith(displayName,''Sales'')' Retrieves all groups whose display name starts with 'Sales', handling pagination automatically. .EXAMPLE Get-GraphData -RequestUri 'https://graph.microsoft.com/v1.0/me/messages?$top=50' -OperationName 'GetUserMessages' Retrieves all messages for the current user with custom operation name for Application Insights tracking. .EXAMPLE Get-GraphData -RequestUri 'https://graph.microsoft.com/v1.0/users' -WithSelect 'id,displayName' -WithFilter "startswith(displayName,'A')" -WithCount -AdditionalHeaders @{ ConsistencyLevel = 'eventual' } Retrieves users with query options built from parameters and the required advanced query header. .EXAMPLE Get-GraphData -RequestUri 'https://graph.microsoft.com/v1.0/users' -WhatIf Shows what request would be executed without calling Microsoft Graph. .NOTES - Automatically handles pagination via @odata.nextLink - Uses Invoke-GraphWithRetry internally for throttling protection - Suitable for large datasets that span multiple pages - Uses the authentication factory configured via Set-GraphAadFactory - Uses the Graph scopes configured via Set-GraphScopes - Supports -WhatIf and -Confirm via ShouldProcess #> param ( [Parameter(Mandatory)] [Alias('Uri')] [string]$RequestUri, [Parameter()] [string]$WithSelect, [Parameter()] [string]$WithFilter, [Parameter()] [switch]$WithCount, [Parameter()] [string]$WithExpand, [Parameter()] [string]$WithSearch, [Parameter()] [Nullable[int]]$Top, [Parameter()] [Nullable[int]]$Skip, [Parameter()] [string]$OperationName = 'Get-GraphData', [Parameter()] [System.Collections.Hashtable]$AdditionalHeaders = @{}, [Parameter()] [switch]$NoContinue ) process { $uri = New-GraphUri -Uri $RequestUri -WithSelect $WithSelect -WithFilter $WithFilter -WithCount:$WithCount -WithExpand $WithExpand -WithSearch $WithSearch -Top $Top -Skip $Skip if (-not $PSCmdlet.ShouldProcess($uri, 'Get Microsoft Graph data with automatic pagination')) { return } while($true) { try { #get page of results $result = Invoke-GraphWithRetry -RequestUri $uri -method Get -Headers $AdditionalHeaders -OperationName $OperationName -Confirm:$false -ErrorAction Stop if($null -ne $result.value) { #returning array of results $result.value } else { #returning single object $result } $uri = $result.'@odata.nextLink' if([string]::IsNullOrEmpty($uri) -or $NoContinue) { #no more pages or we just wanted first page break; } } catch { $err = $_ $shouldContinue = $false switch($ErrorActionPreference) { 'Stop' { throw } 'Continue' { Write-Error "Could not retrieve data for uri: $uri. Error: $($err.Exception.Message)" $shouldContinue = $false break } 'SilentlyContinue' { Write-Warning "Could not retrieve data for uri: $uri. Error: $($err.Exception.Message)" $shouldContinue = $false break } 'Inquire' { $response = $PSCmdlet.ShouldContinue("Error retrieving data for uri: $uri. Do you want to continue?", "Error: $($err.Exception.Message)") if(-not $response) { $shouldContinue = $false break } } } if(-not $shouldContinue) { break } } } } } function Invoke-GraphBatch { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] <# .SYNOPSIS Sends a Microsoft Graph batch request. .DESCRIPTION Collects one or more batch request definitions, builds the Graph $batch payload, sends it through Invoke-GraphWithRetry, and returns batch response items. .PARAMETER BatchRequest One or more Graph batch request objects created by New-GraphBatchRequest. .PARAMETER RequestHeaders Additional HTTP headers for the outer $batch request. .PARAMETER OperationName The operation name to use for Application Insights logging. Default is 'Invoke-GraphBatch'. .OUTPUTS System.Object[] Returns response items from the Graph batch response. .EXAMPLE $requests = @( New-GraphBatchRequest -Id '1' -Method GET -Url '/me' New-GraphBatchRequest -Id '2' -Method GET -Url (New-GraphUri -Uri '/users' -Top 5 -Relative) New-GraphBatchRequest -Id '3' -Method POST -Url '/groups' -Body @{ displayName = 'Batch Group'; mailEnabled = $false; mailNickname = 'batch-group'; securityEnabled = $true } ) Invoke-GraphBatch -BatchRequest $requests Sends three Graph API requests in one batch and returns the response items. Use New-GraphUri with -Relative to build query strings cleanly. .INPUTS System.Management.Automation.PSCustomObject[] Accepts batch request objects from the pipeline. .EXAMPLE @( New-GraphBatchRequest -Id '1' -Method GET -Url '/me' New-GraphBatchRequest -Id '2' -Method GET -Url '/organization' ) | Invoke-GraphBatch Sends request definitions from the pipeline. .NOTES - Uses Invoke-GraphWithRetry internally for reliability. - Sends to the /$batch endpoint under the configured BaseUri. - Microsoft Graph batch requests support up to 20 subrequests per batch. #> param ( [Parameter(Mandatory, ValueFromPipeline)] [Alias('Requests')] [PSCustomObject[]]$BatchRequest, [Parameter()] [System.Collections.Hashtable]$RequestHeaders = @{}, [Parameter()] [string]$OperationName = 'Invoke-GraphBatch' ) begin { $requests = [System.Collections.Generic.List[hashtable]]::new() $ids = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) } process { foreach ($item in $BatchRequest) { if ($null -eq $item) { continue } $propertyNames = $item.PSObject.Properties.Name if ('id' -notin $propertyNames -or 'method' -notin $propertyNames -or 'url' -notin $propertyNames) { throw 'Each batch request must include id, method, and url properties. Use New-GraphBatchRequest to create requests.' } $id = [string]$item.id $method = [string]$item.method $url = [string]$item.url if ([string]::IsNullOrWhiteSpace($id) -or [string]::IsNullOrWhiteSpace($method) -or [string]::IsNullOrWhiteSpace($url)) { throw 'Each batch request must include non-empty id, method, and url values.' } if (-not $ids.Add($id)) { throw "Duplicate batch request id '$id' is not allowed." } $normalizedRequest = [ordered]@{ id = [string]$id method = [string]$method.ToUpperInvariant() url = [string]$url } $headers = @{} $providedHeaders = $item.headers if ($null -ne $providedHeaders) { foreach ($key in $providedHeaders.Keys) { $headers[$key] = $providedHeaders[$key] } } $bodyWasProvided = $false if ('body' -in $propertyNames) { $normalizedRequest.body = $item.body $bodyWasProvided = $true } if ($bodyWasProvided -and -not $headers.ContainsKey('Content-Type')) { $headers['Content-Type'] = 'application/json' } if ($headers.Count -gt 0) { $normalizedRequest.headers = $headers } if ('dependsOn' -in $propertyNames -and $null -ne $item.dependsOn -and $item.dependsOn.Count -gt 0) { $normalizedRequest.dependsOn = $item.dependsOn } [void]$requests.Add($normalizedRequest) } } end { if ($requests.Count -eq 0) { Write-Warning 'No batch requests were provided.' return } if ($requests.Count -gt 20) { throw "Microsoft Graph batch requests support a maximum of 20 subrequests per batch. Received $($requests.Count)." } $batchUri = New-GraphUri -Uri '/$batch' if (-not $PSCmdlet.ShouldProcess($batchUri, "Post Microsoft Graph batch request with $($requests.Count) subrequests")) { return } $payload = @{ requests = $requests } $result = Invoke-GraphWithRetry ` -RequestUri $batchUri ` -Method Post ` -Body ($payload | ConvertTo-Json -Depth 20) ` -ContentType 'application/json' ` -Headers $RequestHeaders ` -OperationName $OperationName ` -Confirm:$false if ($null -ne $result.responses) { $result.responses } else { $result } } } function Invoke-GraphWithRetry { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] <# .SYNOPSIS Invokes a Graph API with automatic retry logic for throttling .DESCRIPTION Executes a Microsoft Graph API request with built-in retry logic to handle HTTP 429 (Too Many Requests) throttling responses. The function will automatically retry up to 100 times with exponential backoff when throttled. If the request returns paged results, it retrieves only a single page - callers should use Get-GraphData for automatic pagination. Supports Application Insights logging when an AILogger instance is provided when importing the module .PARAMETER RequestUri The complete Microsoft Graph API request URL including query parameters. Example: 'https://graph.microsoft.com/v1.0/users?$top=10' .PARAMETER Method The HTTP method to use for the request. Valid values are: Get, Post, Put, Patch, Delete. Default is 'Get'. .PARAMETER Body The request body for Post, Put, or Patch requests. Can be a string or object that will be sent with the request. .PARAMETER ContentType The content type for the request body. Default is 'application/json'. .PARAMETER Headers Additional HTTP headers to include in the request. The Authorization header will be automatically added. .PARAMETER OperationName The operation name to use for Application Insights logging. Default is 'Invoke-GraphWithRetry'. .PARAMETER RetryableErrorCodes HTTP status codes that should trigger retries. Default is 429. .PARAMETER MaxRetries Maximum number of retry attempts before the error is thrown. Default is 100. .PARAMETER DefaultBackOffSeconds Fallback delay in seconds used when the response does not include Retry-After. .INPUTS None This command does not accept pipeline input. .OUTPUTS System.Object Returns the response from the Graph API call. .EXAMPLE Invoke-GraphWithRetry -RequestUri 'https://graph.microsoft.com/v1.0/users' Retrieves users from Microsoft Graph using the default GET method. .EXAMPLE $body = @{ displayName = 'Test Group' } | ConvertTo-Json Invoke-GraphWithRetry -RequestUri 'https://graph.microsoft.com/v1.0/groups' -Method Post -Body $body Creates a new group in Microsoft Graph. .EXAMPLE Invoke-GraphWithRetry -RequestUri 'https://graph.microsoft.com/v1.0/users/user@domain.com' -Method Delete Deletes a user from Microsoft Graph. .EXAMPLE Invoke-GraphWithRetry -RequestUri 'https://graph.microsoft.com/v1.0/users/user@domain.com' -Method Delete -WhatIf Shows what delete request would run without calling Microsoft Graph. .NOTES - Automatically handles HTTP 429 throttling with exponential backoff - Maximum retry attempts: 100 - Uses the authentication factory configured via Set-GraphAadFactory - Uses the Graph scopes configured via Set-GraphScopes - Supports Application Insights telemetry when configured - Supports -WhatIf and -Confirm via ShouldProcess #> param ( [Parameter(Mandatory)] [Alias('Uri')] [string]$RequestUri, [Parameter()] [ValidateSet('Get', 'Post', 'Put', 'Patch', 'Delete')] $method = 'Get', [Parameter()] $Body, [Parameter()] $ContentType = 'application/json', [parameter()] [System.Collections.Hashtable] $Headers = @{}, [Parameter()] $OperationName = 'Invoke-GraphWithRetry', [Parameter()] [int[]]$RetryableErrorCodes = @(429), [Parameter()] [int]$MaxRetries = 100, [Parameter()] [int]$DefaultBackOffSeconds = 1 ) begin { $retries = 0 $graphUri = New-GraphUri -Uri $RequestUri } process { if (-not $PSCmdlet.ShouldProcess($graphUri, "$method Microsoft Graph request")) { return } do { $authHeader = Get-GraphAuthorizationHeader Write-Verbose "Invoking Graph API: $graphUri with method $method. Attempt #$($retries + 1)" $headers['Authorization'] = $authHeader['Authorization'] $resultCode = 'Ok' try { $requestStart = [DateTime]::UtcNow switch($method) { {$_ -in @('Get', 'Delete')} { $result = Invoke-RestMethod -method $method -Uri $graphUri -headers $headers -ErrorAction Stop -Verbose:$VerbosePreference break; } {$_ -in @('Post', 'Patch', 'Put')} { $result = Invoke-RestMethod -method $method -Uri $graphUri -body $body -headers $headers -ContentType $contentType -ErrorAction Stop -Verbose:$VerbosePreference break; } } if($script:graphConnection.AiLogger) { Write-AiDependency -Target 'graph.microsoft.com' -DependencyType 'Graph API' -Name $OperationName -Data $graphUri -Start $requestStart -ResultCode 'Ok' -Success $true -Connection $script:graphConnection.AiLogger } $result break; #do-while } catch { $err = $_ if($null -ne $script:graphConnection.AiLogger) { Write-AiException -Exception $err.Exception -Connection $script:graphConnection.AiLogger } if($null -ne $err.exception.Response.StatusCode) { $resultCode = $err.exception.Response.StatusCode } else { $resultCode = 'Unknown' } if($retries -le $MaxRetries -and ($err.exception.Response.StatusCode -in $RetryableErrorCodes)) { $retries++ switch($err.exception.Response.StatusCode) { 429 { $retryAfter = $err.exception.Response.Headers['Retry-After'] if($null -eq $retryAfter) { $retryAfter = $DefaultBackOffSeconds * $retries } $waitTime = [int]$retryAfter break; } default { $waitTime = $DefaultBackOffSeconds * $retries break; } } Write-Warning "Retrying because of status code $($err.exception.Response.StatusCode) for $waitTime secs" start-sleep -Seconds $waitTime } else { throw } } finally { if($null -ne $script:graphConnection.AiLogger) { Write-AiDependency -Target 'graph.microsoft.com' -DependencyType 'Graph API' -Name $OperationName -Data $graphUri -Start $requestStart -ResultCode $resultCode -Success ($resultCode -eq 'Ok') -Connection $script:graphConnection.AiLogger } } }while($true) } } function New-GraphBatchRequest { <# .SYNOPSIS Creates a Microsoft Graph batch request item. .DESCRIPTION Builds a normalized request object suitable for Invoke-GraphBatch and Graph /$batch payloads. .PARAMETER Method HTTP method for the subrequest. Allowed values: GET, POST, PUT, PATCH, DELETE. .PARAMETER Url Relative Graph URL for the subrequest. Example: '/me' or '/users?$top=5'. .PARAMETER Id Request identifier. This value is required by Graph batch requests. .PARAMETER Headers Optional headers for this subrequest. When Body is provided and Content-Type is not set, application/json is used. .PARAMETER Body Optional body for POST, PUT, or PATCH requests. .PARAMETER DependsOn Optional list of request IDs this request depends on. .INPUTS None This command does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject Returns a batch request object. .EXAMPLE New-GraphBatchRequest -Method GET -Url '/me' -Id '1' Creates a batch request item that gets the signed-in user profile. .EXAMPLE New-GraphBatchRequest -Id '2' -Method PATCH -Url '/users/john.doe@contoso.com' -Body @{ jobTitle = 'Principal Engineer' } Creates a batch request item that updates a user. .NOTES Use this command together with Invoke-GraphBatch to send multiple Graph requests in a single round-trip. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateSet('GET', 'POST', 'PUT', 'PATCH', 'DELETE')] [string]$Method, [Parameter(Mandatory)] [string]$Url, [Parameter(Mandatory)] [string]$Id, [Parameter()] [System.Collections.Hashtable]$Headers, [Parameter()] [AllowNull()] $Body, [Parameter()] [string[]]$DependsOn ) process { if ([string]::IsNullOrWhiteSpace($Url)) { throw 'Url cannot be empty.' } if ($Url.StartsWith('http', [System.StringComparison]::OrdinalIgnoreCase)) { throw 'Url must be a relative Graph path for batch requests (for example /me or /users?$top=5).' } $request = [ordered]@{ method = $Method.ToUpperInvariant() url = $Url } if ([string]::IsNullOrWhiteSpace($Id)) { throw 'Id cannot be empty.' } $request.id = $Id $resolvedHeaders = @{} if ($null -ne $Headers) { foreach ($key in $Headers.Keys) { $resolvedHeaders[$key] = $Headers[$key] } } if ($PSBoundParameters.ContainsKey('Body')) { $request.body = $Body if (-not $resolvedHeaders.ContainsKey('Content-Type')) { $resolvedHeaders['Content-Type'] = 'application/json' } } if ($resolvedHeaders.Count -gt 0) { $request.headers = $resolvedHeaders } if ($null -ne $DependsOn -and $DependsOn.Count -gt 0) { $request.dependsOn = $DependsOn } $requestObject = [PSCustomObject]$request $requestObject.PSTypeNames.Insert(0, 'GraphApiHelper.GraphBatchRequest') $requestObject } } function New-GraphUri { <# .SYNOPSIS Builds a Microsoft Graph request URL. .DESCRIPTION Returns a Microsoft Graph request URL using the same query option parameters as Get-GraphData, without sending a request. The command can build either absolute URLs (using the configured BaseUri) or relative paths (for example for Graph batch subrequests). Query options are appended to existing query strings and $search values are normalized for Graph search syntax. .PARAMETER Uri The base Microsoft Graph request URL or relative path. When an absolute URL is provided with -Relative, only PathAndQuery is returned. .PARAMETER WithSelect Optional value for the $select query option. .PARAMETER WithFilter Optional value for the $filter query option. .PARAMETER WithCount Adds $count=true to the request. .PARAMETER WithExpand Optional value for the $expand query option. .PARAMETER WithSearch Optional value for the $search query option. If the value does not start with '(' or '"', the value is automatically wrapped in double quotes. .PARAMETER Top Optional value for the $top query option. .PARAMETER Skip Optional value for the $skip query option. .PARAMETER Relative Returns a relative Graph path instead of prepending the configured BaseUri. Use this when building batch request URLs. If Uri is absolute, the host and scheme are removed. .INPUTS None This command does not accept pipeline input. .OUTPUTS System.String Returns the fully constructed request URL or relative Graph path. .EXAMPLE New-GraphUri -Uri '/users' -Top 25 Returns an absolute Graph URL for /users with the $top query option. .EXAMPLE New-GraphUri -Uri '/users' -WithSelect 'id,displayName' -WithFilter "accountEnabled eq true" Returns a URL with $select and $filter query options. .EXAMPLE New-GraphUri -Uri '/users' -WithSearch '"displayName:alex"' -WithCount Returns a URL with $search and $count query options. .EXAMPLE New-GraphUri -Uri '/users' -WithSearch 'displayName:alex' Returns a URL where the search value is automatically quoted. .EXAMPLE New-GraphUri -Uri 'https://graph.microsoft.com/v1.0/users' -Relative Returns the relative path '/v1.0/users'. .EXAMPLE New-GraphUri -Uri '/users?$orderby=displayName' -Top 10 Returns a URL that preserves existing query parameters and appends new ones using '&'. .EXAMPLE New-GraphUri -Uri '/users' -Top 5 -Relative Returns a relative path intended for batch subrequest URLs. .NOTES If a relative Uri is provided and -Relative is not used, Set-GraphBaseUri must be configured first. Values are not URL-encoded by this function. Pass already encoded values when required. .LINK https://learn.microsoft.com/graph/query-parameters #> [CmdletBinding()] param ( [Parameter(Mandatory)] [string]$Uri, [Parameter()] [string]$WithSelect, [Parameter()] [string]$WithFilter, [Parameter()] [switch]$WithCount, [Parameter()] [string]$WithExpand, [Parameter()] [string]$WithSearch, [Parameter()] [Nullable[int]]$Top, [Parameter()] [Nullable[int]]$Skip, [Parameter()] [switch]$Relative ) process { if ($Uri.StartsWith('http')) { if ($Relative) { # Extract relative path from absolute URI $parsedUri = [System.Uri]::new($Uri) $Uri = $parsedUri.PathAndQuery if ([string]::IsNullOrEmpty($Uri)) { $Uri = '/' } } # else: Uri is already absolute, use as-is } else { # Uri is relative if (-not $Relative) { # Prepend BaseUri if(-not $script:graphConnection.BaseUri) { throw "BaseUri is not set. Please call Set-GraphBaseUri first or provide a full Uri" } $Uri = "$($script:graphConnection.BaseUri.TrimEnd('/'))/$($Uri.TrimStart('/'))" } # else: Uri is already relative, use as-is } $queryParams = [System.Collections.Generic.List[string]]::new() if(-not [string]::IsNullOrWhiteSpace($WithSelect)) { $queryParams.Add("`$select=$($WithSelect.Trim())") } if(-not [string]::IsNullOrWhiteSpace($WithFilter)) { $queryParams.Add("`$filter=$($WithFilter.Trim())") } if($WithCount) { $queryParams.Add('$count=true') } if(-not [string]::IsNullOrWhiteSpace($WithExpand)) { $queryParams.Add("`$expand=$($WithExpand.Trim())") } if(-not [string]::IsNullOrWhiteSpace($WithSearch)) { $clause = $WithSearch.Trim() if(-not ($clause.StartsWith('(')) -and -not ($clause.StartsWith('"'))) { $clause = "`"$clause`"" } $queryParams.Add("`$search=$clause") } if($null -ne $Top) { $queryParams.Add("`$top=$Top") } if($null -ne $Skip) { $queryParams.Add("`$skip=$Skip") } if($queryParams.Count -gt 0) { $separator = if($Uri.Contains('?')) { '&' } else { '?' } $Uri = $Uri + $separator + ($queryParams -join '&') } return $Uri } } function Set-GraphAadFactory { <# .SYNOPSIS Sets the AAD authentication factory for Graph API operations .DESCRIPTION Configures the authentication factory to be used for obtaining access tokens when making Graph API calls. The factory name corresponds to a factory registered with the AadAuthenticationFactory module. By default, the command validates that the factory exists before updating module state. .PARAMETER Name The name of the authentication factory to use. This should match a factory registered with AadAuthenticationFactory module. Common values include 'ManagedIdentityFactory' or custom factory names. .PARAMETER Force Skips validation that the specified factory exists and sets the value directly. .INPUTS None This command does not accept pipeline input. .OUTPUTS None This command updates module configuration and does not return an object. .EXAMPLE Set-GraphAadFactory -Name 'ManagedIdentityFactory' Configures the module to use managed identity for authentication. .EXAMPLE Set-GraphAadFactory -Name 'MyCustomFactory' Configures the module to use a custom authentication factory. .EXAMPLE Set-GraphAadFactory -Name 'FactoryRegisteredLater' -Force Sets the factory name without validating its current registration. .NOTES - When -Force is not specified, the command throws if the factory cannot be found. - The configured value is used by subsequent GraphApiHelper commands that request tokens. #> param ( [Parameter(Mandatory)] [string]$Name, [switch]$Force ) process { if($null -eq (Get-AadAuthenticationFactory -Name $Name) -and -not $Force) { throw "Authentication factory '$Name' not found. Please register it with the AadAuthenticationFactory module before using." } $script:graphConnection.FactoryName = $Name } } function Set-GraphAiLogger { <# .SYNOPSIS Sets the Application Insights logger for telemetry .DESCRIPTION Configures the Application Insights logger instance to be used for logging telemetry data during Graph API operations. .PARAMETER Logger The AILogger instance to use for logging. This should be created using the ApplicationInsights module. .INPUTS None This command does not accept pipeline input. .OUTPUTS None This command updates module configuration and does not return an object. .EXAMPLE $aiLogger = New-AiLogger -InstrumentationKey 'your-instrumentation-key' Set-GraphAiLogger -Logger $aiLogger Configures the module to use the specified Application Insights logger for telemetry. .NOTES Invoke-GraphWithRetry uses this logger for dependency and exception telemetry when configured. #> param ( [Parameter(Mandatory)] $Logger ) process { $script:graphConnection.AiLogger = $Logger } } function Set-GraphBaseUri { <# .SYNOPSIS Sets the base URI used for Microsoft Graph API requests. .DESCRIPTION Configures the base URI used to build absolute request URIs when a relative path is supplied to commands such as Invoke-GraphWithRetry and Get-GraphData. .PARAMETER BaseUri The base URI to use for Graph requests. Defaults to https://graph.microsoft.com/v1.0 when the module is imported. .INPUTS None This command does not accept pipeline input. .OUTPUTS None This command updates module configuration and does not return an object. .EXAMPLE Set-GraphBaseUri -BaseUri 'https://graph.microsoft.com/v1.0' Uses the global Microsoft Graph endpoint. .EXAMPLE Set-GraphBaseUri -BaseUri 'https://graph.microsoft.us/v1.0' Uses the Microsoft Graph US Government endpoint. .NOTES This value is used when commands receive relative Uri values, for example in New-GraphUri and Invoke-GraphWithRetry. #> param ( [Parameter(Mandatory)] [string]$BaseUri ) process { $script:graphConnection.BaseUri = $BaseUri } } function Set-GraphScopes { <# .SYNOPSIS Sets the scopes for Graph API authentication .DESCRIPTION Configures the scope to be used when requesting access tokens for Graph API calls. The default scope is 'https://graph.microsoft.com/.default' which uses the permissions assigned to the application in Azure AD. .PARAMETER Scopes The scopes to use when requesting access tokens. The default is 'https://graph.microsoft.com/.default'. .INPUTS None This command does not accept pipeline input. .OUTPUTS None This command updates module configuration and does not return an object. .EXAMPLE Set-GraphScopes -Scopes 'https://graph.microsoft.com/.default' Configures the module to use the default Graph API scope for authentication. .EXAMPLE Set-GraphScopes -Scopes 'https://graph.microsoft.com/User.Read' Configures the module to request a token with only User.Read permissions. .EXAMPLE Set-GraphScopes -Scopes @('https://graph.microsoft.com/User.Read', 'https://graph.microsoft.com/Mail.Read') Configures multiple delegated scopes for token acquisition. .NOTES The configured scopes are used by Get-GraphAuthorizationHeader when requesting tokens. #> param ( [Parameter()] [string[]]$Scopes = @('https://graph.microsoft.com/.default') ) process { $script:graphConnection.GraphScope = $Scopes } } #endregion Public commands #region Internal commands #endregion Internal commands #region Module initialization $script:graphConnection = [PSCustomObject]@{ FactoryName = 'graph' AiLogger = $null BaseUri = 'https://graph.microsoft.com/v1.0' GraphScope = @('https://graph.microsoft.com/.default') } #endregion Module initialization # SIG # Begin signature block # MIIuNAYJKoZIhvcNAQcCoIIuJTCCLiECAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCfZCKmSXqUsTTB # G+NpZ+TcPcz9X1AgaIfv099rS0hfoqCCE2AwggWQMIIDeKADAgECAhAFmxtXno4h # MuI5B72nd3VcMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV # BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0z # ODAxMTUxMjAwMDBaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB # AL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/z # G6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZ # anMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7s # Wxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL # 2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfb # BHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3 # JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3c # AORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqx # YxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0 # viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aL # T8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1Ud # EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzf # Lmc/57qYrhwPTzANBgkqhkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNk # aA9Wz3eucPn9mkqZucl4XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjS # PMFDQK4dUPVS/JA7u5iZaWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK # 7VB6fWIhCoDIc2bRoAVgX+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eB # cg3AFDLvMFkuruBx8lbkapdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp # 5aPNoiBB19GcZNnqJqGLFNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msg # dDDS4Dk0EIUhFQEI6FUy3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vri # RbgjU2wGb2dVf0a1TD9uKFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ7 # 9ARj6e/CVABRoIoqyc54zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5 # nLGbsQAe79APT0JsyQq87kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3 # i0objwG2J5VT6LaJbVu8aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0H # EEcRrYc9B9F1vM/zZn4wggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0G # CSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTla # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz # ODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C # 0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce # 2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0da # E6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6T # SXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoA # FdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7Oh # D26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM # 1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z # 8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05 # huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNY # mtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP # /2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0T # AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYD # VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG # A1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV # HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU # cnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATAN # BgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95Ry # sQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HL # IvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5Btf # Q/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnh # OE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIh # dXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV # 9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/j # wVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYH # Ki8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmC # XBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l # /aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZW # eE4wggcUMIIE/KADAgECAhAP9xCe9qf4ax3LBs7uih/sMA0GCSqGSIb3DQEBCwUA # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz # ODQgMjAyMSBDQTEwHhcNMjMxMTA4MDAwMDAwWhcNMjYxMDAxMjM1OTU5WjCBnDET # MBEGCysGAQQBgjc8AgEDEwJDWjEdMBsGA1UEDwwUUHJpdmF0ZSBPcmdhbml6YXRp # b24xETAPBgNVBAUTCDA0OTIzNjkzMQswCQYDVQQGEwJDWjEOMAwGA1UEBxMFUHJh # aGExGjAYBgNVBAoTEUdyZXlDb3JiZWwgcy5yLm8uMRowGAYDVQQDExFHcmV5Q29y # YmVsIHMuci5vLjCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAJ8t/Qga # dJKtGC7EqH4pmIU73fInH+j1scmVnrJtXL8tGlKzWZ7qlWDWOJBR3owF9CVqL4IX # BGImH8Miowj6RKKqhEe9UtxiH5ipV6msnzAjTFkwqR9vjfEm9vrU1JuXWvAWAfYx # qYg92oyCEBDQxpURpZmqAVSBy9U/ScDwE4NykZGzb0oYSPtzStd8RJvtUkc4126w # YKMbVe/kdY1mDbKO9DLfpbSIj3vghrH6XeHwEb7/jAVYI7Vl+jUyyqfmYHD7FldQ # X2fZfwvoGSibY1uWvvP0/vm0yd6uDbDjCDOTQW8Lxl5wvlXEf5ewn2oaPSoa6ov3 # 1XmnxL5iT8c1LM06JFCwfHS9e0NSyNr86IiKaxQO9/MANrYciTicObtD3cBcSRDO # pEUfhc4TvA5DQZaakSduVJWPdMhxQs9iWeYMOzh5NDTB3xAx8eLBn7Uj++hjI3FQ # WGEPw4Ew6WoDsJShU0HemlDJGTPW9EZSWHGdNFr1BxXEPb4F7DbjJZn33QIDAQAB # o4ICAjCCAf4wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYDVR0O # BBYEFP2yViJvcgO05qXIH6aJSXB/QcEhMD0GA1UdIAQ2MDQwMgYFZ4EMAQMwKTAn # BggrBgEFBQcCARYbaHR0cDovL3d3dy5kaWdpY2VydC5jb20vQ1BTMA4GA1UdDwEB # /wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBP # hk1odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2Rl # U2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2Ny # bDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0 # MDk2U0hBMzg0MjAyMUNBMS5jcmwwgZQGCCsGAQUFBwEBBIGHMIGEMCQGCCsGAQUF # BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wXAYIKwYBBQUHMAKGUGh0dHA6 # Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWdu # aW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3J0MAkGA1UdEwQCMAAwDQYJKoZIhvcN # AQELBQADggIBADCe9Fh40HN9RneCehz5MrBy4O9WYsYCMJ7qJ9DsBT+Hed98UOKB # k/XjgSLfsj5eZRHRmz3HzhGDK1PaRI+yIUVQx96a4qL7adktmrHex3fW39Iq+tPB # rHtiEIp9rwunATeZpk+876u0AXYD1VDRWCtkL8zwZU0oqL6U/mWEIXzkryCB5N3x # xtE54jMmW7MKi1+To4yQcrK3zQ394e2dr50L+aF2fgJ5mo1/YJvzyLLhigbqpoYG # U/gjZonhNJXUaYogpHSTgUaBRlIKZ5xCnrFfJlOsbkhex4QAcdkU6XC+XyYfEQka # 7ERwgxmEoRT3NlZ8/EbrQxJP4S1H8Z29M4D3L6rXNXXmv0IbfA9FQcqEco3Y3tRW # dgdcFEwJmYTo0mCZrYTJHgkKW8xDvQ5BJISAp/ydOX5tSa71ojx1/Kp7qizqjBN/ # W77jdqJ89N1y+N/SOiHOCH9NO5pDLsHpTWW/arvjZT0I8dVYkqK0V39rh95XELI+ # NwBZvV4AsKLirjrkZU3pwCz6O99VmPkBqp9TA5wl13NdTpDHuQ6QyVT7hbC8LF5p # z6x/xO/+tEGxG+1A31UTJPmkxhhUlR+NE3ZXiXhcG72CFHYUUvqwlThPkFYe4Ygf # j9ADmss08k0JhVU5rkbrC2h+549HPlFu/XOSIrps4SXzInjHPEYuBETzMYIaKjCC # GiYCAQEwfTBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4x # QTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQw # OTYgU0hBMzg0IDIwMjEgQ0ExAhAP9xCe9qf4ax3LBs7uih/sMA0GCWCGSAFlAwQC # AQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwG # CisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZI # hvcNAQkEMSIEIDfxtjgUf6BLaLQsZ/vJsD7Fh/Y+lU1pGpNF5qI27pY0MA0GCSqG # SIb3DQEBAQUABIIBgIWDSLjheLrh47B5WwwHwdz0oKWAulW6OITEbITCtEB6CYk6 # uLGIWJmmHPgOZcwj0S2Cga2P9eVsh8VvnYJLD48PwdJb7XcGMg4RnLuMToLRP4hT # 2lSOBpVuSqiopd8h6hN2LmEe6oePUJGaBzAPxbXdkDwuRvTwZzsS/vE1fyDorgo8 # MKPY8WX65zvISh49ZIlWeF9QYIlxKdtPKOUAwidDbg/zafPb4nPsxmGxwXLwnDSs # BQuA8GlIsWEd7wKZCza3rA1dgDNpMdsMCQxG7dJbaDyQqMyJAnSQl/MC8LuRg6pB # qSa/H7w6VSrAi17yDAKppa/44ulgd0lplPZU3nXk+rLjZunhtgQTNiPlVe7KsLvr # 97LoPaYKggP2UYvkxYEX6S3gKJ6bG1c43gr/cAi/IxMNiNslizRddpFoNQHwYac4 # 5H+ZFYHGUDqMRcwNoUhZN7JfmZGfZhpCvfXSi87qYn9o+Jwh7DeN3SY5xeR6/HQe # eQA7kRjuJKqL/610aqGCF3cwghdzBgorBgEEAYI3AwMBMYIXYzCCF18GCSqGSIb3 # DQEHAqCCF1AwghdMAgEDMQ8wDQYJYIZIAWUDBAIBBQAweAYLKoZIhvcNAQkQAQSg # aQRnMGUCAQEGCWCGSAGG/WwHATAxMA0GCWCGSAFlAwQCAQUABCBGH1mhSi6YPkWe # y7To73ON5RD9ON7CG3xH/HS2B+jp3gIRAPD3g4A+NtekL/yuKwzIOEYYDzIwMjYw # NTExMTAyOTU3WqCCEzowggbtMIIE1aADAgECAhAKgO8YS43xBYLRxHanlXRoMA0G # CSqGSIb3DQEBCwUAMGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg # SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcg # UlNBNDA5NiBTSEEyNTYgMjAyNSBDQTEwHhcNMjUwNjA0MDAwMDAwWhcNMzYwOTAz # MjM1OTU5WjBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4x # OzA5BgNVBAMTMkRpZ2lDZXJ0IFNIQTI1NiBSU0E0MDk2IFRpbWVzdGFtcCBSZXNw # b25kZXIgMjAyNSAxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0Eas # LRLGntDqrmBWsytXum9R/4ZwCgHfyjfMGUIwYzKomd8U1nH7C8Dr0cVMF3BsfAFI # 54um8+dnxk36+jx0Tb+k+87H9WPxNyFPJIDZHhAqlUPt281mHrBbZHqRK71Em3/h # CGC5KyyneqiZ7syvFXJ9A72wzHpkBaMUNg7MOLxI6E9RaUueHTQKWXymOtRwJXcr # cTTPPT2V1D/+cFllESviH8YjoPFvZSjKs3SKO1QNUdFd2adw44wDcKgH+JRJE5Qg # 0NP3yiSyi5MxgU6cehGHr7zou1znOM8odbkqoK+lJ25LCHBSai25CFyD23DZgPfD # rJJJK77epTwMP6eKA0kWa3osAe8fcpK40uhktzUd/Yk0xUvhDU6lvJukx7jphx40 # DQt82yepyekl4i0r8OEps/FNO4ahfvAk12hE5FVs9HVVWcO5J4dVmVzix4A77p3a # wLbr89A90/nWGjXMGn7FQhmSlIUDy9Z2hSgctaepZTd0ILIUbWuhKuAeNIeWrzHK # YueMJtItnj2Q+aTyLLKLM0MheP/9w6CtjuuVHJOVoIJ/DtpJRE7Ce7vMRHoRon4C # WIvuiNN1Lk9Y+xZ66lazs2kKFSTnnkrT3pXWETTJkhd76CIDBbTRofOsNyEhzZtC # GmnQigpFHti58CSmvEyJcAlDVcKacJ+A9/z7eacCAwEAAaOCAZUwggGRMAwGA1Ud # EwEB/wQCMAAwHQYDVR0OBBYEFOQ7/PIx7f391/ORcWMZUEPPYYzoMB8GA1UdIwQY # MBaAFO9vU0rp5AZ8esrikFb2L9RJ7MtOMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUB # Af8EDDAKBggrBgEFBQcDCDCBlQYIKwYBBQUHAQEEgYgwgYUwJAYIKwYBBQUHMAGG # GGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBdBggrBgEFBQcwAoZRaHR0cDovL2Nh # Y2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0VGltZVN0YW1waW5n # UlNBNDA5NlNIQTI1NjIwMjVDQTEuY3J0MF8GA1UdHwRYMFYwVKBSoFCGTmh0dHA6 # Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGlu # Z1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjAL # BglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIBAGUqrfEcJwS5rmBB7NEIRJ5j # QHIh+OT2Ik/bNYulCrVvhREafBYF0RkP2AGr181o2YWPoSHz9iZEN/FPsLSTwVQW # o2H62yGBvg7ouCODwrx6ULj6hYKqdT8wv2UV+Kbz/3ImZlJ7YXwBD9R0oU62Ptgx # Oao872bOySCILdBghQ/ZLcdC8cbUUO75ZSpbh1oipOhcUT8lD8QAGB9lctZTTOJM # 3pHfKBAEcxQFoHlt2s9sXoxFizTeHihsQyfFg5fxUFEp7W42fNBVN4ueLaceRf9C # q9ec1v5iQMWTFQa0xNqItH3CPFTG7aEQJmmrJTV3Qhtfparz+BW60OiMEgV5GWoB # y4RVPRwqxv7Mk0Sy4QHs7v9y69NBqycz0BZwhB9WOfOu/CIJnzkQTwtSSpGGhLdj # nQ4eBpjtP+XB3pQCtv4E5UCSDag6+iX8MmB10nfldPF9SVD7weCC3yXZi/uuhqdw # kgVxuiMFzGVFwYbQsiGnoa9F5AaAyBjFBtXVLcKtapnMG3VH3EmAp/jsJ3FVF3+d # 1SVDTmjFjLbNFZUWMXuZyvgLfgyPehwJVxwC+UpX2MSey2ueIu9THFVkT+um1vsh # ETaWyQo8gmBto/m3acaP9QsuLj3FNwFlTxq25+T4QwX9xa6ILs84ZPvmpovq90K8 # eWyG2N01c4IhSOxqt81nMIIGtDCCBJygAwIBAgIQDcesVwX/IZkuQEMiDDpJhjAN # BgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQg # SW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2Vy # dCBUcnVzdGVkIFJvb3QgRzQwHhcNMjUwNTA3MDAwMDAwWhcNMzgwMTE0MjM1OTU5 # WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNV # BAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hB # MjU2IDIwMjUgQ0ExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtHgx # 0wqYQXK+PEbAHKx126NGaHS0URedTa2NDZS1mZaDLFTtQ2oRjzUXMmxCqvkbsDpz # 4aH+qbxeLho8I6jY3xL1IusLopuW2qftJYJaDNs1+JH7Z+QdSKWM06qchUP+AbdJ # gMQB3h2DZ0Mal5kYp77jYMVQXSZH++0trj6Ao+xh/AS7sQRuQL37QXbDhAktVJMQ # bzIBHYJBYgzWIjk8eDrYhXDEpKk7RdoX0M980EpLtlrNyHw0Xm+nt5pnYJU3Gmq6 # bNMI1I7Gb5IBZK4ivbVCiZv7PNBYqHEpNVWC2ZQ8BbfnFRQVESYOszFI2Wv82wnJ # RfN20VRS3hpLgIR4hjzL0hpoYGk81coWJ+KdPvMvaB0WkE/2qHxJ0ucS638ZxqU1 # 4lDnki7CcoKCz6eum5A19WZQHkqUJfdkDjHkccpL6uoG8pbF0LJAQQZxst7VvwDD # jAmSFTUms+wV/FbWBqi7fTJnjq3hj0XbQcd8hjj/q8d6ylgxCZSKi17yVp2NL+cn # T6Toy+rN+nM8M7LnLqCrO2JP3oW//1sfuZDKiDEb1AQ8es9Xr/u6bDTnYCTKIsDq # 1BtmXUqEG1NqzJKS4kOmxkYp2WyODi7vQTCBZtVFJfVZ3j7OgWmnhFr4yUozZtqg # PrHRVHhGNKlYzyjlroPxul+bgIspzOwbtmsgY1MCAwEAAaOCAV0wggFZMBIGA1Ud # EwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFO9vU0rp5AZ8esrikFb2L9RJ7MtOMB8G # A1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjAT # BgNVHSUEDDAKBggrBgEFBQcDCDB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGG # GGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2Nh # Y2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYD # VR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0 # VHJ1c3RlZFJvb3RHNC5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9 # bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQAXzvsWgBz+Bz0RdnEwvb4LyLU0pn/N0IfF # iBowf0/Dm1wGc/Do7oVMY2mhXZXjDNJQa8j00DNqhCT3t+s8G0iP5kvN2n7Jd2E4 # /iEIUBO41P5F448rSYJ59Ib61eoalhnd6ywFLerycvZTAz40y8S4F3/a+Z1jEMK/ # DMm/axFSgoR8n6c3nuZB9BfBwAQYK9FHaoq2e26MHvVY9gCDA/JYsq7pGdogP8HR # trYfctSLANEBfHU16r3J05qX3kId+ZOczgj5kjatVB+NdADVZKON/gnZruMvNYY2 # o1f4MXRJDMdTSlOLh0HCn2cQLwQCqjFbqrXuvTPSegOOzr4EWj7PtspIHBldNE2K # 9i697cvaiIo2p61Ed2p8xMJb82Yosn0z4y25xUbI7GIN/TpVfHIqQ6Ku/qjTY6hc # 3hsXMrS+U0yy+GWqAXam4ToWd2UQ1KYT70kZjE4YtL8Pbzg0c1ugMZyZZd/BdHLi # Ru7hAWE6bTEm4XYRkA6Tl4KSFLFk43esaUeqGkH/wyW4N7OigizwJWeukcyIPbAv # jSabnf7+Pu0VrFgoiovRDiyx3zEdmcif/sYQsfch28bZeUz2rtY/9TCA6TD8dC3J # E3rYkrhLULy7Dc90G6e8BlqmyIjlgp2+VqsS9/wQD7yFylIz0scmbKvFoW2jNrbM # 1pD2T7m3XDCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZIhvcN # AQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcG # A1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJl # ZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVowYjEL # MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 # LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0 # MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjwwIjBp # M+zCpyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J58soR # 0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMHhOZ0 # O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6Zu53 # yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQecN4 # x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4bA3Vd # eGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9WV1C # doeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCUtNJh # besz2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvoZKYz # 0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/JvNNB # ERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCPorF+ # CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMBAf8w # HQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXroq/0 # ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRtMGsw # JAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcw # AoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElE # Um9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdpY2Vy # dC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgwBgYE # VR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cHvZqs # oYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8UgPI # TtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTnf+hZ # qPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxUjG/v # oVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8jLfR+ # cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDGCA3wwggN4 # AgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEw # PwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IFRpbWVTdGFtcGluZyBSU0E0MDk2 # IFNIQTI1NiAyMDI1IENBMQIQCoDvGEuN8QWC0cR2p5V0aDANBglghkgBZQMEAgEF # AKCB0TAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8X # DTI2MDUxMTEwMjk1N1owKwYLKoZIhvcNAQkQAgwxHDAaMBgwFgQU3WIwrIYKLTBr # 2jixaHlSMAf7QX4wLwYJKoZIhvcNAQkEMSIEIN1qEj6t8jXKaAY3k9FZcgwlAsBl # SIgNsAp0S+PV+DJVMDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEIEqgP6Is11yExVyT # j4KOZ2ucrsqzP+NtJpqjNPFGEQozMA0GCSqGSIb3DQEBAQUABIICACVX+GO5nBmg # lKSsuIzB7FjG0TFIOiqV0WpeiDNPSCHXyDwdDATIgdaWVAu6OrOLlhMxQcqI0RWP # tP06kVAZLM6IP4Fx3K2dm7jYO+z5wE2XZ4/IB0IypMHPNt7jywL2MNpWvhiH9eT5 # 1JUWKVn0gqMxqlCn7Qr1D7aXKq5uhr7Mrz42eq+I588kzs/XnH+RM/XWlLY9LsKx # CMSrUhnoayVt2UlD2O3M/FFjfp1LYJPcAL2+7fSywR5cXvEXNegzXxp8XKCf1iEb # VKUKGu9BB6+2bqf8R/TGUk9ocLqU8UdBIzTlqGbhrj9LLkTephmt2v1lNOXDtevu # VGtrJoWuhLoucohVIJ1hUgWHSmGMoZLBQ6Q6t3gZJzJmK3nMyTAT+GUMf8twsKoI # GhrcJLMUklb/LhRBn7YRngs+W+Ji7LPhkLGAVRf8MyAZ5v0SdalbvodOO9OkTrsD # ezVf3Ks8iAYUhMMAKRhAjB3ZQA3c9ZJp7uOmN13+zYVojAHvZeBkFlX1vmaFrtIW # csQJGZ9QjfWFZdpb6g84i8rLyRTLmG2BraFKxbFPGiTn9i73L0pBGx4pa0VTzI/r # xSKrZmvlMbxN4f66TzdFPp1GOql5RY2MjgqEbmODJS19/RTjiz7RhEMszmuFuU3U # 2YeVxeFLYjA2IrCWGz+/vaWyMkg9qks5 # SIG # End signature block |