Graphimo.psm1
function Join-UriQuery { <# .SYNOPSIS Provides ability to join two Url paths together including advanced querying .DESCRIPTION Provides ability to join two Url paths together including advanced querying which is useful for RestAPI/GraphApi calls .PARAMETER BaseUri Primary Url to merge .PARAMETER RelativeOrAbsoluteUri Additional path to merge with primary url (optional) .PARAMETER QueryParameter Parameters and their values in form of hashtable .EXAMPLE Join-UriQuery -BaseUri 'https://evotec.xyz/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts' -QueryParameter @{ page = 1 per_page = 20 search = 'SearchString' } .EXAMPLE Join-UriQuery -BaseUri 'https://evotec.xyz/wp-json/wp/v2/posts' -QueryParameter @{ page = 1 per_page = 20 search = 'SearchString' } .EXAMPLE Join-UriQuery -BaseUri 'https://evotec.xyz' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts' .NOTES General notes #> [alias('Join-UrlQuery')] [CmdletBinding()] param ( [parameter(Mandatory)][uri] $BaseUri, [parameter(Mandatory = $false)][uri] $RelativeOrAbsoluteUri, [Parameter()][System.Collections.IDictionary] $QueryParameter ) # Join primary url with additional path if needed if ($BaseUri -and $RelativeOrAbsoluteUri) { $Url = Join-Uri -BaseUri $BaseUri -RelativeOrAbsoluteUri $RelativeOrAbsoluteUri } else { $Url = $BaseUri } # Create a http name value collection from an empty string if ($QueryParameter) { $Collection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty) foreach ($key in $QueryParameter.Keys) { $Collection.Add($key, $QueryParameter.$key) } } # Build the uri $uriRequest = [System.UriBuilder] $Url if ($Collection) { $uriRequest.Query = $Collection.ToString() } #return $uriRequest.Uri.OriginalUri return $uriRequest.Uri.AbsoluteUri } function Join-Uri { <# .SYNOPSIS Provides ability to join two Url paths together .DESCRIPTION Provides ability to join two Url paths together .PARAMETER BaseUri Primary Url to merge .PARAMETER RelativeOrAbsoluteUri Additional path to merge with primary url .EXAMPLE Join-Uri 'https://evotec.xyz/' '/wp-json/wp/v2/posts' .EXAMPLE Join-Uri 'https://evotec.xyz/' 'wp-json/wp/v2/posts' .EXAMPLE Join-Uri -BaseUri 'https://evotec.xyz/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts' .EXAMPLE Join-Uri -BaseUri 'https://evotec.xyz/test/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts' .NOTES General notes #> [alias('Join-Url')] [cmdletBinding()] param( [parameter(Mandatory)][uri] $BaseUri, [parameter(Mandatory)][uri] $RelativeOrAbsoluteUri ) return ($BaseUri.OriginalString.TrimEnd('/') + "/" + $RelativeOrAbsoluteUri.OriginalString.TrimStart('/')) #return [Uri]::new([Uri]::new($BaseUri), $RelativeOrAbsoluteUri).ToString() #return [Uri]::new($BaseUri.OriginalString, $RelativeOrAbsoluteUri).ToString() } function Connect-MsalToken { [cmdletBinding()] param( [System.Collections.IDictionary] $MsalTokenSplat, [System.Collections.IDictionary] $Authorization, [int] $ExpiresTimeout = 30, [switch] $ForceRefesh ) if (-not $Script:AuthorizationCache) { $Script:AuthorizationCache = [ordered] @{} } if ($Authorization.Splat) { $ApplicationID = $Authorization.Splat.ClientId if ($Script:AuthorizationCache[$ApplicationID] -and -not $ForceRefesh) { if ($Script:AuthorizationCache[$ApplicationID].MsalToken.ExpiresOn.UtcDateTime -gt ([datetime]::UtcNow).AddSeconds($ExpiresTimeout)) { Write-Verbose "Connect-MsalToken - Using cache for $ApplicationID" return $Script:AuthorizationCache[$ApplicationID] } } $Splat = $Authorization.Splat try { $MsalToken = Get-MsalToken @Splat -ErrorAction Stop } catch { Write-Warning -Message "Connect-MsalToken - Couldn't execute Get-MsalToken. Error: $($_.Exception.Message)" return } $Script:AuthorizationCache[$ApplicationID] = [ordered] @{ 'MsalToken' = $MsalToken 'Splat' = $Authorization.Splat } $Script:AuthorizationCache[$ApplicationID] } else { Write-Warning -Message "Connect-MsalToken - Using old authorization format without Splatting. No refresh of tokens will be available." } } function Convert-GraphInternalUser { <# .SYNOPSIS Converts user returned by graph with onPremisesExtensionAttributes to new simplified object .DESCRIPTION Converts user returned by graph with onPremisesExtensionAttributes to new simplified object .PARAMETER InputObject The object to convert .EXAMPLE An example .NOTES General notes #> [CmdletBinding()] param( [parameter(Mandatory, ValueFromPipeline)][PSCustomObject[]] $InputObject ) Process { $InputObject | ForEach-Object { $NewObject = [ordered] @{} $Object = $_ foreach ($Property in $Object.PSObject.Properties.Name) { if ($Property -eq 'onPremisesExtensionAttributes') { foreach ($ExtensionAttribute in $Object.onPremisesExtensionAttributes.PSObject.Properties.Name) { $NewObject[$ExtensionAttribute] = $Object.onPremisesExtensionAttributes.$ExtensionAttribute } } else { $NewObject[$Property] = $Object.$Property } } [PSCustomObject] $NewObject } } } function Invoke-InternalGraphimo { [CmdletBinding()] param( [Array] $OutputQuery, [int] $First, [int] $CurrentCount ) if ($OutputQuery.value) { $FoundUsers = $OutputQuery.value } if ($First) { if ($CurrentCount) { $First = $First - $CurrentCount } if ($FoundUsers.Count -eq $First) { return $FoundUsers } elseif ($FoundUsers.Count -gt $First) { $FoundUsers = $FoundUsers | Select-Object -First $First return $FoundUsers } else { $First = $First - $FoundUsers.Count $FoundUsers } } else { $FoundUsers } } function Add-GraphGroup { [CmdletBinding()] param( [parameter(Mandatory)][alias('Authorization')][System.Collections.IDictionary] $Headers, [string] $DisplayName, [string] $Name, [string] $Description, [string] $MailNickname, [switch] $SecurityEnabled, [switch] $MailEnabled ) $URI = "/groups" $BaseUri = 'https://graph.microsoft.com/v1.0' $Body = [ordered]@{ groupTypes = @() } if ($PSBoundParameters.ContainsKey('DisplayName')) { $Body['displayName'] = $DisplayName } if ($PSBoundParameters.ContainsKey('Name')) { $Body['name'] = $Name } if ($PSBoundParameters.ContainsKey('Description')) { $Body['description'] = $Description } if ($PSBoundParameters.ContainsKey('MailNickname')) { $Body['mailNickname'] = $MailNickname } if ($PSBoundParameters.ContainsKey('SecurityEnabled')) { $Body['securityEnabled'] = $SecurityEnabled.IsPresent } if ($PSBoundParameters.ContainsKey('MailEnabled')) { $Body['mailEnabled'] = $MailEnabled.IsPresent } if ($Body.Count -gt 0) { Invoke-Graphimo -Uri $URI -Method POST -Headers $Headers -Body $Body -BaseUri $BaseUri } } function Add-GraphGroupMember { [cmdletBinding(SupportsShouldProcess)] param( [parameter(Mandatory)][alias('Authorization')][System.Collections.IDictionary] $Headers, [parameter(Mandatory)][alias('GroupID')][string] $ID, [parameter(Mandatory)][string] $MemberID ) $URI = "/groups/$ID/members/`$ref" $Body = @{ "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$MemberID" } Invoke-Graphimo -Uri $URI -Method POST -Headers $Headers -Body $Body } function Add-GraphUser { [cmdletBinding(SupportsShouldProcess)] param( [parameter(Mandatory)][alias('Authorization')][System.Collections.IDictionary] $Headers, [Parameter(Mandatory)][string] $UserPrincipalName, [string] $Name, [parameter(Mandatory)][alias('AccountEnabled')][bool] $Enabled, [alias('FirstName')][string] $GivenName, [alias('LastName')][string] $Surname, [alias('Title')][string] $JobTitle, [string] $EmployeeId, [string] $City, [Parameter(Mandatory)][string] $MailNickname, [alias('EmailAddress')][string] $Mail, [string] $Country, [string] $Department, [string] $PostalCode, [alias('Fax')][string] $FaxNumber, [string] $State, [string] $StreetAddress, [alias('OfficePhone')][string] $BusinessPhones, [alias('Mobile')][string] $MobilePhone, [string] $OfficeLocation, [string] $CompanyName, [Parameter(Mandatory)][string] $DisplayName, [switch] $ShowInAddressList, [switch] $DoNotForceChangePasswordNextSignIn, [string] $EmployeeType, [Parameter(Mandatory)][string] $Password, [alias('HireDate')][DateTime] $StartDate, [alias('CustomProperty')][System.Collections.IDictionary] $CustomProperties ) $URI = "/users" $Body = [ordered]@{} # https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0 if ($PSBoundParameters.ContainsKey('StartDate')) { # requires fixing # The date and time when the user was hired or will start work in case of a future hire. $Body['employeeHireDate'] = $StartDate } if ($PSBoundParameters.ContainsKey('UserPrincipalName')) { $Body['userPrincipalName'] = $UserPrincipalName } if ($PSBoundParameters.ContainsKey('JobTitle')) { $Body['jobTitle'] = $JobTitle } if ($PSBoundParameters.ContainsKey('EmployeeId')) { $Body['employeeId'] = $EmployeeId } if ($PSBoundParameters.ContainsKey('MailNickname')) { $Body['mailNickname'] = $MailNickname } if ($PSBoundParameters.ContainsKey('Mail')) { $Body['mail'] = $Mail } if ($PSBoundParameters.ContainsKey('FaxNumber')) { $Body['faxNumber'] = $FaxNumber } if ($PSBoundParameters.ContainsKey('givenName')) { $Body['givenName'] = $givenName } if ($PSBoundParameters.ContainsKey('Surname')) { $Body['surname'] = $Surname } if ($PSBoundParameters.ContainsKey('City')) { $Body['city'] = $City } if ($PSBoundParameters.ContainsKey('Country')) { $Body['country'] = $Country } if ($PSBoundParameters.ContainsKey('Department')) { $Body['department'] = $Department } if ($PSBoundParameters.ContainsKey('PostalCode')) { $Body['postalCode'] = $PostalCode } if ($PSBoundParameters.ContainsKey('State')) { $Body['state'] = $State } if ($PSBoundParameters.ContainsKey('StreetAddress')) { $Body['streetAddress'] = $StreetAddress } if ($PSBoundParameters.ContainsKey('businessPhones')) { $Body['businessPhones'] = @($businessPhones) } if ($PSBoundParameters.ContainsKey('mobilePhone')) { $Body['mobilePhone'] = $mobilePhone } if ($PSBoundParameters.ContainsKey('OfficeLocation')) { $Body['officeLocation'] = $OfficeLocation } if ($PSBoundParameters.ContainsKey('CompanyName')) { $Body['companyName'] = $CompanyName } if ($PSBoundParameters.ContainsKey('DisplayName')) { $Body['displayName'] = $DisplayName } if ($PSBoundParameters.ContainsKey('ShowInAddressList')) { $Body['showInAddressList'] = $ShowInAddressList.IsPresent } if ($PSBoundParameters.ContainsKey('Enabled')) { $Body['accountEnabled'] = $Enabled } if ($PSBoundParameters.ContainsKey('EmployeeType')) { $Body['employeeType'] = $EmployeeType $BaseUri = 'https://graph.microsoft.com/beta' } else { $BaseUri = 'https://graph.microsoft.com/v1.0' } $Body['passwordProfile'] = @{ forceChangePasswordNextSignIn = -not $DoNotForceChangePasswordNextSignIn.IsPresent password = $Password } foreach ($Property in $CustomProperties.Keys) { $Body[$Property] = $CustomProperties[$Property] } #Remove-EmptyValue -Hashtable $Body if ($Body.Count -gt 0) { Invoke-Graphimo -Uri $URI -Method POST -Headers $Headers -Body $Body -BaseUri $BaseUri } } function Connect-Graphimo { [cmdletBinding(DefaultParameterSetName = 'ClearText')] param( [parameter(Mandatory, ParameterSetName = 'Encrypted')] [parameter(Mandatory, ParameterSetName = 'ClearText')][string][alias('ClientID')] $ApplicationID, [parameter(Mandatory, ParameterSetName = 'ClearText')][string][alias('ClientSecret')] $ApplicationKey, [parameter(Mandatory, ParameterSetName = 'Encrypted')][string][alias('ClientSecretEncrypted')] $ApplicationKeyEncrypted, [parameter(Mandatory, ParameterSetName = 'Credential')][PSCredential] $Credential, [parameter(Mandatory, ParameterSetName = 'Encrypted')] [parameter(Mandatory, ParameterSetName = 'ClearText')] [parameter(Mandatory, ParameterSetName = 'Credential')] [string] $TenantDomain, [parameter(ParameterSetName = 'Encrypted')] [parameter(ParameterSetName = 'ClearText')] [parameter(ParameterSetName = 'Credential')] [ValidateSet("https://manage.office.com", "https://graph.microsoft.com", "https://graph.microsoft.com/beta", 'https://graph.microsoft.com/.default')] $Resource = 'https://graph.microsoft.com/.default', [int] $ExpiresTimeout = 30, [switch] $ForceRefesh, [parameter(Mandatory, ParameterSetName = 'MsalToken')][System.Collections.IDictionary] $MsalToken ) # Comparison V1/V2 https://nicolgit.github.io/AzureAD-Endopoint-V1-vs-V2-comparison/ if (-not $Script:AuthorizationCache) { $Script:AuthorizationCache = [ordered] @{} } if ($Credential) { $RestSplat = @{ ErrorAction = 'Stop' Method = 'POST' Body = @{ grant_type = "client_credentials" client_id = $Credential.UserName client_secret = $Credential.GetNetworkCredential().Password } } } elseif ($ApplicationKey -or $ApplicationKeyEncrypted) { if ($ApplicationKeyEncrypted) { try { $ApplicationKeyTemp = $ApplicationKeyEncrypted | ConvertTo-SecureString -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Warning -Message "Connect-Graphimo - Error: $ErrorMessage" return } $ApplicationKey = [System.Net.NetworkCredential]::new([string]::Empty, $ApplicationKeyTemp).Password } $RestSplat = @{ ErrorAction = 'Stop' Method = 'POST' Body = @{ grant_type = "client_credentials" client_id = $ApplicationID client_secret = $ApplicationKey } } } elseif ($MsalToken) { $Authorization = @{ Splat = $MsalToken } return Connect-MsalToken -Authorization $Authorization -ExpiresTimeout $ExpiresTimeout -ForceRefesh:$ForceRefesh } if ($Script:AuthorizationCache[$ApplicationID] -and -not $ForceRefesh) { if ($Script:AuthorizationCache[$ApplicationID].ExpiresOn -gt [datetime]::UtcNow) { Write-Verbose "Connect-Graphimo - Using cache for $ApplicationID" return $Script:AuthorizationCache[$ApplicationID] } } if ($Resource -in 'https://graph.microsoft.com/.default', "https://graph.microsoft.com/beta") { # V2 Endpoint $RestSplat['Body']['scope'] = $Resource $RestSplat['Uri'] = "https://login.microsoftonline.com/$($TenantDomain)/oauth2/v2.0/token" } else { # V1 Endpoint $RestSplat['Body']['resource'] = $Resource $RestSplat['Uri'] = "https://login.microsoftonline.com/$($TenantDomain)/oauth2/token" } Write-Verbose "Connect-Graphimo - EndPoint $($RestSplat['Uri'])" try { $Authorization = Invoke-RestMethod @RestSplat -Verbose:$false $Key = [ordered] @{ 'Authorization' = "$($Authorization.token_type) $($Authorization.access_token)" 'Extended' = $Authorization 'Error' = '' 'ExpiresOn' = ([datetime]::UtcNow).AddSeconds($Authorization.expires_in - $ExpiresTimeout) 'Splat' = [ordered] @{ ApplicationID = $RestSplat['Body']['client_id'] ApplicationKey = $RestSplat['Body']['client_secret'] TenantDomain = $TenantDomain Resource = $Resource } } $Script:AuthorizationCache[$ApplicationID] = $Key } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Warning -Message "Connect-Graphimo - Error: $ErrorMessage" $Key = [ordered] @{ 'Authorization' = $Null 'Extended' = $Null 'Error' = $ErrorMessage 'ExpiresOn' = $null 'Splat' = [ordered] @{ ApplicationID = $RestSplat['Body']['client_id'] ApplicationKey = $RestSplat['Body']['client_secret'] TenantDomain = $TenantDomain Resource = $Resource } } } $Key } function Get-GraphApplication { [cmdletBinding()] param( [parameter(Mandatory)][alias('Authorization')][System.Collections.IDictionary] $Headers, [string] $ID, [string] $DisplayName, [string[]] $Property ) if ($ID) { # Query a single group $RelativeURI = "/applications/$ID" $QueryParameter = @{ '$Select' = $Property -join ',' } } elseif ($DisplayName) { $RelativeURI = '/applications' $QueryParameter = @{ '$Select' = $Property -join ',' '$filter' = "displayName eq '$DisplayName'" '$orderby' = $OrderBy } } else { # Query multiple groups $RelativeURI = '/applications' $QueryParameter = @{ '$Select' = $Property -join ',' '$filter' = $Filter '$orderby' = $OrderBy } } Remove-EmptyValue -Hashtable $QueryParameter Invoke-Graphimo -Uri $RelativeURI -Method GET -Headers $Headers -QueryParameter $QueryParameter } function Get-GraphContact { [cmdletBinding()] param( [parameter(Mandatory)][alias('Authorization')][System.Collections.IDictionary] $Headers, [string] $Id, [string[]] $Property, [string] $Filter, [string] $OrderBy ) if ($ID) { # Query a single group $RelativeURI = "/contacts/$ID" $QueryParameter = @{ '$Select' = $Property -join ',' } } else { # Query multiple groups $RelativeURI = '/contacts' $QueryParameter = @{ '$Select' = $Property -join ',' '$filter' = $Filter '$orderby' = $OrderBy } } Remove-EmptyValue -Hashtable $QueryParameter Invoke-Graphimo -Uri $RelativeURI -Method GET -Headers $Headers -QueryParameter $QueryParameter } function Get-GraphGroup { [cmdletBinding()] param( [parameter(Mandatory)][alias('Authorization')][System.Collections.IDictionary] $Headers, [string] $Id, [string[]] $Property, [string] $Filter, [string] $OrderBy ) if ($ID) { # Query a single group $RelativeURI = "/groups/$ID" $QueryParameter = @{ '$Select' = $Property -join ',' } } else { # Query multiple groups $RelativeURI = '/groups' $QueryParameter = @{ '$Select' = $Property -join ',' '$filter' = $Filter '$orderby' = $OrderBy } } Remove-EmptyValue -Hashtable $QueryParameter Invoke-Graphimo -Uri $RelativeURI -Method GET -Headers $Headers -QueryParameter $QueryParameter } function Get-GraphGroupMember { [cmdletBinding()] param( [parameter(Mandatory)][alias('Authorization')][System.Collections.IDictionary] $Headers, [parameter(Mandatory)][string] $Id, [string] $Search, [string[]] $Property ) if ($ID) { # Query a single group $RelativeURI = "/groups/$ID/members" $QueryParameter = @{ '$Select' = $Property -join ',' '$Search' = $Search } if ($QueryParameter.'$Search') { # This is required for search to work # https://developer.microsoft.com/en-us/identity/blogs/build-advanced-queries-with-count-filter-search-and-orderby/ $Headers['ConsistencyLevel'] = 'eventual' } } Remove-EmptyValue -Hashtable $QueryParameter Invoke-Graphimo -Uri $RelativeURI -Method GET -Headers $Headers -QueryParameter $QueryParameter } function Get-GraphUser { [alias('Get-GraphUsers')] [cmdletBinding(DefaultParameterSetName = 'Default')] param( [parameter(ParameterSetName = 'Default', Mandatory)] [parameter(ParameterSetName = 'EmailAddress', Mandatory)] [parameter(ParameterSetName = 'UserPrincipalName', Mandatory)] [parameter(ParameterSetName = 'Filter', Mandatory)] [parameter(ParameterSetName = 'Id', Mandatory)] [parameter(Mandatory)][alias('Authorization')][System.Collections.IDictionary] $Headers, [parameter(ParameterSetName = 'Id')][string] $Id, [parameter(ParameterSetName = 'UserPrincipalName')][string] $UserPrincipalName, [alias('Mail')][parameter(ParameterSetName = 'EmailAddress')][string] $EmailAddress, [string[]] $Property, [parameter(ParameterSetName = 'Filter')][string] $Filter, [string] $OrderBy, [switch] $IncludeManager, [int] $First ) if ($Property -contains 'EmployeeType') { $BaseURI = 'https://graph.microsoft.com/beta' } else { $BaseURI = 'https://graph.microsoft.com/v1.0' } $NewProperties = foreach ($P in $Property) { if ($P -like "extensionAttribute*") { 'onPremisesExtensionAttributes' } else { $P } } $Property = $NewProperties | Select-Object -Unique if ($UserPrincipalName) { $RelativeURI = '/users' $QueryParameter = @{ '$Select' = $Property -join ',' '$filter' = "userPrincipalName eq '$UserPrincipalName'" } } elseif ($EmailAddress) { $RelativeURI = '/users' $QueryParameter = @{ '$Select' = $Property -join ',' '$filter' = "mail eq '$EmailAddress'" } } elseif ($ID) { # Query a single user # doing it standard way doesn't seem to work so lets user filter instead #$RelativeURI = "/users/$ID" $RelativeURI = "/users" $QueryParameter = @{ '$filter' = "id eq '$ID'" '$Select' = $Property -join ',' } } else { # Query multiple groups $RelativeURI = '/users' $QueryParameter = @{ '$Select' = $Property -join ',' '$filter' = $Filter '$orderby' = $OrderBy } } if ($IncludeManager) { $QueryParameter['$expand'] = 'manager' } Remove-EmptyValue -Hashtable $QueryParameter if ($Property -contains 'onPremisesExtensionAttributes') { Invoke-Graphimo -Uri $RelativeURI -Method GET -Headers $Headers -QueryParameter $QueryParameter -BaseUri $BaseURI -First $First | Convert-GraphInternalUser } else { Invoke-Graphimo -Uri $RelativeURI -Method GET -Headers $Headers -QueryParameter $QueryParameter -BaseUri $BaseURI -First $First } } function Import-GraphGuest { [cmdletBinding()] param( [parameter(Mandatory)][alias('Authorization')][System.Collections.IDictionary] $Headers, [string] $Name, [Parameter(Mandatory)][string] $EmailAddress, [switch] $SendInvitationMessage, [string] $InviteRedirectUrl = "https://portal.office.com", [switch] $ResetRedemption, [string] $InvitedUserID ) $URI = '/invitations' $body = [ordered]@{ 'invitedUserDisplayName' = $Name 'invitedUserEmailAddress' = $EmailAddress 'inviteRedirectUrl' = $InviteRedirectUrl 'sendInvitationMessage' = $SendInvitationMessage.IsPresent 'resetRedemption' = $ResetRedemption.IsPresent } if ($InvitedUserID) { $Body['invitedUser'] = @{ 'id' = $InvitedUserID } } Invoke-Graphimo -Uri $URI -Method POST -Headers $Headers -Body $Body } function Invoke-Graphimo { [cmdletBinding(SupportsShouldProcess)] param( [alias('PrimaryUri')][uri] $BaseUri = 'https://graph.microsoft.com/v1.0', [uri] $Uri, [parameter(Mandatory)][alias('Authorization')][System.Collections.IDictionary] $Headers, [validateset('GET', 'DELETE', 'POST', 'PATCH', 'PUT')][string] $Method = 'GET', [string] $ContentType = "application/json; charset=UTF-8", [System.Collections.IDictionary] $Body, [System.Collections.IDictionary] $QueryParameter, [switch] $FullUri, [int] $First ) if ($Headers.MsalToken) { if ($Headers.Splat) { $Splat = $Headers.Splat $Headers = Connect-MsalToken -Authorization $Headers } } else { # This forces a reconnect of session in case it's about to time out. If it's not timeouting a cache value is used if ($Headers.Splat) { $Splat = $Headers.Splat $Headers = Connect-Graphimo @Splat } } if ($Headers.Error) { Write-Warning "Invoke-Graphimo - Authorization error. Skipping." return } if ($Headers.MsalToken) { $RestSplat = @{ Headers = @{ Authorization = $Headers.MsalToken.TokenType + ' ' + $Headers.MsalToken.AccessToken } Method = $Method ContentType = $ContentType } } else { $RestSplat = @{ Headers = $Headers Method = $Method ContentType = $ContentType } } if ($Body) { $RestSplat['Body'] = $Body | ConvertTo-Json -Depth 5 } if ($FullUri) { $RestSplat.Uri = $Uri } else { $RestSplat.Uri = Join-UriQuery -BaseUri $BaseUri -RelativeOrAbsoluteUri $Uri -QueryParameter $QueryParameter } if ($RestSplat['Body']) { $WhatIfInformation = "Invoking [$Method] " + [System.Environment]::NewLine + $RestSplat['Body'] + [System.Environment]::NewLine } else { $WhatIfInformation = "Invoking [$Method] " } try { if ($Method -eq 'GET') { Write-Verbose "Invoke-Graphimo - $($WhatIfInformation)over URI $($RestSplat.Uri)" $OutputQuery = Invoke-RestMethod @RestSplat -Verbose:$false # if ($OutputQuery.value) { # $FoundUsers = $OutputQuery.value # } # if ($First) { # if ($FoundUsers.Count -eq $First) { # return $FoundUsers # } elseif ($FoundUsers.Count -gt $First) { # $FoundUsers = $FoundUsers | Select-Object -First $First # return $FoundUsers # } else { # $First = $First - $FoundUsers.Count # $FoundUsers # } # } else { # $FoundUsers # } $Count = 1 [Array] $FoundUsers = Invoke-InternalGraphimo -OutputQuery $OutputQuery -First $First $CurrentCount = $FoundUsers.Count if ($First -gt 0 -and $CurrentCount -ge $First) { return $FoundUsers } else { $FoundUsers } if ($OutputQuery.'@odata.nextLink') { Do { $RestSplat.Uri = $OutputQuery.'@odata.nextLink' #$MoreData = Invoke-Graphimo @RestSplat -FullUri -First $First Write-Verbose "Invoke-Graphimo - $($WhatIfInformation)NextLink (Page $Count/Current Count: $($CurrentCount))) over URI $($RestSplat.Uri)" $OutputQuery = Invoke-RestMethod @RestSplat -Verbose:$false [Array] $FoundUsers = Invoke-InternalGraphimo -OutputQuery $OutputQuery -First $First -CurrentCount $CurrentCount $FoundUsers $Count++ $CurrentCount = $CurrentCount + $FoundUsers.Count } Until (($First -gt 0 -and $CurrentCount -ge $First) -or $null -eq $OutputQuery.'@odata.nextLink') } } else { Write-Verbose "Invoke-Graphimo - $($WhatIfInformation)over URI $($RestSplat.Uri)" if ($PSCmdlet.ShouldProcess($($RestSplat.Uri), $WhatIfInformation)) { $OutputQuery = Invoke-RestMethod @RestSplat -Verbose:$false if ($Method -in 'POST') { $OutputQuery } else { return $true } } } } catch { $RestError = $_.ErrorDetails.Message if ($RestError) { try { $ErrorMessage = ConvertFrom-Json -InputObject $RestError # Write-Warning -Message "Invoke-Graphimo - [$($ErrorMessage.error.code)] $($ErrorMessage.error.message), exception: $($_.Exception.Message)" Write-Warning -Message "Invoke-Graphimo - Error: $($_.Exception.Message) $($ErrorMessage.error.message)" } catch { Write-Warning -Message "Invoke-Graphimo - Error: $($_.Exception.Message)" } } else { Write-Warning -Message "Invoke-Graphimo - Error: $($_.Exception.Message)" } if ($Method -notin 'GET', 'POST') { return $false } } } function Remove-GraphGroupMember { [cmdletBinding(SupportsShouldProcess)] param( [parameter(Mandatory, ParameterSetName = 'All')] [parameter(Mandatory, ParameterSetName = 'PerID')] [parameter(Mandatory, ParameterSetName = 'BySearch')] [alias('Authorization')][System.Collections.IDictionary] $Headers, [parameter(Mandatory, ParameterSetName = 'All')] [parameter(Mandatory, ParameterSetName = 'PerID')] [parameter(Mandatory, ParameterSetName = 'BySearch')] [alias('GroupID')][string] $ID, [parameter(Mandatory, ParameterSetName = 'PerID')][string] $MemberID, [parameter(Mandatory, ParameterSetName = 'BySearch')][string] $Search, [parameter(ParameterSetName = 'All')][switch] $All ) if ($All) { # Lets remove all, but to do that we need to know who to remove $Users = Get-GraphGroupMember -Id $ID -Headers $Headers -Verbose -Property id, displayName foreach ($User in $Users) { $URI = "/groups/$ID/members/$($User.id)/`$ref" $Status = Invoke-Graphimo -Uri $URI -Method DELETE -Headers $Headers $Status } } elseif ($Search) { $Users = Get-GraphGroupMember -Id $ID -Headers $Headers -Verbose -Property id, displayName -Search $Search foreach ($User in $Users) { $URI = "/groups/$ID/members/$($User.id)/`$ref" $Status = Invoke-Graphimo -Uri $URI -Method DELETE -Headers $Headers $Status } } else { # Lets delete just one record $URI = "/groups/$ID/members/$MemberID/`$ref" Invoke-Graphimo -Uri $URI -Method DELETE -Headers $Headers } } function Remove-GraphManager { [cmdletBinding(SupportsShouldProcess)] param( [parameter(Mandatory)][alias('Authorization')][System.Collections.IDictionary] $Headers, [alias('UserID')][string] $ID, [string] $UserPrincipalName ) if ($ID) { $URI = "/users/$ID/manager/`$ref" } else { $URI = "/users/$UserPrincipalName/manager/`$ref" } Invoke-Graphimo -Uri $URI -Method DELETE -Headers $Headers } function Remove-GraphUser { [cmdletBinding(SupportsShouldProcess)] param( [parameter(Mandatory)][alias('Authorization')][System.Collections.IDictionary] $Headers, [string] $UserPrincipalName, [alias('UserID')][string] $ID ) if ($ID) { $URI = "/users/$ID" } else { $URI = "/users/$UserPrincipalName" } Invoke-Graphimo -Uri $URI -Method DELETE -Headers $Headers } function Set-GraphManager { [cmdletBinding(SupportsShouldProcess)] param( [parameter(Mandatory)][alias('Authorization')][System.Collections.IDictionary] $Headers, [alias('UserID')][string] $ID, [string] $UserPrincipalName, [string] $Name, [string] $ManagerID, [string] $ManagerDisplayName ) if ($ID) { $URI = "/users/$ID/manager/`$ref" } else { $URI = "/users/$UserPrincipalName/manager/`$ref" } $Body = [ordered]@{ "@odata.id" = "https://graph.microsoft.com/v1.0/users/$ManagerID" } Invoke-Graphimo -Uri $URI -Method PUT -Headers $Headers -Body $Body } function Set-GraphUser { [cmdletBinding(SupportsShouldProcess)] param( [parameter(Mandatory)][alias('Authorization')][System.Collections.IDictionary] $Headers, [alias('UserID')][string] $ID, [string] $SearchUserPrincipalName, [string] $UserPrincipalName, [string] $Name, [alias('AccountEnabled')][nullable[bool]] $Enabled, [alias('FirstName')][string] $GivenName, [alias('LastName')][string] $Surname, [alias('Title')][string] $JobTitle, [string] $EmployeeId, [string] $City, [string] $MailNickname, [alias('EmailAddress')][string] $Mail, [string] $Country, [string] $Department, [string] $PostalCode, [alias('Fax')][string] $FaxNumber, [string] $State, [string] $StreetAddress, [alias('OfficePhone')][string] $BusinessPhones, [alias('Mobile')][string] $MobilePhone, [string] $OfficeLocation, [string] $CompanyName, [string] $DisplayName, [string] $EmployeeType, [switch] $ShowInAddressList, [alias('HireDate')][DateTime] $StartDate, [alias('CustomProperty')][System.Collections.IDictionary] $CustomProperties, [string] $ExtensionAttribute1, [string] $ExtensionAttribute2, [string] $ExtensionAttribute3, [string] $ExtensionAttribute4, [string] $ExtensionAttribute5, [string] $ExtensionAttribute6, [string] $ExtensionAttribute7, [string] $ExtensionAttribute8, [string] $ExtensionAttribute9, [string] $ExtensionAttribute10, [string] $ExtensionAttribute11, [string] $ExtensionAttribute12, [string] $ExtensionAttribute13, [string] $ExtensionAttribute14, [string] $ExtensionAttribute15, [System.Collections.IDictionary] $OnPremisesExtensionAttributes ) $Body = [ordered]@{} # https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0 if ($PSBoundParameters.ContainsKey('StartDate')) { # requires fixing # The date and time when the user was hired or will start work in case of a future hire. $Body['employeeHireDate'] = $StartDate } if ($PSBoundParameters.ContainsKey('JobTitle')) { $Body['jobTitle'] = $JobTitle } if ($PSBoundParameters.ContainsKey('EmployeeId')) { $Body['employeeId'] = $EmployeeId } if ($PSBoundParameters.ContainsKey('UserPrincipalName')) { $Body['userPrincipalName'] = $UserPrincipalName } if ($PSBoundParameters.ContainsKey('MailNickname')) { $Body['mailNickname'] = $MailNickname } if ($PSBoundParameters.ContainsKey('Mail')) { $Body['mail'] = $Mail } if ($PSBoundParameters.ContainsKey('FaxNumber')) { $Body['faxNumber'] = $FaxNumber } if ($PSBoundParameters.ContainsKey('givenName')) { $Body['givenName'] = $givenName } if ($PSBoundParameters.ContainsKey('Surname')) { $Body['surname'] = $Surname } if ($PSBoundParameters.ContainsKey('City')) { $Body['city'] = $City } if ($PSBoundParameters.ContainsKey('Country')) { $Body['country'] = $Country } if ($PSBoundParameters.ContainsKey('Department')) { $Body['department'] = $Department } if ($PSBoundParameters.ContainsKey('PostalCode')) { $Body['postalCode'] = $PostalCode } if ($PSBoundParameters.ContainsKey('State')) { $Body['state'] = $State } if ($PSBoundParameters.ContainsKey('StreetAddress')) { $Body['streetAddress'] = $StreetAddress } if ($PSBoundParameters.ContainsKey('businessPhones')) { $Body['businessPhones'] = @($businessPhones) } if ($PSBoundParameters.ContainsKey('mobilePhone')) { $Body['mobilePhone'] = $mobilePhone } if ($PSBoundParameters.ContainsKey('OfficeLocation')) { $Body['officeLocation'] = $OfficeLocation } if ($PSBoundParameters.ContainsKey('CompanyName')) { $Body['companyName'] = $CompanyName } if ($PSBoundParameters.ContainsKey('DisplayName')) { $Body['displayName'] = $DisplayName } if ($PSBoundParameters.ContainsKey('ShowInAddressList')) { $Body['showInAddressList'] = $ShowInAddressList.IsPresent } if ($PSBoundParameters.ContainsKey('Enabled')) { $Body['accountEnabled'] = $Enabled } if ($PSBoundParameters.ContainsKey('EmployeeType')) { $Body['employeeType'] = $EmployeeType $BaseUri = 'https://graph.microsoft.com/beta' } else { $BaseUri = 'https://graph.microsoft.com/v1.0' } foreach ($Property in $CustomProperties.Keys) { $Body[$Property] = $CustomProperties[$Property] } $Body['onPremisesExtensionAttributes'] = [ordered] @{} if ($PSBoundParameters.ContainsKey('ExtensionAttribute1')) { $Body['onPremisesExtensionAttributes']['extensionAttribute1'] = $ExtensionAttribute1 } if ($PSBoundParameters.ContainsKey('ExtensionAttribute2')) { $Body['onPremisesExtensionAttributes']['extensionAttribute2'] = $ExtensionAttribute2 } if ($PSBoundParameters.ContainsKey('ExtensionAttribute3')) { $Body['onPremisesExtensionAttributes']['extensionAttribute3'] = $ExtensionAttribute3 } if ($PSBoundParameters.ContainsKey('ExtensionAttribute4')) { $Body['onPremisesExtensionAttributes']['extensionAttribute4'] = $ExtensionAttribute4 } if ($PSBoundParameters.ContainsKey('ExtensionAttribute5')) { $Body['onPremisesExtensionAttributes']['extensionAttribute5'] = $ExtensionAttribute5 } if ($PSBoundParameters.ContainsKey('ExtensionAttribute6')) { $Body['onPremisesExtensionAttributes']['extensionAttribute6'] = $ExtensionAttribute6 } if ($PSBoundParameters.ContainsKey('ExtensionAttribute7')) { $Body['onPremisesExtensionAttributes']['extensionAttribute7'] = $ExtensionAttribute7 } if ($PSBoundParameters.ContainsKey('ExtensionAttribute8')) { $Body['onPremisesExtensionAttributes']['extensionAttribute8'] = $ExtensionAttribute8 } if ($PSBoundParameters.ContainsKey('ExtensionAttribute9')) { $Body['onPremisesExtensionAttributes']['extensionAttribute9'] = $ExtensionAttribute9 } if ($PSBoundParameters.ContainsKey('ExtensionAttribute10')) { $Body['onPremisesExtensionAttributes']['extensionAttribute10'] = $ExtensionAttribute10 } if ($PSBoundParameters.ContainsKey('ExtensionAttribute11')) { $Body['onPremisesExtensionAttributes']['extensionAttribute11'] = $ExtensionAttribute11 } if ($PSBoundParameters.ContainsKey('ExtensionAttribute12')) { $Body['onPremisesExtensionAttributes']['extensionAttribute12'] = $ExtensionAttribute12 } if ($PSBoundParameters.ContainsKey('ExtensionAttribute13')) { $Body['onPremisesExtensionAttributes']['extensionAttribute13'] = $ExtensionAttribute13 } if ($PSBoundParameters.ContainsKey('ExtensionAttribute14')) { $Body['onPremisesExtensionAttributes']['extensionAttribute14'] = $ExtensionAttribute14 } if ($PSBoundParameters.ContainsKey('ExtensionAttribute15')) { $Body['onPremisesExtensionAttributes']['extensionAttribute15'] = $ExtensionAttribute15 } if ($ID) { $URI = "/users/$ID" } else { $URI = "/users/$SearchUserPrincipalName" } if ($Body['onPremisesExtensionAttributes'].Count -eq 0) { $Body.Remove('onPremisesExtensionAttributes') } if ($Body.Count -gt 0) { $UriEncoded = [System.Web.HttpUtility]::UrlEncode($Uri) Invoke-Graphimo -Uri $UriEncoded -Method PATCH -Headers $Headers -Body $Body -BaseUri $BaseUri } else { Write-Warning -Message "Set-GraphUser - No changes were made to the user, as no field to change." } } # Export functions and aliases as required Export-ModuleMember -Function @('Add-GraphGroup', 'Add-GraphGroupMember', 'Add-GraphUser', 'Connect-Graphimo', 'Get-GraphApplication', 'Get-GraphContact', 'Get-GraphGroup', 'Get-GraphGroupMember', 'Get-GraphUser', 'Import-GraphGuest', 'Invoke-Graphimo', 'Remove-GraphGroupMember', 'Remove-GraphManager', 'Remove-GraphUser', 'Set-GraphManager', 'Set-GraphUser') -Alias @('Get-GraphUsers') # SIG # Begin signature block # MIInPgYJKoZIhvcNAQcCoIInLzCCJysCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCACtkycI1P3l098 # B249kg7C1wdObIFHxrAtof9VI+Tha6CCITcwggO3MIICn6ADAgECAhAM5+DlF9hG # /o/lYPwb8DA5MA0GCSqGSIb3DQEBBQUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBa # Fw0zMTExMTAwMDAwMDBaMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lD # ZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC # AQoCggEBAK0OFc7kQ4BcsYfzt2D5cRKlrtwmlIiq9M71IDkoWGAM+IDaqRWVMmE8 # tbEohIqK3J8KDIMXeo+QrIrneVNcMYQq9g+YMjZ2zN7dPKii72r7IfJSYd+fINcf # 4rHZ/hhk0hJbX/lYGDW8R82hNvlrf9SwOD7BG8OMM9nYLxj+KA+zp4PWw25EwGE1 # lhb+WZyLdm3X8aJLDSv/C3LanmDQjpA1xnhVhyChz+VtCshJfDGYM2wi6YfQMlqi # uhOCEe05F52ZOnKh5vqk2dUXMXWuhX0irj8BRob2KHnIsdrkVxfEfhwOsLSSplaz # vbKX7aqn8LfFqD+VFtD/oZbrCF8Yd08CAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGG # MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEXroq/0ksuCMS1Ri6enIZ3zbcgP # MB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBBQUA # A4IBAQCiDrzf4u3w43JzemSUv/dyZtgy5EJ1Yq6H6/LV2d5Ws5/MzhQouQ2XYFwS # TFjk0z2DSUVYlzVpGqhH6lbGeasS2GeBhN9/CTyU5rgmLCC9PbMoifdf/yLil4Qf # 6WXvh+DfwWdJs13rsgkq6ybteL59PyvztyY1bV+JAbZJW58BBZurPSXBzLZ/wvFv # hsb6ZGjrgS2U60K3+owe3WLxvlBnt2y98/Efaww2BxZ/N3ypW2168RJGYIPXJwS+ # S86XvsNnKmgR34DnDDNmvxMNFG7zfx9jEB76jRslbWyPpbdhAbHSoyahEHGdreLD # +cOZUbcrBwjOLuZQsqf6CkUvovDyMIIFMDCCBBigAwIBAgIQBAkYG1/Vu2Z1U0O1 # b5VQCDANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGln # aUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtE # aWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMTMxMDIyMTIwMDAwWhcNMjgx # MDIyMTIwMDAwWjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5j # MRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBT # SEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEF # AAOCAQ8AMIIBCgKCAQEA+NOzHH8OEa9ndwfTCzFJGc/Q+0WZsTrbRPV/5aid2zLX # cep2nQUut4/6kkPApfmJ1DcZ17aq8JyGpdglrA55KDp+6dFn08b7KSfH03sjlOSR # I5aQd4L5oYQjZhJUM1B0sSgmuyRpwsJS8hRniolF1C2ho+mILCCVrhxKhwjfDPXi # TWAYvqrEsq5wMWYzcT6scKKrzn/pfMuSoeU7MRzP6vIK5Fe7SrXpdOYr/mzLfnQ5 # Ng2Q7+S1TqSp6moKq4TzrGdOtcT3jNEgJSPrCGQ+UpbB8g8S9MWOD8Gi6CxR93O8 # vYWxYoNzQYIH5DiLanMg0A9kczyen6Yzqf0Z3yWT0QIDAQABo4IBzTCCAckwEgYD # VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYB # BQUHAwMweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k # aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0 # LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4 # oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJv # b3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy # dEFzc3VyZWRJRFJvb3RDQS5jcmwwTwYDVR0gBEgwRjA4BgpghkgBhv1sAAIEMCow # KAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCgYIYIZI # AYb9bAMwHQYDVR0OBBYEFFrEuXsqCqOl6nEDwGD5LfZldQ5YMB8GA1UdIwQYMBaA # FEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBCwUAA4IBAQA+7A1aJLPz # ItEVyCx8JSl2qB1dHC06GsTvMGHXfgtg/cM9D8Svi/3vKt8gVTew4fbRknUPUbRu # pY5a4l4kgU4QpO4/cY5jDhNLrddfRHnzNhQGivecRk5c/5CxGwcOkRX7uq+1UcKN # JK4kxscnKqEpKBo6cSgCPC6Ro8AlEeKcFEehemhor5unXCBc2XGxDI+7qPjFEmif # z0DLQESlE/DmZAwlCEIysjaKJAL+L3J+HNdJRZboWR3p+nRka7LrZkPas7CM1ekN # 3fYBIM6ZMWM9CBoYs4GbT8aTEAb8B4H6i9r5gkn3Ym6hU/oSlBiFLpKR6mhsRDKy # ZqHnGKSaZFHvMIIFPTCCBCWgAwIBAgIQBNXcH0jqydhSALrNmpsqpzANBgkqhkiG # 9w0BAQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkw # FwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEy # IEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDYyNjAwMDAwMFoXDTIz # MDcwNzEyMDAwMFowejELMAkGA1UEBhMCUEwxEjAQBgNVBAgMCcWabMSFc2tpZTER # MA8GA1UEBxMIS2F0b3dpY2UxITAfBgNVBAoMGFByemVteXPFgmF3IEvFgnlzIEVW # T1RFQzEhMB8GA1UEAwwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMIIBIjANBgkq # hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7KB3iyBrhkLUbbFe9qxhKKPBYqDBqln # r3AtpZplkiVjpi9dMZCchSeT5ODsShPuZCIxJp5I86uf8ibo3vi2S9F9AlfFjVye # 3dTz/9TmCuGH8JQt13ozf9niHecwKrstDVhVprgxi5v0XxY51c7zgMA2g1Ub+3ti # i0vi/OpmKXdL2keNqJ2neQ5cYly/GsI8CREUEq9SZijbdA8VrRF3SoDdsWGf3tZZ # zO6nWn3TLYKQ5/bw5U445u/V80QSoykszHRivTj+H4s8ABiforhi0i76beA6Ea41 # zcH4zJuAp48B4UhjgRDNuq8IzLWK4dlvqrqCBHKqsnrF6BmBrv+BXQIDAQABo4IB # xTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0OBBYE # FBixNSfoHFAgJk4JkDQLFLRNlJRmMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK # BggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdpY2Vy # dC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2NybDQu # ZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUwQzA3 # BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu # Y29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNpZ25p # bmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAmr1sz4ls # LARi4wG1eg0B8fVJFowtect7SnJUrp6XRnUG0/GI1wXiLIeow1UPiI6uDMsRXPHU # F/+xjJw8SfIbwava2eXu7UoZKNh6dfgshcJmo0QNAJ5PIyy02/3fXjbUREHINrTC # vPVbPmV6kx4Kpd7KJrCo7ED18H/XTqWJHXa8va3MYLrbJetXpaEPpb6zk+l8Rj9y # G4jBVRhenUBUUj3CLaWDSBpOA/+sx8/XB9W9opYfYGb+1TmbCkhUg7TB3gD6o6ES # Jre+fcnZnPVAPESmstwsT17caZ0bn7zETKlNHbc1q+Em9kyBjaQRcEQoQQNpezQu # g9ufqExx6lHYDjCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZI # hvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ # MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNz # dXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVow # YjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290 # IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjww # IjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J5 # 8soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMH # hOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6 # Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQ # ecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4b # A3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9 # WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCU # tNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvo # ZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/J # vNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCP # orF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMB # Af8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXr # oq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRt # MGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEF # BQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl # ZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgw # BgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cH # vZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8 # UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTn # f+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxU # jG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8j # LfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDCCBq4w # ggSWoAMCAQICEAc2N7ckVHzYR6z9KGYqXlswDQYJKoZIhvcNAQELBQAwYjELMAkG # A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp # Z2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4X # DTIyMDMyMzAwMDAwMFoXDTM3MDMyMjIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAV # BgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVk # IEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQTCCAiIwDQYJKoZIhvcN # AQEBBQADggIPADCCAgoCggIBAMaGNQZJs8E9cklRVcclA8TykTepl1Gh1tKD0Z5M # om2gsMyD+Vr2EaFEFUJfpIjzaPp985yJC3+dH54PMx9QEwsmc5Zt+FeoAn39Q7SE # 2hHxc7Gz7iuAhIoiGN/r2j3EF3+rGSs+QtxnjupRPfDWVtTnKC3r07G1decfBmWN # lCnT2exp39mQh0YAe9tEQYncfGpXevA3eZ9drMvohGS0UvJ2R/dhgxndX7RUCyFo # bjchu0CsX7LeSn3O9TkSZ+8OpWNs5KbFHc02DVzV5huowWR0QKfAcsW6Th+xtVhN # ef7Xj3OTrCw54qVI1vCwMROpVymWJy71h6aPTnYVVSZwmCZ/oBpHIEPjQ2OAe3Vu # JyWQmDo4EbP29p7mO1vsgd4iFNmCKseSv6De4z6ic/rnH1pslPJSlRErWHRAKKtz # Q87fSqEcazjFKfPKqpZzQmiftkaznTqj1QPgv/CiPMpC3BhIfxQ0z9JMq++bPf4O # uGQq+nUoJEHtQr8FnGZJUlD0UfM2SU2LINIsVzV5K6jzRWC8I41Y99xh3pP+OcD5 # sjClTNfpmEpYPtMDiP6zj9NeS3YSUZPJjAw7W4oiqMEmCPkUEBIDfV8ju2TjY+Cm # 4T72wnSyPx4JduyrXUZ14mCjWAkBKAAOhFTuzuldyF4wEr1GnrXTdrnSDmuZDNIz # tM2xAgMBAAGjggFdMIIBWTASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBS6 # FtltTYUvcyl2mi91jGogj57IbzAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qY # rhwPTzAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwdwYIKwYB # BQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w # QQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy # dFRydXN0ZWRSb290RzQuY3J0MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwz # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3JsMCAGA1UdIAQZ # MBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAfVmO # wJO2b5ipRCIBfmbW2CFC4bAYLhBNE88wU86/GPvHUF3iSyn7cIoNqilp/GnBzx0H # 6T5gyNgL5Vxb122H+oQgJTQxZ822EpZvxFBMYh0MCIKoFr2pVs8Vc40BIiXOlWk/ # R3f7cnQU1/+rT4osequFzUNf7WC2qk+RZp4snuCKrOX9jLxkJodskr2dfNBwCnzv # qLx1T7pa96kQsl3p/yhUifDVinF2ZdrM8HKjI/rAJ4JErpknG6skHibBt94q6/ae # sXmZgaNWhqsKRcnfxI2g55j7+6adcq/Ex8HBanHZxhOACcS2n82HhyS7T6NJuXdm # kfFynOlLAlKnN36TU6w7HQhJD5TNOXrd/yVjmScsPT9rp/Fmw0HNT7ZAmyEhQNC3 # EyTN3B14OuSereU0cZLXJmvkOHOrpgFPvT87eK1MrfvElXvtCl8zOYdBeHo46Zzh # 3SP9HSjTx/no8Zhf+yvYfvJGnXUsHicsJttvFXseGYs2uJPU5vIXmVnKcPA3v5gA # 3yAWTyf7YGcWoWa63VXAOimGsJigK+2VQbc61RWYMbRiCQ8KvYHZE/6/pNHzV9m8 # BPqC3jLfBInwAM1dwvnQI38AC+R2AibZ8GV2QqYphwlHK+Z/GqSFD/yYlvZVVCsf # gPrA8g4r5db7qS9EFUrnEw4d2zc4GqEr9u3WfPwwggbAMIIEqKADAgECAhAMTWly # S5T6PCpKPSkHgD1aMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYD # VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH # NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjIwOTIxMDAwMDAw # WhcNMzMxMTIxMjM1OTU5WjBGMQswCQYDVQQGEwJVUzERMA8GA1UEChMIRGlnaUNl # cnQxJDAiBgNVBAMTG0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDIyIC0gMjCCAiIwDQYJ # KoZIhvcNAQEBBQADggIPADCCAgoCggIBAM/spSY6xqnya7uNwQ2a26HoFIV0Mxom # rNAcVR4eNm28klUMYfSdCXc9FZYIL2tkpP0GgxbXkZI4HDEClvtysZc6Va8z7GGK # 6aYo25BjXL2JU+A6LYyHQq4mpOS7eHi5ehbhVsbAumRTuyoW51BIu4hpDIjG8b7g # L307scpTjUCDHufLckkoHkyAHoVW54Xt8mG8qjoHffarbuVm3eJc9S/tjdRNlYRo # 44DLannR0hCRRinrPibytIzNTLlmyLuqUDgN5YyUXRlav/V7QG5vFqianJVHhoV5 # PgxeZowaCiS+nKrSnLb3T254xCg/oxwPUAY3ugjZNaa1Htp4WB056PhMkRCWfk3h # 3cKtpX74LRsf7CtGGKMZ9jn39cFPcS6JAxGiS7uYv/pP5Hs27wZE5FX/NurlfDHn # 88JSxOYWe1p+pSVz28BqmSEtY+VZ9U0vkB8nt9KrFOU4ZodRCGv7U0M50GT6Vs/g # 9ArmFG1keLuY/ZTDcyHzL8IuINeBrNPxB9ThvdldS24xlCmL5kGkZZTAWOXlLimQ # prdhZPrZIGwYUWC6poEPCSVT8b876asHDmoHOWIZydaFfxPZjXnPYsXs4Xu5zGcT # B5rBeO3GiMiwbjJ5xwtZg43G7vUsfHuOy2SJ8bHEuOdTXl9V0n0ZKVkDTvpd6kVz # HIR+187i1Dp3AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/ # BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEE # AjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8w # HQYDVR0OBBYEFGKK3tBh/I8xFO2XC809KpQU31KcMFoGA1UdHwRTMFEwT6BNoEuG # SWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQw # OTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQG # CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKG # TGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJT # QTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIB # AFWqKhrzRvN4Vzcw/HXjT9aFI/H8+ZU5myXm93KKmMN31GT8Ffs2wklRLHiIY1UJ # RjkA/GnUypsp+6M/wMkAmxMdsJiJ3HjyzXyFzVOdr2LiYWajFCpFh0qYQitQ/Bu1 # nggwCfrkLdcJiXn5CeaIzn0buGqim8FTYAnoo7id160fHLjsmEHw9g6A++T/350Q # p+sAul9Kjxo6UrTqvwlJFTU2WZoPVNKyG39+XgmtdlSKdG3K0gVnK3br/5iyJpU4 # GYhEFOUKWaJr5yI+RCHSPxzAm+18SLLYkgyRTzxmlK9dAlPrnuKe5NMfhgFknADC # 6Vp0dQ094XmIvxwBl8kZI4DXNlpflhaxYwzGRkA7zl011Fk+Q5oYrsPJy8P7mxNf # arXH4PMFw1nfJ2Ir3kHJU7n/NBBn9iYymHv+XEKUgZSCnawKi8ZLFUrTmJBFYDOA # 4CPe+AOk9kVH5c64A0JH6EE2cXet/aLol3ROLtoeHYxayB6a1cLwxiKoT5u92Bya # UcQvmvZfpyeXupYuhVfAYOd4Vn9q78KVmksRAsiCnMkaBXy6cbVOepls9Oie1FqY # yJ+/jbsYXEP10Cro4mLueATbvdH7WwqocH7wl4R44wgDXUcsY6glOJcB0j862uXl # 9uab3H4szP8XTE0AotjWAQ64i+7m4HJViSwnGWH2dwGMMYIFXTCCBVkCAQEwgYYw # cjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVk # IElEIENvZGUgU2lnbmluZyBDQQIQBNXcH0jqydhSALrNmpsqpzANBglghkgBZQME # AgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEM # BgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqG # SIb3DQEJBDEiBCBkcwDqCENA2KhcfMuQvVkTjLTVMFtwnBNBvFkQv2b+OjANBgkq # hkiG9w0BAQEFAASCAQBVchtdmcuj5jVcuHRCh71UvoA0uZQ0mJx+Az0X4+TdZV4N # Mc8k5wyAtKBVKuzEWJ1YTPnrGvmEwA1GTJznVh5sE8As+shUaNANVSnRGEIbscxq # MdiEWJ7+SQsjurPw4NFev0zMHsQMeOSdexdFvfj0MnmeeqOH3/c0HAk+CituQUyv # m3I9aBPSKa9Fx+OZlPim5l3ccP8QCnQTauwchRqL9Kd9w8jzAGNj9WlD2wCiGmQV # /s2kgMi+HvytgsMZJbOxgvnhdZ6kz90ZwKHJy+bl0DLU5AAb80icBrxtlh+SY+RF # l4ZaASEUtc3bEDhWxQ/0UGZr6LGPg2zsVwRD7QxjoYIDIDCCAxwGCSqGSIb3DQEJ # BjGCAw0wggMJAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0 # LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hB # MjU2IFRpbWVTdGFtcGluZyBDQQIQDE1pckuU+jwqSj0pB4A9WjANBglghkgBZQME # AgEFAKBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X # DTIzMDQxMTE5MzEwM1owLwYJKoZIhvcNAQkEMSIEIJNxfzc1UQ3jJkZyY984u/US # /tsUYjE6+d3WimpRwJedMA0GCSqGSIb3DQEBAQUABIICAABd/sFt6gKOu0eMj18t # FMPxj5zgq+j/7zUHBpPrhcGND2iRW0hFjUawaFtX849Q3LNb+dafHWU4Y7EN2U6s # owUUEOwKAirAltpqKu/VHHtuG44smpUSKPyqKM7uTfIrq4cGsXdV0daoDtpqj4BI # wSqjbLpLJV0SL8MaOuG9bkgRKIpdzvRpKapK+3WVTkvFrAM5WyKZEG0j4XfcTna/ # pXKj+zdJWS/vyQH8JdyUNalkWzviwZL4dACC/RsljnaQd6gmexrbUPWZx58W9Fi7 # Xi6wbR92CwGUuy1tGmgpSXjXNsSe4orut1lA4+z9S5TIByz8djZvwB4jNcfsStL0 # O3WS9bjfHOMMzzvU6q4FgXEFS1mM4MBUBB6WSeI4FVqDiHEYbpfGPrmPwqastke+ # oV0wTUWhPNCqsBqyaunMjjt4vNxEW73iw5dNBj3qsnOICzx6EGLIr6RsAPh/z6My # UWayueS6V4i23y5Zfn5VhG5vLq8IEIqm5d+mWa6c3f9xcyhYLnG237ByqFVA70FO # 4dMd44JcFgA0XpHW2WNtUar455HIb/UkOk63D/9yp1anZ09Ez/TnSv+jBa8bDdS7 # BowVbZ774nGnltXXjoJ2afU74iGFS9JaIiySCOcArG5Bpa/iOY/T5riaDGXC+sBS # KKVBiP/FbtIk67qaJZwjTVM4 # SIG # End signature block |