PartnerCustomerCommunity.psm1
using namespace Microsoft.Store.PartnerCenter using namespace Microsoft.Store.PartnerCenter.Models using namespace Microsoft.Store.PartnerCenter.Extensions # Install-Package -Name 'Microsoft.Store.PartnerCenter' -SkipDependencies -Verbose -Scope CurrentUser # Install-Package -Name 'Microsoft.Identity.Client' -SkipDependencies -Verbose -Scope CurrentUser # Copy-Item "$env:LOCALAPPDATA\PackageManagement\NuGet\Packages\Microsoft.Identity.Client.4.46.1\lib\netcoreapp2.1\Microsoft.Identity.Client.dll" '.\lib\' # Copy-Item "$env:LOCALAPPDATA\PackageManagement\NuGet\Packages\Microsoft.Store.PartnerCenter.3.1.2\lib\netstandard2.0\Microsoft.Store.PartnerCenter.dll" '.\lib\' # Copy-Item "$env:LOCALAPPDATA\PackageManagement\NuGet\Packages\Microsoft.Store.PartnerCenter.3.1.2\lib\netstandard2.0\Microsoft.Store.PartnerCenter.Models.dll" '.\lib\' # Copy-Item "$env:LOCALAPPDATA\PackageManagement\NuGet\Packages\Microsoft.Store.PartnerCenter.3.1.2\lib\netstandard2.0\Microsoft.Store.PartnerCenter.Extensions.dll" '.\lib\' # Add-Type -Path (Resolve-Path -Path "$PSScriptRoot\lib\Microsoft.Identity.Client.dll") # Add-Type -Path (Resolve-Path -Path "$PSScriptRoot\lib\Microsoft.Store.PartnerCenter.dll") # Add-Type -Path (Resolve-Path -Path "$PSScriptRoot\lib\Microsoft.Store.PartnerCenter.Models.dll") # Add-Type -Path (Resolve-Path -Path "$PSScriptRoot\lib\Microsoft.Store.PartnerCenter.Extensions.dll") if (!(Get-Module -Name 'PSRunspacedDelegate')) { Import-Module -Name "$PSScriptRoot\PSRunspacedDelegate" } $ErrorActionPreference = 'Stop' function New-PartnerAccessToken { <# .OUTPUTS [String] Temporary token. [DateTimeOffset] Temporary token expiration time. .NOTES https://docs.microsoft.com/en-us/partner-center/develop/enable-secure-app-model#get-access-token https://www.powershellgallery.com/packages/PartnerCenterLW/1.1 #> param ( # Application ID and secret. [Parameter(Mandatory)] [PSCredential]$Credential, # User token MFA authenticated. [Parameter(Mandatory)] [String]$RefreshToken, # Limit to specific tenant. [string]$Tenant = 'common', [ValidateSet('Raw', 'Minimal')] [string]$OutputFormat = 'Minimal' ) if (!$Tenant) { $Tenant = 'common' } $AuthBody = @{ client_id = $Credential.UserName refresh_token = $RefreshToken grant_type = "refresh_token" client_secret = $Credential.GetNetworkCredential().Password } $Uri = "https://login.microsoftonline.com/$Tenant/oauth2/token" $ReturnCred = Invoke-RestMethod -Uri $Uri -ContentType "application/x-www-form-urlencoded" -Method POST -Body $AuthBody -ErrorAction Stop if ($OutputFormat -eq 'Raw') { $ReturnCred } elseif ($OutputFormat -eq 'Minimal') { [PSCustomObject][ordered]@{ AccessTokenExpiration = [DateTimeOffset]::FromUnixTimeSeconds($ReturnCred.expires_on).LocalDateTime AccessToken = $ReturnCred.access_token RefreshToken = $ReturnCred.refresh_token } } } function New-PartnerRefreshToken { <# .SYNOPSIS Uses device code flow to get a refresh token for the Partner Center API. .DESCRIPTION Gets a refresh token for the Partner Center API using an authorization code (with device code flow) and a second call to get the refresh token. .EXAMPLE New-PartnerRefreshToken -Tenant $Tenant -ApplicationId $ApplicationId .NOTES https://docs.microsoft.com/en-us/partner-center/develop/enable-secure-app-model #> param( # Limit to specific tenant. [string]$Tenant = 'common', # Your CSP/Partner Center application/Client ID. [Parameter(Mandatory)] [string]$ApplicationId, # Scope of the RefreshToken. Each endpoint needs its own consented RefreshToken. [ArgumentCompleter({ 'user.read', 'openid', 'profile', 'https://api.partnercenter.microsoft.com/user_impersonation', 'https://outlook.office365.com/.default' })] [string[]]$Scopes = @('https://api.partnercenter.microsoft.com/user_impersonation'), # Authorization grant flow. # https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code [ValidateSet('OIDC', 'DeviceCode')] [string]$AuthenticationFlow = 'DeviceCode', [ValidateSet('Raw', 'OnlyRefreshToken')] [string]$OutputFormat = 'OnlyRefreshToken' ) if (!$Tenant) { $Tenant = 'common' } $CodeBody = @{ client_id = $ApplicationId scope = $Scopes -join ' ' } if ($AuthenticationFlow -eq 'DeviceCode') { # Get the authorization code. $AuthorizationCodeResponse = Invoke-RestMethod -Method POST -Uri "https://login.microsoftonline.com/$Tenant/oauth2/v2.0/devicecode" -Body $CodeBody Write-Warning $AuthorizationCodeResponse.message # Get the RefreshToken. $RefreshTokenBody = @{ grant_type = 'urn:ietf:params:oauth:grant-type:device_code' code = $AuthorizationCodeResponse.device_code client_id = $ApplicationId } while ([string]::IsNullOrEmpty($RefreshTokenResponse.access_token)) { Start-Sleep -Seconds 5 $RefreshTokenResponse = try { Invoke-RestMethod -Method POST -Uri "https://login.microsoftonline.com/$Tenant/oauth2/token" -Body $RefreshTokenBody } catch { $ErrorMessage = $_.ErrorDetails.Message | ConvertFrom-Json # If not waiting for auth, throw error if ($ErrorMessage.error -ne "authorization_pending") { if ($ErrorMessage.error_description -like '*AADSTS7000218*') { throw ('"-Flow DeviceCode" requires "Allow public client flows" in Azure Portal -> "App registrations" -> "Authentication". Original Error:' + [Environment]::NewLine + $_.ErrorDetails.Message) } throw $_ } } } } elseif ($AuthenticationFlow -eq 'OIDC') { throw '"-Flow OIDC" is not supported yet.' } if ($OutputFormat -eq 'Raw') { $RefreshTokenResponse } elseif ($OutputFormat -eq 'OnlyRefreshToken') { $RefreshTokenResponse.refresh_token } } function New-PartnerWebApp { <# .SYNOPSIS Creates a new Azure web app for Partner Center. .NOTES https://docs.microsoft.com/en-us/partner-center/develop/enable-secure-app-model#create-a-web-app Updated version of https://www.cyberdrain.com/connect-to-exchange-online-automated-when-mfa-is-enabled-using-the-secureapp-model/ #> param( # Limit to specific tenant. [string]$Tenant, [Parameter(Mandatory)] [string]$DisplayName, # Authorization grant flow. # https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code [ValidateSet('OIDC', 'DeviceCode')] [string]$AuthenticationFlow = 'OIDC', # Use this if you need the WebApp to support `New-PartnerRefreshToken -AuthenticationFlow DeviceCode` [switch]$IsFallbackPublicClient, # Stay connected to MgGraph. [switch]$StayConnected, [ValidateSet('Raw', 'Minimal')] [string]$OutputFormat = 'Minimal' ) # Check if the Azure AD PowerShell module has already been loaded. $Modules = 'Microsoft.Graph.Authentication', 'Microsoft.Graph.Applications', 'Microsoft.Graph.Groups' $ModulesToImport = @() $ModulesToInstall = @() foreach ($Module in $Modules) { if (!(Get-Module -Name $Module)) { # Check if the Azure AD PowerShell module is installed. if (Get-Module -Name $Module -ListAvailable) { # The Azure AD PowerShell module is not load and it is installed. This module # must be loaded for other operations performed by this script. $ModulesToImport += $Module } else { $ModulesToInstall += $Module } } } if ($ModulesToImport) { Write-Host -ForegroundColor Green "Loading the $ModulesToImport PowerShell modules..." Import-Module $ModulesToImport } elseif ($ModulesToInstall) { Write-Host -ForegroundColor Green "Installing the $ModulesToInstall PowerShell modules..." Install-Module $ModulesToInstall } $MgGraphParams = @{ Scopes = 'Application.ReadWrite.All', 'User.Read', 'Group.Read.All', 'GroupMember.ReadWrite.All' } if ($AuthenticationFlow -eq 'DeviceCode') { $MgGraphParams['UseDeviceAuthentication'] = $true } if ($Tenant) { $MgGraphParams['TenantId'] = $Tenant } Write-Host -ForegroundColor Green "When prompted please enter the appropriate credentials... Warning: Window might have pop-under in VSCode" $null = Connect-MgGraph @MgGraphParams $AdAppAccess = @{ ResourceAppId = "00000002-0000-0000-c000-000000000000"; ResourceAccess = @( @{ Id = "5778995a-e1bf-45b8-affa-663a9f3f4d04"; Type = "Role" }, @{ Id = "a42657d6-7f20-40e3-b6f0-cee03008a62a"; Type = "Scope" }, @{ Id = "311a71cc-e848-46a1-bdf8-97ff7156d8e6"; Type = "Scope" } ) } $GraphAppAccess = @{ ResourceAppId = "00000003-0000-0000-c000-000000000000"; ResourceAccess = @( @{ Id = "bf394140-e372-4bf9-a898-299cfc7564e5"; Type = "Role" }, @{ Id = "7ab1d382-f21e-4acd-a863-ba3e13f7da61"; Type = "Role" } ) } $PartnerCenterAppAccess = @{ ResourceAppId = "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd"; ResourceAccess = @( @{ Id = "1cebfa2a-fb4d-419e-b5f9-839b4383e05a"; Type = "Scope" } ) } Write-Host -ForegroundColor Green "Creating the Azure AD application and related resources..." $Application = New-MgApplication -DisplayName $DisplayName -RequiredResourceAccess ($AdAppAccess, $GraphAppAccess, $PartnerCenterAppAccess) -IsFallbackPublicClient:$IsFallbackPublicClient -SignInAudience 'AzureADMultipleOrgs' -Web @{ RedirectUris = @("urn:ietf:wg:oauth:2.0:oob", "https://login.microsoftonline.com/organizations/oauth2/nativeclient", "https://localhost", "http://localhost", "http://localhost:8400") } $ApplicationPassword = Add-MgApplicationPassword -ApplicationId $Application.Id $ServicePrincipal = New-MgServicePrincipal -AppId $Application.AppId -DisplayName $DisplayName $AdminAgentsGroup = Get-MgGroup -Filter "DisplayName eq 'AdminAgents'" $null = $ServicePrincipal | New-MgGroupMember -GroupId $AdminAgentsGroup.Id if (!$StayConnected) { Write-Host "Disconnecting from Microsoft Graph" $null = Disconnect-MgGraph } if ($OutputFormat -eq 'Raw') { [PSCustomObject][ordered]@{ ApplicationPassword = $ApplicationPassword Application = $Application } } elseif ($OutputFormat -eq 'Minimal') { $SecretSecureString = $ApplicationPassword.SecretText | ConvertTo-SecureString -AsPlainText -Force $Output = [ordered]@{ Credential = [System.Management.Automation.PSCredential]::new($Application.AppId, $SecretSecureString) SecretExpiration = $ApplicationPassword.EndDateTime } if ($Tenant) { $Output['Tenant'] = $Tenant } [PSCustomObject]$Output } } function Connect-PartnerCenter { <# .OUTPUTS PartnerOperations object that can be used to perform operations on the Partner Center API. .NOTES https://docs.microsoft.com/en-us/partner-center/develop/partner-center-authentication#app--user-authentication #> [OutputType('Microsoft.Store.PartnerCenter.AggregatePartnerOperations')] param ( # Application ID and secret. [Parameter(Mandatory)] [PSCredential]$Credential, # User token MFA authenticated. [Parameter(Mandatory)] [String]$RefreshToken, # Limit to specific tenant. [string]$Tenant = 'common' ) if (!$Tenant) { $Tenant = 'common' } $AccessToken = New-PartnerAccessToken -Credential $Credential -RefreshToken $RefreshToken -Tenant $Tenant $ApplicationId = $Credential.UserName class ScriptBlockDelegate { [ScriptBlock]$Code ScriptBlockDelegate([ScriptBlock]$Code) { $this.Code = $Code } [System.Threading.Tasks.Task[Microsoft.Store.PartnerCenter.AuthenticationToken]]Delegate([Microsoft.Store.PartnerCenter.AuthenticationToken]$ExpiredAuthenticationToken) { $Func = New-RunspacedDelegate ([Func[object, Microsoft.Store.PartnerCenter.AuthenticationToken]] $this.Code) Return [System.Threading.Tasks.TaskFactory[Microsoft.Store.PartnerCenter.AuthenticationToken]]::new().StartNew($Func, $ExpiredAuthenticationToken) } } $Callback = { param ( [Microsoft.Store.PartnerCenter.AuthenticationToken]$ExpiredAuthenticationToken ) $AccessToken = New-PartnerAccessToken -Credential $Credential -RefreshToken $RefreshToken -Tenant $Tenant [Microsoft.Store.PartnerCenter.AuthenticationToken]::new($AccessToken.AccessToken, $AccessToken.AccessTokenExpiration) }.GetNewClosure() $Delegate = [ScriptBlockDelegate]::new($Callback) $TokenRefresher = $Delegate.Delegate $PartnerCredentials = [Microsoft.Store.PartnerCenter.Extensions.PartnerCredentials]::Instance.GenerateByUserCredentials( $ApplicationId, [Microsoft.Store.PartnerCenter.AuthenticationToken]::new($AccessToken.AccessToken, $AccessToken.AccessTokenExpiration), $TokenRefresher, $null ) $Script:PartnerOperations = [Microsoft.Store.PartnerCenter.PartnerService]::Instance.CreatePartnerOperations($PartnerCredentials) Return $Script:PartnerOperations } function Get-PartnerOrganizationProfile { <# .NOTES https://docs.microsoft.com/en-us/partner-center/develop/get-an-organization-profile#c #> [OutputType('Microsoft.Store.PartnerCenter.Models.Partners.OrganizationProfile')] param ( # Return Task instead of result to support fast parallel execution. [switch]$Async, # PartnerOperations session, if not provided last generated one will be automatically used. $PartnerOperations = $Script:PartnerOperations ) $Get = $Async ? 'GetAsync' : 'Get' Return $PartnerOperations.Profiles.OrganizationProfile.$Get() } function Get-PartnerOrganizationProfileRestExample { <# .NOTES https://docs.microsoft.com/en-us/partner-center/develop/get-an-organization-profile#rest-request #> param ( [Parameter(Mandatory)] $AccessToken ) $ContentType = [System.Net.Mime.MediaTypeNames+Application]::Json, ('charset={0}' -f [System.Text.Encoding]::UTF8.WebName) -join ';' $Response = Invoke-RestMethod -ContentType $ContentType -Uri 'https://api.partnercenter.microsoft.com/v1/profiles/organization' -Headers @{ "Authorization" = "Bearer " + $AccessToken 'Accept' = [System.Net.Mime.MediaTypeNames+Application]::Json, ('charset={0}' -f ([System.Text.Encoding]::UTF8).WebName) -join ';' 'MS-RequestId' = 'b85cb7ab-cc2e-4966-93f0-cf0d8377a93f' 'MS-CorrelationId' = '1bb03149-88d2-4bc2-9cc1-d6e83890fa9e' } $Response.Substring(1) | ConvertFrom-Json } function Get-PartnerCustomer { <# .NOTES https://docs.microsoft.com/en-us/partner-center/develop/get-customers-of-an-indirect-reseller#c #> [CmdletBinding(DefaultParameterSetName = 'Default')] [OutputType('Microsoft.Store.PartnerCenter.Models.Customers.Customer')] param ( # Customer object. [Parameter(ParameterSetName = 'PipeLine', Mandatory, ValueFromPipeLine)] $InputObject, # Customer tenant ID, if not provided will get all customers. [Parameter(ParameterSetName = 'Customer', Mandatory)] [String]$CustomerId, # Reseller tenant ID, if provided filter customers from specific reseller. [Parameter(ParameterSetName = 'IndirectReseller', Mandatory)] [String]$IndirectResellerId, # Return Task instead of result to support fast parallel execution. [switch]$Async, # PartnerOperations session, if not provided last generated one will be automatically used. $PartnerOperations = $Script:PartnerOperations ) $Get = $Async ? 'GetAsync' : 'Get' if ($InputObject) { $CustomerId = $InputObject.Id } if ($IndirectResellerId) { # Create a filter. $Filter = [Microsoft.Store.PartnerCenter.Models.Query.SimpleFieldFilter]::new( [Microsoft.Store.PartnerCenter.Models.Customers.CustomerSearchField]::IndirectReseller.ToString(), [Microsoft.Store.PartnerCenter.Models.Query.FieldFilterOperation]::StartsWith, $IndirectResellerId ) # Create an iQuery object to pass to the Query method. $MyQuery = [Microsoft.Store.PartnerCenter.Models.Query.QueryFactory]::Instance.BuildSimpleQuery($Filter) # Get the collection of matching customers. $CustomersPage = $PartnerOperations.Customers.Query($MyQuery); # Create a customer enumerator for traversing the customer pages. $CustomersEnumerator = $PartnerOperations.Enumerators.Customers.Create($CustomersPage); while ($CustomersEnumerator.HasValue) { # Work with the current page. foreach ($Customer in $CustomersEnumerator.Current.Items) { $Customer } # Get the next page of customers. $CustomersEnumerator.Next() } } elseif ($CustomerId) { $PartnerOperations.Customers.ById($CustomerId).$Get() } else { $CustomersPage = $PartnerOperations.Customers.Get() # Create a customer enumerator for traversing the customer pages. $CustomersEnumerator = $PartnerOperations.Enumerators.Customers.Create($CustomersPage); while ($CustomersEnumerator.HasValue) { # Work with the current page. foreach ($Customer in $CustomersEnumerator.Current.Items) { $Customer } # Get the next page of customers. $CustomersEnumerator.Next() } } } function Get-PartnerCustomerRestExample { <# .NOTES https://docs.microsoft.com/en-us/partner-center/develop/get-customers-of-an-indirect-reseller#rest-request WARNING: This example missing a "paging" retrieval code so it will only return the first page of customers. #> [CmdletBinding(DefaultParameterSetName = 'Default')] [OutputType('Microsoft.Store.PartnerCenter.Models.Customers.Customer')] param ( [Parameter(Mandatory)] $AccessToken, # Customer tenant ID, if not provided will get all customers. [Parameter(ParameterSetName = 'Customer', Mandatory)] [String]$CustomerId, # Reseller tenant ID, if not provided filter customers from specific reseller. [Parameter(ParameterSetName = 'IndirectReseller', Mandatory)] [String]$IndirectResellerId ) if ($IndirectResellerId) { $ContentType = [System.Net.Mime.MediaTypeNames+Application]::Json, ('charset={0}' -f [System.Text.Encoding]::UTF8.WebName) -join ';' $Response = Invoke-RestMethod -ContentType $ContentType -Uri ('https://api.partnercenter.microsoft.com/v1/customers?filter={{"field":"IndirectReseller","value":"{0}","operator":"starts_with"}}' -f $IndirectResellerId) -Headers @{ "Authorization" = "Bearer " + $AccessToken 'Accept' = [System.Net.Mime.MediaTypeNames+Application]::Json, ('charset={0}' -f ([System.Text.Encoding]::UTF8).WebName) -join ';' } $Response } elseif ($CustomerId) { $ContentType = [System.Net.Mime.MediaTypeNames+Application]::Json, ('charset={0}' -f [System.Text.Encoding]::UTF8.WebName) -join ';' $Response = Invoke-RestMethod -ContentType $ContentType -Uri "https://api.partnercenter.microsoft.com/v1/customers/$CustomerId" -Headers @{ "Authorization" = "Bearer " + $AccessToken 'Accept' = [System.Net.Mime.MediaTypeNames+Application]::Json, ('charset={0}' -f ([System.Text.Encoding]::UTF8).WebName) -join ';' } $Response } else { $ContentType = [System.Net.Mime.MediaTypeNames+Application]::Json, ('charset={0}' -f [System.Text.Encoding]::UTF8.WebName) -join ';' $Response = Invoke-RestMethod -ContentType $ContentType -Uri "https://api.partnercenter.microsoft.com/v1/customers" -Headers @{ "Authorization" = "Bearer " + $AccessToken 'Accept' = [System.Net.Mime.MediaTypeNames+Application]::Json, ('charset={0}' -f ([System.Text.Encoding]::UTF8).WebName) -join ';' } $Response } } function Get-PartnerCustomerSubscription { <# .NOTES https://docs.microsoft.com/en-us/partner-center/develop/get-all-subscriptions-by-partner#c #> [OutputType('Microsoft.Store.PartnerCenter.Models.Subscriptions.Subscription')] param ( # Customer object. [Parameter(ParameterSetName = 'PipeLine', Mandatory, ValueFromPipeLine)] $InputObject, # Customer tenant ID. [Parameter(ParameterSetName = 'CustomerId', Mandatory)] [string]$CustomerId, # $MpnId # $SubscriptionId # Return Task instead of result to support fast parallel execution. [switch]$Async, # PartnerOperations session, if not provided last generated one will be automatically used. $PartnerOperations = $Script:PartnerOperations ) $Get = $Async ? 'GetAsync' : 'Get' if ($InputObject) { $CustomerId = $InputObject.Id } $PartnerOperations.Customers.ById($CustomerId).Subscriptions.$Get().Items } function Get-PartnerIndirectReseller { <# .NOTES https://docs.microsoft.com/en-us/partner-center/develop/get-indirect-resellers-of-a-customer#c #> [CmdletBinding(DefaultParameterSetName = 'Default')] [OutputType('Microsoft.Store.PartnerCenter.Models.Relationships.PartnerRelationship')] param ( # Customer object. [Parameter(ParameterSetName = 'PipeLine', Mandatory, ValueFromPipeLine)] $InputObject, # Customer tenant ID. [Parameter(ParameterSetName = 'CustomerId', Mandatory)] [string]$CustomerId, # Return Task instead of result to support fast parallel execution. [switch]$Async, # PartnerOperations session, if not provided last generated one will be automatically used. $PartnerOperations = $Script:PartnerOperations ) $Get = $Async ? 'GetAsync' : 'Get' if ($InputObject) { $CustomerId = $InputObject.Id } if ($CustomerId) { $PartnerOperations.Customers.ById($CustomerId).Relationships.$Get().Items } else { $PartnerOperations.Relationships.$Get([Relationships.PartnerRelationshipType]::IsIndirectCloudSolutionProviderOf).Items } } |