3

I am trying to run PowerShell script from ASP.NET Core Web API. Please see the environment details below:

.NET SDKs installed:
  3.1.403 [C:\Program Files\dotnet\sdk]
  7.0.200 [C:\Program Files\dotnet\sdk]
  7.0.201 [C:\Program Files\dotnet\sdk]
  7.0.306 [C:\Program Files\dotnet\sdk]
  7.0.400 [C:\Program Files\dotnet\sdk]
  8.0.100-preview.3.23178.7 [C:\Program Files\dotnet\sdk]
  8.0.101 [C:\Program Files\dotnet\sdk]

The API runs on .NET 7; here is the .csproj file:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.PowerShell.SDK" Version="7.3.9" />
  </ItemGroup>

The API code:

using System.Diagnostics;
using System.Management.Automation;
using System.Management.Automation.Runspaces;

public static ScriptResult Run2(string filePath, Dictionary<string,string> parameters)
{
    string myScript = File.ReadAllText(filePath);
    
    ScriptResult scriptResult = new ScriptResult();

    using (System.Management.Automation.PowerShell ps = System.Management.Automation.PowerShell.Create())
    {
        ps.AddScript(myScript);

        foreach (var kvp in parameters)
        {
            ps.AddParameter(kvp.Key, kvp.Value);
        }

        var result = ps.Invoke();

        if (ps.HadErrors)
        {
            string errorMessage = "";

            foreach (var error in ps.Streams.Error)
            {
                errorMessage += error.ToString() + "\n"; <-- This is where I am getting the error message
            }

            throw new Exception(errorMessage);
        }

        foreach (var PSObject in result)
        {
            var x = PSObject.ToString();
        }
    }
    
    return scriptResult;
}

Output of Get-ExecutionPolicy -List:

        Scope ExecutionPolicy
        ----- ---------------
MachinePolicy       Undefined
   UserPolicy       Undefined
      Process       Undefined
  CurrentUser    Unrestricted
 LocalMachine          Bypass

Note: The script contains following line:

Import-Module MicrosoftTeams

##script to get token##

Connect-MicrosoftTeams -AccessTokens @("$graphToken", "$teamsToken") | Out-Null

$getUserSettings = Get-CSOnlineUser -Identity $UserEmail -ErrorAction 'stop'

$output = @{
  UserConfig = $getUserSettings
}

$output | ConvertTo-JSON

Write-Output $output

Output of PSVersionTable

Name                           Value
----                           -----
PSVersion                      5.1.19041.4046
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.19041.4046
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1

Getting error:

File C:\Program Files\WindowsPowerShell\Modules\MicrosoftTeams\5.5.0\MicrosoftTeams.psm1 cannot be loaded because running scripts is disabled on this system. For more information, see about_Execution_Policies at https://go.microsoft.com/fwlink/?LinkID=135170.

0

2 Answers 2

3

An ASP.Net web app is almost certainly running as a different user , such that it's CurrentUser scope is probably still the default Undefined, which is the same as Restricted on Windows. This would exactly explain your observed result.

However, we're not done yet. The documentation indicates the LocalMachine policy of ByPass should override this:

The valid values for Scope are MachinePolicy, UserPolicy, Process, CurrentUser, and LocalMachine. LocalMachine is the default when setting an execution policy.

The Scope values are listed in precedence order. The policy that takes precedence is effective in the current session, even if a more restrictive policy was set at a lower level of precedence.

I actually think this documentation is ambiguous, because it leaves room for interpretation about which direction is increasing vs decreasing precedence as you read the list. But my own use makes me confident the LocalMachine setting should be what is used here.

So there's still the question of why the script won't run. I'm not 100% sure on this, but I have a suspicion.

