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 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). The function automatically creates an upload session and manages the chunked upload process for the current invocation. .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 - Uses conflict behavior 'replace' so existing files are 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 { try { $item = Get-Item -Path $LocalFilePath $fileSize = $item.length $fileStream = [System.IO.File]::OpenRead($item.FullName) Write-Verbose "Filesize: $fileSize" Write-Verbose "Chunksize: $chunkSize" $payload = @{ item = @{ '@microsoft.graph.conflictBehavior' = 'replace' } } Write-Verbose "Requesting upload session on $graphUri`:/createUploadSession" try { $uploadSession = Invoke-GraphWithRetry ` -RequestUri "$graphUri`:/createUploadSession" ` -method Post ` -body ($payload | ConvertTo-Json -Depth 10) ` -ErrorAction Stop } catch { Write-Error -ErrorRecord $_ return } if($null -ne $uploadSession.uploadUrl) { Write-Verbose "Upload session created: $($uploadSession.uploadUrl)" $uploadUrl = $uploadSession.uploadUrl $offset = 0 try { 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 } ` -ErrorAction Stop ` -ContentType 'application/octet-stream' | out-null $offset += $bytesRead } } } catch { Write-Error -ErrorRecord $_ } } else { Write-Error "Failed to create upload session. Response: $($uploadSession | ConvertTo-Json -Depth 10)" } } finally { if($null -ne $fileStream) { $fileStream.Close() } } } } function Add-GraphReference { <# .SYNOPSIS Adds a reference to a Microsoft Graph object. .DESCRIPTION Adds a reference to a Microsoft Graph group, application, or service principal. This is typically used to add members or owners by creating the corresponding $ref link. .PARAMETER ObjectId The identifier of the Microsoft Graph object that will receive the reference. .PARAMETER objectType The Microsoft Graph object type. Valid values are groups, applications, and servicePrincipals. .PARAMETER ReferenceType The reference collection to update. Valid values are members and owners. .PARAMETER MemberId The identifier of the object being referenced, such as a user, group, or service principal. .PARAMETER PermissiveModify Suppresses errors when the reference already exists. .INPUTS System.String Accepts MemberId values from the pipeline. .OUTPUTS None This command performs a Graph API call and does not emit output. .EXAMPLE Add-GraphReference -ObjectId $groupId -MemberId $userId Adds the specified user as a member of the group. .EXAMPLE Add-GraphReference -ObjectId $groupId -ReferenceType owners -MemberId $userId -PermissiveModify Adds the specified user as a group owner and ignores the request if the reference already exists. #> [CmdletBinding()] param ( [Parameter(Mandatory)] $ObjectId, [Parameter()] [ValidateSet('groups','applications','servicePrincipals')] [string]$objectType = 'groups', [Parameter()] [ValidateSet('members', 'owners')] [string]$ReferenceType = 'members', [Parameter(Mandatory, ValueFromPipeline)] [string]$MemberId, [switch]$PermissiveModify ) begin { $uri = New-GraphUri -Uri "/$objectType/$ObjectId/$ReferenceType/`$ref" } process { $body = @{ "@odata.id" = $script:graphConnection.GetReference($MemberId) } | ConvertTo-Json try { # we want this to throw, so to honor the -PermissiveModify switch Invoke-GraphWithRetry -Method Post -Uri $uri -Body $body -ErrorAction Stop Write-Verbose "User with ID $MemberId added to $ReferenceType of $ObjectId." } catch { $details = $_ | ConvertFrom-GraphErrorRecord if($details.error.message -match 'object references already exist' -and $PermissiveModify) { Write-Verbose -Message "User with ID $MemberId is already a $ReferenceType of $ObjectId." } else { Write-Error -ErrorRecord $_ } } } } function ConvertFrom-GraphErrorRecord { <# .SYNOPSIS Extracts Microsoft Graph error details from a PowerShell error record. .DESCRIPTION Parses the ErrorDetails payload from a PowerShell ErrorRecord and returns the deserialized Graph error object when it contains an error message. This helper is useful when handling failures from Invoke-GraphWithRetry and other commands that return Graph error payloads in JSON format. .PARAMETER ErrorRecord The PowerShell ErrorRecord to parse. .INPUTS System.Management.Automation.ErrorRecord Accepts error records from the pipeline. .OUTPUTS System.Object Returns the deserialized Graph error object when available. .EXAMPLE try { Invoke-GraphWithRetry -RequestUri 'https://graph.microsoft.com/v1.0/users/does-not-exist' -ErrorAction Stop } catch { $_ | ConvertFrom-GraphErrorRecord } Parses the Graph error payload from the caught exception. .EXAMPLE $details = $Error[0] | ConvertFrom-GraphErrorRecord Parses the most recent error record and returns Graph error details when present. .NOTES Returns nothing when the error details are not JSON or do not contain error.message. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline)] [System.Management.Automation.ErrorRecord]$ErrorRecord ) process { $details = $ErrorRecord.ErrorDetails | ConvertFrom-Json -ErrorAction SilentlyContinue if($null -ne $details.error.message) { $details } } } 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 values 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 RetryableErrorCodes HTTP status codes that should be treated as transient and retried by Invoke-GraphWithRetry. Default is 429. .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 Graph response objects, automatically handling pagination for collection responses. .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. .EXAMPLE Get-GraphData -RequestUri 'https://graph.microsoft.com/v1.0/users' -RetryableErrorCodes 429,503 Retrieves users while treating 429 and 503 responses as retryable transient failures. .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()] [int[]]$RetryableErrorCodes = @(429), [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) { #get page of results $result = Invoke-GraphWithRetry ` -RequestUri $uri ` -method Get ` -Headers $AdditionalHeaders ` -OperationName $OperationName ` -Confirm:$false ` -ErrorAction $ErrorActionPreference ` -RetryableErrorCodes $RetryableErrorCodes 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; } } } } 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 RetryableErrorCodes HTTP status codes that should be treated as transient and retried by Invoke-GraphWithRetry. Default is 429. .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. .EXAMPLE Invoke-GraphBatch -BatchRequest $requests -RetryableErrorCodes 429,503 Sends batch requests while treating 429 and 503 responses from the outer batch call as retryable transient failures. .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()] [int[]]$RetryableErrorCodes = @(429), [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 ` -RetryableErrorCodes $RetryableErrorCodes ` -OperationName $OperationName ` -ErrorAction $ErrorActionPreference ` -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 transient HTTP responses. The function retries retryable status codes (429 by default), using Retry-After when provided, or incremental backoff based on DefaultBackOffSeconds and the retry attempt number. 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 retry threshold used by the retry loop before the error is written. 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 retries status codes listed in RetryableErrorCodes (429 by default) - Uses Retry-After for 429 responses when available; otherwise uses incremental backoff - 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 { Write-Error -ErrorRecord $_ break; } } 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 values 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.AbsoluteUri)/$($Uri.TrimStart('/'))" } # else: Uri is already relative, use as-is } $queryParams = [System.Collections.Generic.List[string]]::new() if($WithSelect.Count -gt 0) { $queryParams.Add("`$select=$($WithSelect -join ',')") } 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 Remove-GraphReference { <# .SYNOPSIS Removes a reference from a Microsoft Graph object. .DESCRIPTION Removes a reference from a Microsoft Graph group, application, or service principal. This is typically used to remove members or owners by deleting the corresponding $ref link. .PARAMETER ObjectId The identifier of the Microsoft Graph object that owns the reference. .PARAMETER objectType The Microsoft Graph object type. Valid values are groups, applications, and servicePrincipals. .PARAMETER ReferenceType The reference collection to update. Valid values are members and owners. .PARAMETER MemberId The identifier of the object being removed from the reference collection. .PARAMETER PermissiveModify Suppresses errors when the reference does not exist. .INPUTS System.String Accepts MemberId values from the pipeline. .OUTPUTS None This command performs a Graph API call and does not emit output. .EXAMPLE Remove-GraphReference -ObjectId $groupId -MemberId $userId Removes the specified user from the group members collection. .EXAMPLE Remove-GraphReference -ObjectId $groupId -ReferenceType owners -MemberId $userId -PermissiveModify Removes the specified user from the group owners collection and ignores the request if the reference is already missing. #> [CmdletBinding()] param ( [Parameter(Mandatory)] $ObjectId, [Parameter()] [ValidateSet('groups','applications','servicePrincipals')] [string]$objectType = 'groups', [Parameter()] [ValidateSet('members', 'owners')] [string]$ReferenceType = 'members', [Parameter(Mandatory, ValueFromPipeline)] [string]$MemberId, [switch]$PermissiveModify ) begin { } process { $uri = New-GraphUri -Uri "/$objectType/$ObjectId/$ReferenceType/$MemberId/`$ref" try { Invoke-GraphWithRetry -Method Delete -Uri $uri -ErrorAction Stop Write-Verbose "User with ID $MemberId removed from $ReferenceType of $ObjectId." } catch { $ex = $_.Exception if($ex.Response.StatusCode -eq 404 -and $PermissiveModify) { Write-Verbose -Message "User with ID $MemberId is not in $ReferenceType of $ObjectId." } else { Write-Error -ErrorRecord $_ } } } } 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 { $uri = New-Object System.Uri($BaseUri.Trim().TrimEnd('/')) if($uri.Segments.Length -lt 2) { throw "Invalid BaseUri. Please provide a valid URI with at least one segment." } if($uri.Segments[1].TrimEnd('/') -notin @('v1.0', 'beta')) { throw "BaseUri must include a version segment (e.g. 'v1.0' or 'beta')." } $script:graphConnection.BaseUri = $uri } } 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 <# .SYNOPSIS Represents module-level connection settings for Microsoft Graph. .DESCRIPTION Stores shared configuration used by GraphApiHelper commands, including the authentication factory name, Graph base URI, scopes, and optional Application Insights logger instance. .NOTES This is an internal type used by module commands and is not exported. #> class GraphConnection { #name of AadAuthenticationFactry factory to use for obtaining tokens [string]$FactoryName #base URI for Microsoft Graph API calls, typically https://graph.microsoft.com/v1.0 or https://graph.microsoft.us/beta [Uri]$BaseUri #scopes required for Microsoft Graph API access [string[]]$GraphScope #optional Application Insights logger instance [object]$AiLogger GraphConnection() { #set defaults $this.FactoryName = 'graph' $this.BaseUri = [Uri]::new('https://graph.microsoft.com/v1.0') $this.GraphScope = @('https://graph.microsoft.com/.default') $this.AiLogger = $null } GraphConnection([string]$BaseUri, [string[]]$GraphScope, $AiLogger) { $this.FactoryName = 'graph' $this.BaseUri = new-object System.Uri($BaseUri) $this.GraphScope = $GraphScope $this.AiLogger = $AiLogger } <# .SYNOPSIS Builds a directory object reference URI for Microsoft Graph. .PARAMETER id The Azure AD object identifier to convert into a directoryObjects reference URI. .OUTPUTS System.String The fully-qualified reference URI for the provided object id. #> [string] GetReference([string]$id) { $ref = "$($this.BaseUri.Scheme)://$($this.BaseUri.Host)/v1.0/directoryObjects/$id" Write-Verbose "Constructed reference URI: $ref" return $ref } } #endregion Internal commands #region Module initialization $script:graphConnection = new-object GraphConnection('https://graph.microsoft.com/v1.0', @('https://graph.microsoft.com/.default'), $null) #endregion Module initialization # SIG # Begin signature block # MIIuMwYJKoZIhvcNAQcCoIIuJDCCLiACAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBAJP6jOtW2Zvsx # 8ZxD9tLdf7c8AH4BJfkuw5kNtiql5KCCE2AwggWQMIIDeKADAgECAhAFmxtXno4h # 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/XOSIrps4SXzInjHPEYuBETzMYIaKTCC # GiUCAQEwfTBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4x # QTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQw # OTYgU0hBMzg0IDIwMjEgQ0ExAhAP9xCe9qf4ax3LBs7uih/sMA0GCWCGSAFlAwQC # AQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwG # CisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZI # hvcNAQkEMSIEICKnQeMxfu4B6Mkmx3znNSlpCg0s+/PjqXG84xoJzA0gMA0GCSqG # SIb3DQEBAQUABIIBgFMfkBhVSCGdg+lRuHq0SELHC+2bsp8R2OVXqEcWn5MjlKdj # prmvNBjWS9g/S6y3fZjifs9C0qCmwy6mucUQY+gIw6+4DnGYpvVWEjkKZA8FFRJM # RiT/MjyGh3hYuH0rYvU/Qz1w4k4eh6tH7soSJFPRcoFxNLwuR8/emDtzEwug/UYJ # +IB5X83tFbo6NjS4E1zhzX9DesAHOk/e6RBL+te7CDCX/JAOMYshvXFHL5Cz1ZQs # C2LvzDE9cAEhmZPpjAXIauE0NTJK0wMTNjEg66ZG3dZqBa1Ruf9vWEPvMEV7Tp+z # AqkQ7NGxRmL6LyImC4wFC5FLM8Pltb/OBFsqOzUDq2RyMPZKGfrCo6GtBApCgG5d # s6YLUZfnN+EzhH6i+NZiTUTkbTTyigKVkq/RIOvjTJj/O1sXt3vbz0ZV/QPzm9/B # BNYcY/Dr61c+LWgC0RgRfyTlhfR78BUVPjmO+jJgJ1ZEdkJ6KX0+dEE/3VWojspB # 6tz6VSFN393mOM6TCqGCF3YwghdyBgorBgEEAYI3AwMBMYIXYjCCF14GCSqGSIb3 # DQEHAqCCF08wghdLAgEDMQ8wDQYJYIZIAWUDBAIBBQAwdwYLKoZIhvcNAQkQAQSg # aARmMGQCAQEGCWCGSAGG/WwHATAxMA0GCWCGSAFlAwQCAQUABCBpW8PvxlqPjomI # EZBeQ+Oi0LNBRqipZOk56PxfC1NSIAIQDZ9orX9JAuysT3tqlPltehgPMjAyNjA2 # MTMxMDM2MjlaoIITOjCCBu0wggTVoAMCAQICEAqA7xhLjfEFgtHEdqeVdGgwDQYJ # KoZIhvcNAQELBQAwaTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ # bmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IFRpbWVTdGFtcGluZyBS # U0E0MDk2IFNIQTI1NiAyMDI1IENBMTAeFw0yNTA2MDQwMDAwMDBaFw0zNjA5MDMy # MzU5NTlaMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7 # MDkGA1UEAxMyRGlnaUNlcnQgU0hBMjU2IFJTQTQwOTYgVGltZXN0YW1wIFJlc3Bv # bmRlciAyMDI1IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDQRqwt # Esae0OquYFazK1e6b1H/hnAKAd/KN8wZQjBjMqiZ3xTWcfsLwOvRxUwXcGx8AUjn # i6bz52fGTfr6PHRNv6T7zsf1Y/E3IU8kgNkeECqVQ+3bzWYesFtkepErvUSbf+EI # YLkrLKd6qJnuzK8Vcn0DvbDMemQFoxQ2Dsw4vEjoT1FpS54dNApZfKY61HAldytx # NM89PZXUP/5wWWURK+IfxiOg8W9lKMqzdIo7VA1R0V3Zp3DjjANwqAf4lEkTlCDQ # 0/fKJLKLkzGBTpx6EYevvOi7XOc4zyh1uSqgr6UnbksIcFJqLbkIXIPbcNmA98Os # kkkrvt6lPAw/p4oDSRZreiwB7x9ykrjS6GS3NR39iTTFS+ENTqW8m6THuOmHHjQN # C3zbJ6nJ6SXiLSvw4Smz8U07hqF+8CTXaETkVWz0dVVZw7knh1WZXOLHgDvundrA # tuvz0D3T+dYaNcwafsVCGZKUhQPL1naFKBy1p6llN3QgshRta6Eq4B40h5avMcpi # 54wm0i2ePZD5pPIssoszQyF4//3DoK2O65Uck5Wggn8O2klETsJ7u8xEehGifgJY # i+6I03UuT1j7FnrqVrOzaQoVJOeeStPeldYRNMmSF3voIgMFtNGh86w3ISHNm0Ia # adCKCkUe2LnwJKa8TIlwCUNVwppwn4D3/Pt5pwIDAQABo4IBlTCCAZEwDAYDVR0T # AQH/BAIwADAdBgNVHQ4EFgQU5Dv88jHt/f3X85FxYxlQQ89hjOgwHwYDVR0jBBgw # FoAU729TSunkBnx6yuKQVvYv1Ensy04wDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB # /wQMMAoGCCsGAQUFBwMIMIGVBggrBgEFBQcBAQSBiDCBhTAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMF0GCCsGAQUFBzAChlFodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdS # U0E0MDk2U0hBMjU2MjAyNUNBMS5jcnQwXwYDVR0fBFgwVjBUoFKgUIZOaHR0cDov # L2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0VGltZVN0YW1waW5n # UlNBNDA5NlNIQTI1NjIwMjVDQTEuY3JsMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsG # CWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAZSqt8RwnBLmuYEHs0QhEnmNA # ciH45PYiT9s1i6UKtW+FERp8FgXRGQ/YAavXzWjZhY+hIfP2JkQ38U+wtJPBVBaj # YfrbIYG+Dui4I4PCvHpQuPqFgqp1PzC/ZRX4pvP/ciZmUnthfAEP1HShTrY+2DE5 # qjzvZs7JIIgt0GCFD9ktx0LxxtRQ7vllKluHWiKk6FxRPyUPxAAYH2Vy1lNM4kze # kd8oEARzFAWgeW3az2xejEWLNN4eKGxDJ8WDl/FQUSntbjZ80FU3i54tpx5F/0Kr # 15zW/mJAxZMVBrTE2oi0fcI8VMbtoRAmaaslNXdCG1+lqvP4FbrQ6IwSBXkZagHL # hFU9HCrG/syTRLLhAezu/3Lr00GrJzPQFnCEH1Y58678IgmfORBPC1JKkYaEt2Od # Dh4GmO0/5cHelAK2/gTlQJINqDr6JfwyYHXSd+V08X1JUPvB4ILfJdmL+66Gp3CS # BXG6IwXMZUXBhtCyIaehr0XkBoDIGMUG1dUtwq1qmcwbdUfcSYCn+OwncVUXf53V # JUNOaMWMts0VlRYxe5nK+At+DI96HAlXHAL5SlfYxJ7La54i71McVWRP66bW+yER # NpbJCjyCYG2j+bdpxo/1Cy4uPcU3AWVPGrbn5PhDBf3Froguzzhk++ami+r3Qrx5 # bIbY3TVzgiFI7Gq3zWcwgga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0G # CSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTla # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEy # NTYgMjAyNSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHT # CphBcr48RsAcrHXbo0ZodLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPh # of6pvF4uGjwjqNjfEvUi6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mA # xAHeHYNnQxqXmRinvuNgxVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBv # MgEdgkFiDNYiOTx4OtiFcMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps # 0wjUjsZvkgFkriK9tUKJm/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF # 83bRVFLeGkuAhHiGPMvSGmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXi # UOeSLsJygoLPp66bkDX1ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOM # CZIVNSaz7BX8VtYGqLt9MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydP # pOjL6s36czwzsucuoKs7Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrU # G2ZdSoQbU2rMkpLiQ6bGRinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+ # sdFUeEY0qVjPKOWug/G6X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0T # AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYD # VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG # A1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV # HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU # cnVzdGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1s # BwEwDQYJKoZIhvcNAQELBQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WI # GjB/T8ObXAZz8OjuhUxjaaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+ # IQhQE7jU/kXjjytJgnn0hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8M # yb9rEVKChHyfpzee5kH0F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2 # th9y1IsA0QF8dTXqvcnTmpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjaj # V/gxdEkMx1NKU4uHQcKfZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2 # Lr3ty9qIijanrUR3anzEwlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFze # GxcytL5TTLL4ZaoBdqbhOhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG # 7uEBYTptMSbhdhGQDpOXgpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+N # Jpud/v4+7RWsWCiKi9EOLLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckT # etiSuEtQvLsNz3Qbp7wGWqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszW # kPZPubdcMIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkqhkiG9w0B # AQwFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD # VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk # IElEIFJvb3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcNMzExMTA5MjM1OTU5WjBiMQsw # CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu # ZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQw # ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz # 7MKnJS7JIT3yithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS # 5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7 # bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfI # SKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jH # trHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14 # Ztk6MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2 # h4mXaXpI8OCiEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt # 6zPZxd9LBADMfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPR # iQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ER # ElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4K # Jpn15GkvmB0t9dmpsh3lGwIDAQABo4IBOjCCATYwDwYDVR0TAQH/BAUwAwEB/zAd # BgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wHwYDVR0jBBgwFoAUReuir/SS # y4IxLVGLp6chnfNtyA8wDgYDVR0PAQH/BAQDAgGGMHkGCCsGAQUFBwEBBG0wazAk # BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAC # hjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURS # b290Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0 # LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwEQYDVR0gBAowCDAGBgRV # HSAAMA0GCSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXnOF+go3QbPbYW1/e/Vwe9mqyh # hyzshV6pGrsi+IcaaVQi7aSId229GhT0E0p6Ly23OO/0/4C5+KH38nLeJLxSA8hO # 0Cre+i1Wz/n096wwepqLsl7Uz9FDRJtDIeuWcqFItJnLnU+nBgMTdydE1Od/6Fmo # 8L8vC6bp8jQ87PcDx4eo0kxAGTVGamlUsLihVo7spNU96LHc/RzY9HdaXFSMb++h # UD38dglohJ9vytsgjTVgHAIDyyCwrFigDkBjxZgiwbJZ9VVrzyerbHbObyMt9H5x # aiNrIv8SuFQtJ37YOtnwtoeW/VvRXKwYw02fc7cBqZ9Xql4o4rmUMYIDfDCCA3gC # AQEwfTBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/ # BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYg # U0hBMjU2IDIwMjUgQ0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUA # oIHRMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcN # MjYwNjEzMTAzNjI5WjArBgsqhkiG9w0BCRACDDEcMBowGDAWBBTdYjCshgotMGva # OLFoeVIwB/tBfjAvBgkqhkiG9w0BCQQxIgQgrR7L5NAan70yPFMJ6FeAa/QAPLSI # RnINSm9wE5bM5CkwNwYLKoZIhvcNAQkQAi8xKDAmMCQwIgQgSqA/oizXXITFXJOP # go5na5yuyrM/420mmqM08UYRCjMwDQYJKoZIhvcNAQEBBQAEggIAsh0hrf8P9C8L # ejrEbMiFfAhW6iqapBegY36RwEMv7tm+WeoHCclQLfmuZgiqm9Ev3YDh7O4F5/Ku # pPsB3kFdRp2/OHz2/Vt/WDDffLvWP2uoSaHFKAN4vM0DJ084Yr+vE66Vk2xxOVbh # +H0jrbtwc06bUtjSxL7vyUmvkfF4Oet0ehmjFAodpenlrZe/ShTAmeznQn4E4bHx # sn4mfbbbWXZyMpyo5Y2KV7Lx4f8kbXm8Gb0F5F8CLdJIx03cUD7bHmPiXZMdrkES # oYqO7ndKP19jS98TxbDq+D15XRUdw5adgAQoxImq2WziSYNVsj8ulVOV8F9J1/W0 # cZ0vkFv6w4imPH0RRqUQFwY5PDE5GfPhNyz/OL+z+/bnehtUYn/qlEuruRDh6Qib # rN8lVJ9nmJM0n5GFrZeTUn3kgquFLIRcUITwEHBsPiw4N1PsiM/D2FqXR6v2m9WN # v+zPd7RLoIS+FIxzVrY+ienDU95U8ICR76xrmkAbyK4aMyTmH3L+gx6y8gwDUpDu # 33oNNjvEKHLAUNzVV3NvWxF/S5Vzsq1O7DOhsIfIdqYYmkczq15IWZ39g6yLW0cm # oEjph6dTs/jpze5z7YWbXhWpCGAgcw6OpIugafyKdPcKYEt1KupGZFsJjeBkW7Tq # peCaDbMPDdOs+Aw19Ca2vEQcP9kCEIc= # SIG # End signature block |