PartnerCenterCommunity.psm1
using namespace Microsoft.Store.PartnerCenter using namespace Microsoft.Store.PartnerCenter.Models using namespace Microsoft.Store.PartnerCenter.Extensions if (!(Get-Module -Name 'PSRunspacedDelegate')) { Import-Module -Name "$PSScriptRoot\PSRunspacedDelegate" } $ErrorActionPreference = 'Stop' function Write-DebugObject { [CmdletBinding()] param ( $Messages, $Subject ) if ($DebugPreference -in 'Continue', 'Inquire', 'Stop') { $NewMessages = foreach ($Message in $Messages) { if ($Message -is [string]) { if ($Message.Length -gt 8) { $Message.Substring(0, 4) + '***' + $Message.Substring($Message.Length - 4, 4) } else { $Message } } elseif ($Message -is [PSCustomObject]) { $NewMessage = $Message.PsObject.Copy() foreach ($Property in $NewMessage.PsObject.Properties) { if ($Property.Value -is [string] -and $Property.Value.Length -gt 8) { $Property.Value = $Property.Value.Substring(0, 4) + '***' + $Property.Value.Substring($Property.Value.Length - 4, 4) } } ($NewMessage | Format-List | Out-String).Trim() } else { throw "Write-DebugObject: $($Message.GetType()) is not a string or a PSCustomObject" } } Write-Debug ($Subject, $NewMessages | Join-String -Separator ([Environment]::NewLine)) } } 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 #> [CmdletBinding()] 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', 'FlatAutoFull')] [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 'Minimal') { [PSCustomObject][ordered]@{ AccessTokenExpiration = [DateTimeOffset]::FromUnixTimeSeconds($ReturnCred.expires_on).LocalDateTime AccessToken = $ReturnCred.access_token RefreshToken = $ReturnCred.refresh_token } } else { $ReturnCred | Format-Output -OutputFormat $OutputFormat } } 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 #> [CmdletBinding(DefaultParameterSetName = 'OIDC')] [CmdletBinding()] param( # Limit to specific tenant. [string]$Tenant = 'common', # Your CSP/Partner Center application/Client ID. [Parameter(ParameterSetName = 'DeviceCode', Mandatory)] [string]$ApplicationId, # Application ID and secret. [Parameter(ParameterSetName = 'OIDC', Mandatory)] [PSCredential]$Credential, # Scope of the RefreshToken. Each endpoint needs its own consented RefreshToken. [ArgumentCompleter({ 'user.read', 'openid', 'profile', 'offline_access', 'https://api.partnercenter.microsoft.com/user_impersonation', 'https://outlook.office365.com/.default' })] [string[]]$Scopes = @('https://api.partnercenter.microsoft.com/user_impersonation', 'offline_access', 'openid', 'profile'), # Authorization grant flow. # OIDC - https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc # DeviceCode - https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code [ValidateSet('OIDC', 'DeviceCode')] [string]$AuthenticationFlow = 'OIDC', [ValidateSet('Raw', 'OnlyRefreshToken', 'FlatAutoFull')] [string]$OutputFormat = 'OnlyRefreshToken' ) if (!$Tenant) { $Tenant = 'common' } if ($AuthenticationFlow -eq 'DeviceCode' -and !($ApplicationId)) { throw "`New-PartnerRefreshToken -ApplicationId` is required when using `-AuthenticationFlow DeviceCode`." } elseif ($AuthenticationFlow -eq 'OIDC' -and !($Credential)) { throw "`New-PartnerRefreshToken -Credential` is required when using `-AuthenticationFlow OIDC`." } if ($AuthenticationFlow -eq 'DeviceCode') { # Get the authorization code. $CodeBody = @{ client_id = $ApplicationId scope = $Scopes -join ' ' } $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' device_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/v2.0/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 ('"-AuthenticationFlow DeviceCode" requires "Allow public client flows" in Azure Portal -> "App registrations" -> "Authentication". Original Error:' + [Environment]::NewLine + $_.ErrorDetails.Message) } throw $_ } } } } elseif ($AuthenticationFlow -eq 'OIDC') { $Module = 'Pode' if (!((Get-Module -Name $Module -ListAvailable) -or (Get-Module -Name $Module))) { Install-Module $Module } $PodeModule = (Get-Module -Name $Module).Path $PodeJob = Start-Job { Import-Module $using:PodeModule $UsedPorts = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue $Port = [Linq.Enumerable]::First([Linq.Enumerable]::Except([Linq.Enumerable]::Range(8400, 600), [int[]]$UsedPorts.LocalPort)) @{ Port = $Port } Start-PodeServer -Quiet { Add-PodeEndpoint -Address localhost -Port $Port -Protocol Http Add-PodeRoute -Method POST -Path '/' -ScriptBlock { Write-PodeJsonResponse -Value $WebEvent.Data Out-PodeVariable -Name Code -Value $WebEvent.Data.code Close-PodeServer } } @{ Code = $Code } } while ($PodeJob.State -eq 'Running') { $PodeJob | Receive-Job | ForEach-Object { if ($_.Port) { $Port = $_.Port break } else { Write-Warning ($_ | Format-List | Out-String) } } Start-Sleep -Seconds 0.2 } $Uri = "https://login.microsoftonline.com/$Tenant/oauth2/v2.0/authorize" $ParametersCode = @{ client_id = $Credential.UserName response_mode = 'form_post' response_type = 'code id_token' scope = $Scopes -join ' ' nonce = 1 redirect_uri = "http://localhost:$port/" prompt = 'select_account' } # Build the URI. $ParametersCodeCollection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty) foreach ($key in $ParametersCode.Keys) { $ParametersCodeCollection.Add($key, $ParametersCode.$key) } $UriRequest = [System.UriBuilder]$Uri $UriRequest.Query = $ParametersCodeCollection.ToString() Start-Process $UriRequest $Code = $null $PodeJob | Receive-Job -Wait | ForEach-Object { if ($_.Code) { $Code = $_.Code } else { Write-Warning ($_ | Format-List | Out-String) } } if (!$Code) { throw 'Failed to get the authorization code.' } $Uri = "https://login.microsoftonline.com/$Tenant/oauth2/v2.0/token" $RefreshTokenParameters = @{ client_id = $Credential.UserName client_secret = $Credential.GetNetworkCredential().Password grant_type = "authorization_code" code = $Code redirect_uri = "http://localhost:$port/" } $RefreshTokenResponse = Invoke-RestMethod -Uri $Uri -ContentType "application/x-www-form-urlencoded" -Method POST -Body $RefreshTokenParameters } if ($OutputFormat -eq 'OnlyRefreshToken') { $RefreshTokenResponse.refresh_token } else { $RefreshTokenResponse | Format-Output -OutputFormat $OutputFormat } } 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/ #> [CmdletBinding()] param( # Limit to specific tenant. [string]$Tenant, [Parameter(Mandatory)] [string]$DisplayName, # Authorization grant flow. # OIDC - https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc # DeviceCode - https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code [ValidateSet('OIDC', 'DeviceCode')] [string]$AuthenticationFlow = 'OIDC', # OIDC - If you need the WebApp to support `New-PartnerRefreshToken -AuthenticationFlow OIDC` (EnableIdTokenIssuance). # DeviceCode - If you need the WebApp to support `New-PartnerRefreshToken -AuthenticationFlow DeviceCode` (IsFallbackPublicClient). [ValidateSet('OIDC', 'DeviceCode')] [string[]]$AuthenticationFlowAllowed = 'OIDC', # Stay connected to MgGraph. [switch]$StayConnected, [ValidateSet('Raw', 'Minimal', 'FlatAutoFull')] [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" Connect-MgGraph @MgGraphParams | ForEach-Object { if ($_ -eq 'Welcome To Microsoft Graph!') { Write-Host -ForegroundColor Green $_ } else { Write-Warning $_ } } $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..." $ApplicationParams = @{ DisplayName = $DisplayName RequiredResourceAccess = ($AdAppAccess, $GraphAppAccess, $PartnerCenterAppAccess) 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") ImplicitGrantSettings = @{ EnableIdTokenIssuance = $true } } } if ($AuthenticationFlowAllowed -contains 'DeviceCode') { $ApplicationParams['IsFallbackPublicClient'] = $true } if ($AuthenticationFlowAllowed -contains 'OIDC') { $ApplicationParams['Web']['ImplicitGrantSettings'] = @{ EnableIdTokenIssuance = $true } } $Application = New-MgApplication @ApplicationParams $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 '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 } else { [PSCustomObject][ordered]@{ ApplicationPassword = $ApplicationPassword Application = $Application } | Format-Output -OutputFormat $OutputFormat } } 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 #> [CmdletBinding()] [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', # # This script will run on "access token" refresh, can be used to save the now generated extended "refresh token" for next time. [scriptblock]$RefreshTokenScript ) if (!$Tenant) { $Tenant = 'common' } $AccessToken = New-PartnerAccessToken -Credential $Credential -RefreshToken $RefreshToken -Tenant $Tenant $ApplicationId = $Credential.UserName class ScriptBlockDelegate { $DebugPreferenceParent = $DebugPreference [PSCredential]$Credential [string]$RefreshToken [string]$Tenant [ScriptBlock]$RefreshTokenScript ScriptBlockDelegate( [PSCredential]$Credential, [string]$RefreshToken, [string]$Tenant, [ScriptBlock]$RefreshTokenScript ) { $this.Credential = $Credential $this.RefreshToken = $RefreshToken $this.Tenant = $Tenant $this.RefreshTokenScript = $RefreshTokenScript } [System.Threading.Tasks.Task[Microsoft.Store.PartnerCenter.AuthenticationToken]]Delegate([Microsoft.Store.PartnerCenter.AuthenticationToken]$ExpiredAuthenticationToken) { $DebugPreference = $this.DebugPreferenceParent $AccessToken = New-PartnerAccessToken -Credential $this.Credential -RefreshToken $this.RefreshToken -Tenant $this.Tenant Write-DebugObject $AccessToken -Subject 'AccessToken refreshed:' $this.RefreshToken = $AccessToken.RefreshToken if ($this.RefreshTokenScript) { $null = & $this.RefreshTokenScript $AccessToken } $AuthenticationToken = [Microsoft.Store.PartnerCenter.AuthenticationToken]::new($AccessToken.AccessToken, $AccessToken.AccessTokenExpiration) # Debug: ([DateTimeOffset]::Now.AddSeconds(32)) $Callback = { $AuthenticationToken }.GetNewClosure() $Func = New-RunspacedDelegate ([Func[object, Microsoft.Store.PartnerCenter.AuthenticationToken]] $Callback) Return [System.Threading.Tasks.TaskFactory[Microsoft.Store.PartnerCenter.AuthenticationToken]]::new().StartNew($Func, $ExpiredAuthenticationToken) } } $Delegate = [ScriptBlockDelegate]::new($Credential, $RefreshToken, $Tenant, $RefreshTokenScript) $TokenRefresher = $Delegate.Delegate $PartnerCredentials = [Microsoft.Store.PartnerCenter.Extensions.PartnerCredentials]::Instance.GenerateByUserCredentials( $ApplicationId, [Microsoft.Store.PartnerCenter.AuthenticationToken]::new($AccessToken.AccessToken, $AccessToken.AccessTokenExpiration), # Debug: ([DateTimeOffset]::Now.AddSeconds(32)) $TokenRefresher, $null ) $Script:PartnerOperations = [Microsoft.Store.PartnerCenter.PartnerService]::Instance.CreatePartnerOperations($PartnerCredentials) $Script:PartnerOperations } # # Wait for multithreading support https://github.com/PowerShell/PowerShell/pull/18138 # function Set-PartnerCenterConnection { # <# # .OUTPUTS # Set a PartnerOperations object as the default connection. # #> # [CmdletBinding()] # [OutputType('Microsoft.Store.PartnerCenter.AggregatePartnerOperations')] # param ( # [Parameter(Mandatory)] # $PartnerOperations # ) # $Script:PartnerOperations = $PartnerOperations # } function Get-PartnerOrganizationProfile { <# .NOTES https://docs.microsoft.com/en-us/partner-center/develop/get-an-organization-profile#c #> [CmdletBinding()] [OutputType('Microsoft.Store.PartnerCenter.Models.Partners.OrganizationProfile')] param ( [ValidateSet('Raw', 'FlatAutoFull', 'FlatAutoNoLinksAttributes', 'Compatibility')] [string]$OutputFormat = 'Raw', # 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' $OutputRaw = $PartnerOperations.Profiles.OrganizationProfile.$Get() if ($OutputFormat -eq 'Compatibility') { [PSCustomObject][ordered]@{ Id = $OutputRaw.Id CompanyName = $OutputRaw.CompanyName DefaultAddress = $OutputRaw.DefaultAddress TenantId = $OutputRaw.TenantId Domain = $OutputRaw.Domain Email = $OutputRaw.Email Language = $OutputRaw.Language Culture = $OutputRaw.Culture } } else { $OutputRaw | Format-Output -OutputFormat $OutputFormat } } function Get-PartnerOrganizationProfileRestExample { <# .NOTES https://docs.microsoft.com/en-us/partner-center/develop/get-an-organization-profile#rest-request #> [CmdletBinding()] 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, [ValidateSet('Raw', 'FlatAutoFull', 'FlatAutoNoLinksAttributes')] [string]$OutputFormat = 'Raw', # 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 } $OutputRaw = 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() } } $OutputRaw | Format-Output -OutputFormat $OutputFormat } 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 #> [CmdletBinding()] [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 # Customer Subscription ID. [string]$SubscriptionId, [ValidateSet('Raw', 'FlatAutoFull', 'FlatAutoNoLinksAttributes')] [string]$OutputFormat = 'Raw', # 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 } $OutputRaw = if ($SubscriptionId) { $PartnerOperations.Customers.ById($CustomerId).Subscriptions.ById($SubscriptionId).$Get() } else { $PartnerOperations.Customers.ById($CustomerId).Subscriptions.$Get().Items } $OutputRaw | Format-Output -OutputFormat $OutputFormat } 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, [ValidateSet('Raw', 'FlatAutoFull', 'FlatAutoNoLinksAttributes')] [string]$OutputFormat = 'Raw', # 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 } $OutputRaw = if ($CustomerId) { $PartnerOperations.Customers.ById($CustomerId).Relationships.$Get().Items } else { $PartnerOperations.Relationships.$Get([Relationships.PartnerRelationshipType]::IsIndirectCloudSolutionProviderOf).Items } $OutputRaw | Format-Output -OutputFormat $OutputFormat } function Format-Output { [CmdletBinding()] param ( [Parameter(ValueFromPipeline)] $InputObject, $OutputFormat ) process { if ($OutputFormat -eq 'Raw') { $InputObject } elseif ($OutputFormat -eq 'FlatAutoFull') { $InputObject | Get-Flattened } elseif ($OutputFormat -eq 'FlatAutoNoLinksAttributes') { $InputObject | Get-Flattened -Filter 'Links', 'Attributes' } else { throw "Unhandled output format: $OutputFormat." } } } function Get-Flattened { [CmdletBinding()] param ( [Parameter(ValueFromPipeline)] $InputObject, $ObjectType, $Prefix, [string[]]$Filter = @(), $_Output ) process { $ObjectType ??= $InputObject.GetType() if ($null -eq $_Output) { $Output = [ordered]@{} $First = $true } else { $First = $false } if ($ObjectType -eq [System.Management.Automation.PSCustomObject]) { foreach ($Property in $InputObject.PSObject.Properties) { $PropertyPrefix = ($Prefix, $Property.Name | Join-String -Separator '_') $PropertyType = $InputObject.($Property.Name)?.GetType() if ($null -eq $PropertyType) { throw "PSCustomObject property ""$($Property.Name)"" is null, cannot flatten reliably." } if ($PropertyType.IsValueType -or $PropertyType -in [string], [DateTime], [System.Uri]) { $Output[$PropertyPrefix] = $InputObject.($Property.Name) } else { Get-Flattened -InputObject $InputObject.($Property.Name) -ObjectType $PropertyType -Prefix $PropertyPrefix -Filter $Filter -_Output $Output } } } else { foreach ($Property in $ObjectType.GetProperties()) { if ($Property.Name -in $Filter) { continue } $PropertyPrefix = ($Prefix, $Property.Name | Join-String -Separator '_') if ($Property.PropertyType.IsValueType -or $Property.PropertyType -in [string], [DateTime], [System.Uri]) { $Output[$PropertyPrefix] = $InputObject.($Property.Name) continue } if ($Property.PropertyType.ImplementedInterfaces -contains [System.Collections.IEnumerable]) { if (($Property.PropertyType.GenericTypeArguments | Select-Object -Unique).Count -eq 1) { $SubType = $Property.PropertyType.GenericTypeArguments[0] } elseif ($Property.PropertyType.GenericTypeArguments.Count -gt 1) { throw 'multiple sub types?' } elseif ($Property.PropertyType.FullName -like '*[[]]' -and ($SubType = $Property.PropertyType.FullName.Trim('[]') -as [type])) { } else { throw 'unexpected sub types?' } if ($SubType.IsValueType -or $SubType -in [string], [DateTime], [System.Uri]) { $Output[$PropertyPrefix] = $InputObject.($Property.Name) -join ', ' continue } else { Get-Flattened -InputObject $InputObject.($Property.Name) -ObjectType $SubType -Prefix $PropertyPrefix -Filter $Filter -_Output $Output } } else { if ($Property.PropertyType -eq $ObjectType) { if ($null -eq $InputObject.($Property.Name)) { $Output[$PropertyPrefix] = $null } else { throw 'recursive?' # $Output[$PropertyPrefix] = $InputObject.($Property.Name) } } else { Get-Flattened -InputObject $InputObject.($Property.Name) -ObjectType $Property.PropertyType -Prefix $PropertyPrefix -Filter $Filter -_Output $Output } } } } if ($First) { [PSCustomObject]$Output } } } function Get-PartnerCustomerOrder { <# .NOTES https://docs.microsoft.com/en-us/partner-center/develop/get-an-order-by-id#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, # Customer Order ID. [string]$OrderId, # Include Price. [bool]$IncludePrice, [ValidateSet('Raw', 'FlatAutoFull', 'FlatAutoNoLinksAttributes')] [string]$OutputFormat = 'Raw', # 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 } $OutputRaw = if ($OrderId) { $PartnerOperations.Customers.ById($CustomerId).Orders.ById($OrderId).$Get($IncludePrice) } else { $PartnerOperations.Customers.ById($CustomerId).Orders.$Get($IncludePrice).Items } $OutputRaw | Format-Output -OutputFormat $OutputFormat } function Get-PartnerTransitionEligibilities { <# .NOTES https://docs.microsoft.com/en-us/partner-center/develop/transition-a-new-commerce-subscription #> [OutputType('Microsoft.Store.PartnerCenter.Models.Subscriptions.Subscription')] param ( # Customer tenant ID. [Parameter(Mandatory)] [string]$CustomerId, # Customer Subscription ID. [Parameter(Mandatory)] [string]$SubscriptionId, [ValidateSet('immediate', 'scheduled')] $EligibilityType, [ValidateSet('Raw', 'FlatAutoFull', 'FlatAutoNoLinksAttributes')] [string]$OutputFormat = 'Raw', # 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' $PartnerOperations.Customers.ById($CustomerId).Subscriptions.ById($SubscriptionId).TransitionEligibilities.$Get($EligibilityType).Items | Format-Output -OutputFormat $OutputFormat } function New-PartnerTransition { <# .NOTES https://docs.microsoft.com/en-us/partner-center/develop/transition-a-new-commerce-subscription #> [OutputType('Microsoft.Store.PartnerCenter.Models.Subscriptions.Subscription')] param ( # Customer tenant ID. [Parameter(Mandatory)] [string]$CustomerId, # Customer Subscription ID. [Parameter(Mandatory)] [string]$SubscriptionId, # The catalog item you are transitioning to. [Parameter(Mandatory)] [string]$ToCatalogItemId, # The subscription ID you are transitioning to. [string]$ToSubscriptionId, # The number of licenses to transition over. [Parameter(Mandatory)] [int]$Quantity, # Specifying the term duration of the subscription. [ArgumentCompleter({ 'P1M', 'P1Y', 'P3Y' })] [string]$TermDuration, # Specifying the billing cycle of the subscription. [ArgumentCompleter({ 'Monthly', 'Annual', 'Triennial' })] [string]$BillingCycle, # The transition type. Possible values - transition_only, transition_with_license_transfer. [Parameter(Mandatory)] [ValidateSet('transition_only', 'transition_with_license_transfer')] [string]$TransitionType, [ValidateSet('Raw', 'FlatAutoFull', 'FlatAutoNoLinksAttributes')] [string]$OutputFormat = 'Raw', # 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 ) $Create = $Async ? 'CreateAsync' : 'Create' $Transition = [Microsoft.Store.PartnerCenter.Models.Subscriptions.Transition]::new() $Transition.ToCatalogItemId = $ToCatalogItemId $Transition.Quantity = $Quantity $Transition.TransitionType = $TransitionType $Transition.ToSubscriptionId = $ToSubscriptionId $Transition.TermDuration = $TermDuration $Transition.BillingCycle = $BillingCycle $PartnerOperations.Customers.ById($CustomerId).Subscriptions.ById($SubscriptionId).Transitions.$Create($Transition) | Format-Output -OutputFormat $OutputFormat } function Get-PartnerTransition { <# .NOTES https://docs.microsoft.com/en-us/partner-center/develop/transition-a-new-commerce-subscription #> param ( # Customer tenant ID. [Parameter(Mandatory)] [string]$CustomerId, # Customer Subscription ID. [Parameter(Mandatory)] [string]$SubscriptionId, [ValidateSet('Raw', 'FlatAutoFull', 'FlatAutoNoLinksAttributes')] [string]$OutputFormat = 'Raw', # 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' $PartnerOperations.Customers.ById($CustomerId).Subscriptions.ById($SubscriptionId).Transitions.$Get() | Format-Output -OutputFormat $OutputFormat } |