One thing to understand is there are effectively two PowerShells: one that comes with Windows (Desktop), and one that is installed separately (Core). Powershells version 6.0 and later are installed separately (though it's likely a future version of Windows will start including the new Powershell instead).

When we see the package reference for the 7.3.9 version of the Powershell SDK, that's an installed version of Powershell "Core", and that's what ASP.Net should be using. But it's likely the test showing the Execution Policy list is instead using the built-in Powershell (likely version 5.0 or 5.1), which, critically, can have it's own set of execution policies. And so now we're back to a place where the ASP.Net app might have a Restricted ExecutionPolicy, even though the results of Get-ExecutionPolicy show otherwise, which again exactly explains the symptoms.


While I'm here, it's worth a few moments to comment on the script you want to run, keeping in mind what I said about the ASP.Net web app running as a different user. The execution environment for the ASP.Net is not likely to see the MS Teams session in the way you expect. The portion to get the tokens is not shown, but if they depend on your current desktop session you may need to go back to the drawing board on how you want to accomplish this in the first place.

Sign up to request clarification or add additional context in comments.

3 Comments

I updated my details with output of $PSVersionTable and it seems I have version 5 of PowerShell. Are you suggesting to downgrade my PowerShell SDK in the API project (which is 7) to match the installed version?
@TechTurtle Not exactly. I'm suggesting what you see in the console when checking the version table and execution policy is not the same thing the App sees. Your session making the checks seems to see the included Desktop version of Powershell, but the ASP.Net web app is likely using the installed Core version of Powershell. You want to open a console session with the installed Core version of Powershell and set the execution policy there.
Yes, that may be the case, I tried Unrestricted for both CurrentUser and LocalMachine but no luck. I also tried setting Group Policy via the Local Group Policy Editor to Enable all and still no luck. Also, their GitHub page doesn't mention any specific pointers of using the SDK itself.
2

Joel Coehoorn's helpful answer provides a key part of the puzzle:

  • The implication is that the ASP.NET application runs with a different user identity than the one that shows Unrestricted as its current-user policy, and that that different user's current-user policy is either explicitly set to Restricted or - more likely - not defined (which Get-ExecutionPolicy -List reports as Undefined).

  • In the latter case, the effective execution policy is defined by the LocalMachine scope; if the latter is Undefined too, the default is Restricted, preventing script-file execution - this is what you saw.

    • Note: If the execution policy is defined by GPOs (MachinePolicy or UserPolicy), it can not be overridden by other means.
    • For a comprehensive overview of PowerShell's execution policies, see this answer.
  • In a PowerShell (Core) 7+ SDK project - unlike one for Windows PowerShell - a stand-alone PowerShell 7+ installation's LocalMachine policy is not visible to the SDK project, so the value of the LocalMachine policy when run from a stand-alone PowerShell 7+ session is irrelevant (as are Windows PowerShell's execution policies, which are entirely separate). However, the CurrentUser policy from a standalone PowerShell 7+ installation is seen and honored.[1]

  • The best solution is not to rely on a preconfigured execution policy to begin with, and instead use an SDK method to set the execution policy dynamically, for the current process only, as follows (this is the equivalent of using Set-ExecutionPolicy -Scope Process Bypass -Force):

    // Create an initial default session state.
    var iss = InitialSessionState.CreateDefault2();
    
    // Set its script-file execution policy (for the current session (process) only).
    // 'Bypass' uncondtionally allows script execution.
    iss.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Bypass;
    
    // Create a PowerShell instance with a runspace based on the 
    // initial session state and run a sample script to show
    // that the process-level Bypass policy took effect.
    using (PowerShell ps = PowerShell.Create(iss))
    {
      Console.WriteLine(
        ps.AddScript(@"Get-ExecutionPolicy -List | Out-String").Invoke()[0]
      );
    }
    

[1] Windows PowerShell stores its (non-GPO) execution policies in a fixed location in the registry, whereas PowerShell 7+ uses powershell.config.json files. For the LocalMachine scope, this file is stored alongside the PowerShell executable, which for SDK projects is not the home directory of any stand-alone PowerShell 7+ installation (which may or may not be present, and can hypothetically be installed anywhere), but the directory of the SDK project's own executable, as reflected in the PSHOME environment variable. By contrast, the CurrentUser-scope powershell.config.json is seen and honored by SDK projects, because it is stored in an installation-independent, fixed directory relative to the user's home directory. Specifically, the CurrentUser policy is stored in "$([Environment]::GetFolderPath('MyDocuments'))/powershell/powershell.config.json", and the LocalMachine one in "$PSHOME\powershell.config.json" (with $PSHOME having different meanings in stand-alone PowerShell vs. SDK projects).
Note that this means that while you could hypothetically bundle a powershell.config.json file with your project with a preconfigured policy, it could still be preempted by a CurrentUser policy from a stand-alone installation. Only the dynamic, per-process policy override shown in the next bullet point can prevent that (except, as noted, if GPOs control the execution policy).

4 Comments

Thank you @mklement0, question: any reason why we are repeating "iss.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Bypass;"?
This is working and thank you for sharing the in-depth knowledge. May I please know if you have any source/documentation around using the PowerShell SDK in C#? it's very confusing/frustrating to hunt online.
@TechTurtle, glad to hear it. The duplication of the iss.ExecutionPolicy = ... statement was accidental, it is now fixed. As for documentation: please see my next comment, but note that there appears to be none about execution policies; what is in this answer came from my own research.
An introduction to the PowerShell SDK is Windows PowerShell Host Quickstart. Use the tree view on the left to find additional topics, such as Adding and invoking commands and code samples. Caveat: The docs haven't been updated for PS 7+, though most still applies.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.