microsoft / Partner-Center-PowerShell

PowerShell module for managing Partner Center resources.
https://docs.microsoft.com/powershell/partnercenter/
MIT License
130 stars 59 forks source link

New-PartnerAccessToken command executed in parallel for multiple tenants from C# code returns multiple tokens for one tenant and empty PSObjects for others #408

Open fant0zzi opened 1 year ago

fant0zzi commented 1 year ago

When the New-PartnerAccessToken command is executed in parallel for multiple tenants from C# code, one of the responses contains multiple tokens, while empty PSObjects are returned for the other tenants. There are no execution errors.

To demonstrate this behaviour, I have written a simple script (using .NET7) that runs multiple New-PartnerAccessToken commands simultaneously in separate runspaces. It's important to note that the results of the execution can vary - sometimes a single token is returned in the response for a tenant, sometimes multiple tokens are returned, one of which is issued for another tenant. However, the results for the other tenants are always empty. I have attached a screenshot where the results dict contains 2 tokens resolved for one tenant, while for other tenants empty results have been received.

using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Security;
using Microsoft.PowerShell;

var tenants = new[]
{
    "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX1",
    "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX2",
    "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX3"
};

var task1 = new ExoAccessTokenGenerator().GetExoAccessTokensAsync(tenants);
Task.WaitAll(task1);

var results = task1.Result;
foreach (var res in results)
{
    Console.WriteLine($"Number of access tokens for {res.Key}: {res.Value.ToArray().Length}");
}
Console.ReadKey();

public class ExoAccessTokenGenerator
{
    public async Task<IDictionary<string, IEnumerable<PSObject>>> GetExoAccessTokensAsync(IEnumerable<string> tenantIds)
    {
        var accessTokens = new Dictionary<string, IEnumerable<PSObject>>();
        var tasks = new List<Task>();

        foreach (var tenantId in tenantIds)
        {
            var task = Task.Run(() =>

            {
                var accessToken = GetExoAccessToken(tenantId);
                accessTokens.Add(tenantId, accessToken);
            });

            tasks.Add(task);
        }

        await Task.WhenAll(tasks);

        return accessTokens;
    }

    private IEnumerable<PSObject> GetExoAccessToken(string tenantId)
    {
        var appId = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
        var appSecret = "";
        var refreshToken = "";
        var scopes = "https://outlook.office365.com/.default";
        var credential = new PSCredential(appId, ToSecureString(appSecret));

        var exoTokenCommand = new Command("New-PartnerAccessToken");
        exoTokenCommand.Parameters.Add("RefreshToken", refreshToken);
        exoTokenCommand.Parameters.Add("Credential", credential);
        exoTokenCommand.Parameters.Add("Scopes", scopes);
        exoTokenCommand.Parameters.Add("ApplicationId", appId);
        exoTokenCommand.Parameters.Add("ServicePrincipal", true);
        exoTokenCommand.Parameters.Add("Tenant", tenantId);

        using (var powerShell = PowerShell.Create())
        {
            InitialSessionState sessionState = InitialSessionState.CreateDefault();
            sessionState.ExecutionPolicy = ExecutionPolicy.Unrestricted;
            sessionState.ThrowOnRunspaceOpenError = true;
            var runspace = RunspaceFactory.CreateRunspace(sessionState);
            runspace.Open();

            powerShell.Runspace = runspace;

            powerShell.Commands.AddCommand(exoTokenCommand);

            var result = powerShell.Invoke();

            if (powerShell.HadErrors)
            {
                var errors = string.Join(Environment.NewLine, powerShell.Streams.Error);
                throw new Exception($"Failed to execute PowerShell command. Errors: {errors}");
            }

            if (result.Count == 0)
            {
                Console.WriteLine($"Token has not been received for tenant {tenantId}");
            }

            return result;
        }
    }

    private static SecureString ToSecureString(string str)
    {
        var secureString = new SecureString();

        foreach (var c in str)
        {
            secureString.AppendChar(c);
        }

        return secureString;
    }
}

Screenshot_8

Environment

PSVersion 5.1 PartnerCenter 3.0.10 Microsoft.PowerShell.SDK 7.3.3 Microsoft.IdentityModel.Token 6.27.0 The issue is reproducable on both .NET 4.8 and .NET 7

meghaj473 commented 1 year ago

Thank you for the explanation! Can we use the same script for .Net Core 6.0? Also can you please help us with the nudget packages that needs to be installed. I am getting the error "The getter method should be public, not void, static, and have one parameter of the type PSObject." when executing the script using .Net core 6. Can you please help

meghaj473 commented 1 year ago

Hello again, I have somehow managed to resolve the error "The getter method should be public, not void, static, and have one parameter of the type PSObject.". But now I get a new error upon executing the powerhell.invoke() command -: "The term 'New-PartnerAccessToken' is not recognized as a name of a cmdlet, function, script file, or executable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again" Can you please help.

Neuromancien62 commented 1 year ago

Hello, I have the same problem using this code with The term 'New-PartnerAccessToken' is not recognized as a name of a cmdlet, function, script file, or executable program. Check the spelling of the name, or if a path was included, check that the path is correct and try again' Did you have a solution? Thank you in advance

Neuromancien62 commented 1 year ago

I am looking to reproduce in c# this process in powershell

TenantID = "xxxxxxxxxxxxxxxxxxx" # Tenant $ApplicationID = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # Application in Tenant "Partner Center" $ApplictionSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

$credential = New-Object PSCredential ($ApplicationID, (ConvertTo-SecureString $ApplictionSecret -AsPlainText -Force)) $token = New-PartnerAccessToken -ApplicationId $ApplicationID -Scopes 'https://api.partnercenter.microsoft.com/user_impersonation' -ServicePrincipal -Credential $credential -Tenant $TenantID -UseAuthorizationCode $token | clip

. Someone help me a little?