From a8d55851e0167bdead4e7c106f0592d5d6de7fb1 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Mon, 6 Jun 2022 12:12:53 -0700 Subject: [PATCH] Add the `-ConfigurationFile` command line parameter to `pwsh` to support local session configuration (#17447) --- .../host/msh/CommandLineParameterParser.cs | 24 +++ .../host/msh/ConsoleHost.cs | 108 +++++++++---- .../host/msh/ConsoleShell.cs | 34 +++- .../host/msh/ManagedEntrance.cs | 5 +- .../CommandLineParameterParserStrings.resx | 5 +- .../resources/ConsoleHostStrings.resx | 3 + .../resources/ManagedEntranceStrings.resx | 8 + .../engine/InitialSessionState.cs | 48 +++++- .../commands/NewPSSessionConfigurationFile.cs | 2 - .../fanin/InitialSessionStateProvider.cs | 149 +++++++++++++++++- .../fanin/OutOfProcTransportManager.cs | 10 +- .../engine/remoting/fanin/WSManPlugin.cs | 14 +- .../server/OutOfProcServerMediator.cs | 83 +++++++--- .../remoting/server/serverremotesession.cs | 65 ++++---- .../resources/ConsoleInfoErrorStrings.resx | 6 + .../resources/RemotingErrorIdStrings.resx | 9 ++ test/SSHRemoting/SSHRemoting.Basic.Tests.ps1 | 23 ++- test/hosting/test_HostingBasic.cs | 13 ++ .../engine/Basic/DefaultCommands.Tests.ps1 | 2 +- .../HelpersRemoting/HelpersRemoting.psm1 | 23 ++- .../Microsoft.PowerShell.RemotingTools.psm1 | 31 ++-- test/xUnit/csharp/test_CommandLineParser.cs | 33 ++++ 22 files changed, 571 insertions(+), 127 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index aefcb58cfd..8d31f35774 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -227,6 +227,7 @@ namespace Microsoft.PowerShell Version = 0x00800000, // -Version | -v WindowStyle = 0x01000000, // -WindowStyle | -w WorkingDirectory = 0x02000000, // -WorkingDirectory | -wd + ConfigurationFile = 0x04000000, // -ConfigurationFile // Enum values for specified ExecutionPolicy EPUnrestricted = 0x0000000100000000, // ExecutionPolicy unrestricted EPRemoteSigned = 0x0000000200000000, // ExecutionPolicy remote signed @@ -370,6 +371,15 @@ namespace Microsoft.PowerShell } } + internal string? ConfigurationFile + { + get + { + AssertArgumentsParsed(); + return _configurationFile; + } + } + internal string? ConfigurationName { get @@ -915,6 +925,19 @@ namespace Microsoft.PowerShell _noInteractive = false; ParametersUsed |= ParameterBitmap.Interactive; } + else if (MatchSwitch(switchKey, "configurationfile", "configurationfile")) + { + ++i; + if (i >= args.Length) + { + SetCommandLineError( + CommandLineParameterParserStrings.MissingConfigurationFileArgument); + break; + } + + _configurationFile = args[i]; + ParametersUsed |= ParameterBitmap.ConfigurationFile; + } else if (MatchSwitch(switchKey, "configurationname", "config")) { ++i; @@ -1474,6 +1497,7 @@ namespace Microsoft.PowerShell private bool _namedPipeServerMode; private bool _sshServerMode; private bool _showVersion; + private string? _configurationFile; private string? _configurationName; private string? _error; private bool _showHelp; diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs index 6a78b4c864..9dbfdf8c80 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs @@ -73,6 +73,9 @@ namespace Microsoft.PowerShell /// /// Help text for minishell. This is displayed on 'minishell -?'. /// + /// + /// True when an external caller provides an InitialSessionState object, which can conflict with '-ConfigurationFile' argument. + /// /// /// The exit code for the shell. /// @@ -99,7 +102,10 @@ namespace Microsoft.PowerShell /// Anyone checking the exit code of the shell or monitor can mask off the high word to determine the exit code passed /// by the script that the shell last executed. /// - internal static int Start(string bannerText, string helpText) + internal static int Start( + string bannerText, + string helpText, + bool issProvidedExternally) { #if DEBUG if (Environment.GetEnvironmentVariable("POWERSHELL_DEBUG_STARTUP") != null) @@ -111,6 +117,12 @@ namespace Microsoft.PowerShell } #endif + // Check for external InitialSessionState configuration conflict with '-ConfigurationFile' argument. + if (issProvidedExternally && !string.IsNullOrEmpty(s_cpp.ConfigurationFile)) + { + throw new ConsoleHostStartupException(ConsoleHostStrings.ShellCannotBeStartedWithConfigConflict); + } + // put PSHOME in front of PATH so that calling `powershell` within `powershell` always starts the same running version string path = Environment.GetEnvironmentVariable("PATH"); string pshome = Utils.DefaultPowerShellAppBase + Path.PathSeparator; @@ -188,7 +200,6 @@ namespace Microsoft.PowerShell #if !UNIX TaskbarJumpList.CreateRunAsAdministratorJumpList(); #endif - // First check for and handle PowerShell running in a server mode. if (s_cpp.ServerMode) { @@ -197,7 +208,9 @@ namespace Microsoft.PowerShell StdIOProcessMediator.Run( initialCommand: s_cpp.InitialCommand, workingDirectory: s_cpp.WorkingDirectory, - configurationName: null); + configurationName: null, + configurationFile: s_cpp.ConfigurationFile, + combineErrOutStream: false); exitCode = 0; } else if (s_cpp.SSHServerMode) @@ -207,7 +220,9 @@ namespace Microsoft.PowerShell StdIOProcessMediator.Run( initialCommand: s_cpp.InitialCommand, workingDirectory: null, - configurationName: null); + configurationName: null, + configurationFile: s_cpp.ConfigurationFile, + combineErrOutStream: true); exitCode = 0; } else if (s_cpp.NamedPipeServerMode) @@ -301,8 +316,8 @@ namespace Microsoft.PowerShell PowerShellConfig.Instance.SetSystemConfigFilePath(s_cpp.SettingsFile); } - // Check registry setting for a Group Policy ConfigurationName entry and - // use it to override anything set by the user. + // Check registry setting for a Group Policy ConfigurationName entry, + // and use it to override anything set by the user on the command line. // It depends on setting file so 'SetSystemConfigFilePath()' should be called before. s_cpp.ConfigurationName = CommandLineParameterParser.GetConfigurationNameFromGroupPolicy(); } @@ -1489,7 +1504,7 @@ namespace Microsoft.PowerShell // NTRAID#Windows Out Of Band Releases-915506-2005/09/09 // Removed HandleUnexpectedExceptions infrastructure - exitCode = DoRunspaceLoop(cpp.InitialCommand, cpp.SkipProfiles, cpp.Args, cpp.StaMode, cpp.ConfigurationName); + exitCode = DoRunspaceLoop(cpp.InitialCommand, cpp.SkipProfiles, cpp.Args, cpp.StaMode, cpp.ConfigurationName, cpp.ConfigurationFile); } while (false); @@ -1502,13 +1517,19 @@ namespace Microsoft.PowerShell /// /// The process exit code to be returned by Main. /// - private uint DoRunspaceLoop(string initialCommand, bool skipProfiles, Collection initialCommandArgs, bool staMode, string configurationName) + private uint DoRunspaceLoop( + string initialCommand, + bool skipProfiles, + Collection initialCommandArgs, + bool staMode, + string configurationName, + string configurationFilePath) { ExitCode = ExitCodeSuccess; while (!ShouldEndSession) { - RunspaceCreationEventArgs args = new RunspaceCreationEventArgs(initialCommand, skipProfiles, staMode, configurationName, initialCommandArgs); + RunspaceCreationEventArgs args = new RunspaceCreationEventArgs(initialCommand, skipProfiles, staMode, configurationName, configurationFilePath, initialCommandArgs); CreateRunspace(args); if (ExitCode == ExitCodeInitFailure) { break; } @@ -1584,14 +1605,12 @@ namespace Microsoft.PowerShell return e; } - private void CreateRunspace(object runspaceCreationArgs) + private void CreateRunspace(RunspaceCreationEventArgs runspaceCreationArgs) { - RunspaceCreationEventArgs args = null; try { - args = runspaceCreationArgs as RunspaceCreationEventArgs; - Dbg.Assert(args != null, "Event Arguments to CreateRunspace should not be null"); - DoCreateRunspace(args.InitialCommand, args.SkipProfiles, args.StaMode, args.ConfigurationName, args.InitialCommandArgs); + Dbg.Assert(runspaceCreationArgs != null, "Arguments to CreateRunspace should not be null."); + DoCreateRunspace(runspaceCreationArgs); } catch (ConsoleHostStartupException startupException) { @@ -1603,7 +1622,7 @@ namespace Microsoft.PowerShell /// /// Check if a screen reviewer utility is running. /// When a screen reader is running, we don't auto-load the PSReadLine module at startup, - /// since PSReadLine is not accessibility-firendly enough as of today. + /// since PSReadLine is not accessibility-friendly enough as of today. /// private bool IsScreenReaderActive() { @@ -1646,12 +1665,33 @@ namespace Microsoft.PowerShell /// Opens and Initializes the Host's sole Runspace. Processes the startup scripts and runs any command passed on the /// command line. /// - private void DoCreateRunspace(string initialCommand, bool skipProfiles, bool staMode, string configurationName, Collection initialCommandArgs) + /// Runspace creation event arguments. + private void DoCreateRunspace(RunspaceCreationEventArgs args) { - Dbg.Assert(_runspaceRef == null, "runspace should be null"); + Dbg.Assert(_runspaceRef == null, "_runspaceRef field should be null"); Dbg.Assert(DefaultInitialSessionState != null, "DefaultInitialSessionState should not be null"); s_runspaceInitTracer.WriteLine("Calling RunspaceFactory.CreateRunspace"); + // Use session configuration file if provided. + bool customConfigurationProvided = false; + if (!string.IsNullOrEmpty(args.ConfigurationFilePath)) + { + try + { + // Replace DefaultInitialSessionState with the initial state configuration defined by the file. + DefaultInitialSessionState = InitialSessionState.CreateFromSessionConfigurationFile( + path: args.ConfigurationFilePath, + roleVerifier: null, + validateFile: true); + } + catch (Exception ex) + { + throw new ConsoleHostStartupException(ConsoleHostStrings.ShellCannotBeStarted, ex); + } + + customConfigurationProvided = true; + } + try { Runspace consoleRunspace = null; @@ -1667,7 +1707,7 @@ namespace Microsoft.PowerShell // powershell -command "Update-Module PSReadline" // This should work just fine as long as no other instances of PowerShell are running. ReadOnlyCollection defaultImportModulesList = null; - if (LoadPSReadline()) + if (!customConfigurationProvided && LoadPSReadline()) { if (IsScreenReaderActive()) { @@ -1682,7 +1722,7 @@ namespace Microsoft.PowerShell consoleRunspace = RunspaceFactory.CreateRunspace(this, DefaultInitialSessionState); try { - OpenConsoleRunspace(consoleRunspace, staMode); + OpenConsoleRunspace(consoleRunspace, args.StaMode); } catch (Exception) { @@ -1702,7 +1742,7 @@ namespace Microsoft.PowerShell } consoleRunspace = RunspaceFactory.CreateRunspace(this, DefaultInitialSessionState); - OpenConsoleRunspace(consoleRunspace, staMode); + OpenConsoleRunspace(consoleRunspace, args.StaMode); } Runspace.PrimaryRunspace = consoleRunspace; @@ -1734,7 +1774,7 @@ namespace Microsoft.PowerShell _readyForInputTimeInMS = (DateTime.Now - Process.GetCurrentProcess().StartTime).TotalMilliseconds; #endif - DoRunspaceInitialization(skipProfiles, initialCommand, configurationName, initialCommandArgs); + DoRunspaceInitialization(args); } private static void OpenConsoleRunspace(Runspace runspace, bool staMode) @@ -1751,7 +1791,7 @@ namespace Microsoft.PowerShell runspace.Open(); } - private void DoRunspaceInitialization(bool skipProfiles, string initialCommand, string configurationName, Collection initialCommandArgs) + private void DoRunspaceInitialization(RunspaceCreationEventArgs args) { if (_runspaceRef.Runspace.Debugger != null) { @@ -1786,13 +1826,13 @@ namespace Microsoft.PowerShell } } - if (!string.IsNullOrEmpty(configurationName)) + if (!string.IsNullOrEmpty(args.ConfigurationName)) { // If an endpoint configuration is specified then create a loop-back remote runspace targeting // the endpoint and push onto runspace ref stack. Ignore profile and configuration scripts. try { - RemoteRunspace remoteRunspace = HostUtilities.CreateConfiguredRunspace(configurationName, this); + RemoteRunspace remoteRunspace = HostUtilities.CreateConfiguredRunspace(args.ConfigurationName, this); remoteRunspace.ShouldCloseOnPop = true; PushRunspace(remoteRunspace); @@ -1827,7 +1867,7 @@ namespace Microsoft.PowerShell currentUserProfile, currentUserHostSpecificProfile)); - if (!skipProfiles) + if (!args.SkipProfiles) { // Run the profiles. // Profiles are run in the following order: @@ -1887,11 +1927,11 @@ namespace Microsoft.PowerShell tempPipeline.Commands.Add(c); - if (initialCommandArgs != null) + if (args.InitialCommandArgs != null) { // add the args passed to the command. - foreach (CommandParameter p in initialCommandArgs) + foreach (CommandParameter p in args.InitialCommandArgs) { c.Parameters.Add(p); } @@ -1948,19 +1988,19 @@ namespace Microsoft.PowerShell ReportException(e1, exec); } } - else if (!string.IsNullOrEmpty(initialCommand)) + else if (!string.IsNullOrEmpty(args.InitialCommand)) { // Run the command passed on the command line s_tracer.WriteLine("running initial command"); - Pipeline tempPipeline = exec.CreatePipeline(initialCommand, true); + Pipeline tempPipeline = exec.CreatePipeline(args.InitialCommand, true); - if (initialCommandArgs != null) + if (args.InitialCommandArgs != null) { // add the args passed to the command. - foreach (CommandParameter p in initialCommandArgs) + foreach (CommandParameter p in args.InitialCommandArgs) { tempPipeline.Commands[0].Parameters.Add(p); } @@ -1976,7 +2016,7 @@ namespace Microsoft.PowerShell ParseError[] errors; // Detect if they're using input. If so, read from it. - Ast parsedInput = Parser.ParseInput(initialCommand, out tokens, out errors); + Ast parsedInput = Parser.ParseInput(args.InitialCommand, out tokens, out errors); if (AstSearcher.IsUsingDollarInput(parsedInput)) { executionOptions |= Executor.ExecutionOptions.ReadInputObjects; @@ -3021,12 +3061,14 @@ namespace Microsoft.PowerShell bool skipProfiles, bool staMode, string configurationName, + string configurationFilePath, Collection initialCommandArgs) { InitialCommand = initialCommand; SkipProfiles = skipProfiles; StaMode = staMode; ConfigurationName = configurationName; + ConfigurationFilePath = configurationFilePath; InitialCommandArgs = initialCommandArgs; } @@ -3038,6 +3080,8 @@ namespace Microsoft.PowerShell internal string ConfigurationName { get; set; } + internal string ConfigurationFilePath { get; set; } + internal Collection InitialCommandArgs { get; set; } } } // namespace diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleShell.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleShell.cs index 11dd276a52..44f2931162 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleShell.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleShell.cs @@ -21,7 +21,12 @@ namespace Microsoft.PowerShell /// An integer value which should be used as exit code for the process. public static int Start(string? bannerText, string? helpText, string[] args) { - return Start(InitialSessionState.CreateDefault2(), bannerText, helpText, args); + return StartImpl( + initialSessionState: InitialSessionState.CreateDefault2(), + bannerText, + helpText, + args, + issProvided: false); } /// Entry point in to ConsoleShell. Used to create a custom Powershell console application. @@ -31,6 +36,31 @@ namespace Microsoft.PowerShell /// Commandline parameters specified by user. /// An integer value which should be used as exit code for the process. public static int Start(InitialSessionState initialSessionState, string? bannerText, string? helpText, string[] args) + { + return StartImpl( + initialSessionState, + bannerText, + helpText, + args, + issProvided: true); + } + + /// + /// Implementation of entry point to ConsoleShell. + /// Used to create a custom Powershell console application. + /// + /// InitialSessionState to be used by the ConsoleHost. + /// Banner text to be displayed by ConsoleHost. + /// Help text for the shell. + /// Commandline parameters specified by user. + /// True when the InitialSessionState object is provided by caller. + /// An integer value which should be used as exit code for the process. + private static int StartImpl( + InitialSessionState initialSessionState, + string? bannerText, + string? helpText, + string[] args, + bool issProvided) { if (initialSessionState == null) { @@ -45,7 +75,7 @@ namespace Microsoft.PowerShell ConsoleHost.ParseCommandLine(args); ConsoleHost.DefaultInitialSessionState = initialSessionState; - return ConsoleHost.Start(bannerText, helpText); + return ConsoleHost.Start(bannerText, helpText, issProvided); } } } diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ManagedEntrance.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ManagedEntrance.cs index 6dbc21ab24..18df48d360 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ManagedEntrance.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ManagedEntrance.cs @@ -96,7 +96,10 @@ namespace Microsoft.PowerShell ConsoleHost.DefaultInitialSessionState = InitialSessionState.CreateDefault2(); - exitCode = ConsoleHost.Start(banner, ManagedEntranceStrings.UsageHelp); + exitCode = ConsoleHost.Start( + bannerText: banner, + helpText: ManagedEntranceStrings.UsageHelp, + issProvidedExternally: false); } catch (HostException e) { diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx index ab893b91a0..e346331b41 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx @@ -187,7 +187,10 @@ Valid formats are: Cannot process the command because -STA and -MTA are both specified. Specify either -STA or -MTA. - Cannot process the command because -Configuration requires an argument that is a remote endpoint configuration name. Specify this argument and try again. + Cannot process the command because -ConfigurationName requires an argument that is a remote endpoint configuration name. Specify this argument and try again. + + + Cannot process the command because -ConfigurationFile requires an argument that is a session configuration (.pssc) file path. Specify this argument and try again. Cannot process the command because -CustomPipeName requires an argument that is a name of the pipe you want to use. Specify this argument and try again. diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/ConsoleHostStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/ConsoleHostStrings.resx index 7296bb30fe..49c22783c1 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/ConsoleHostStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/ConsoleHostStrings.resx @@ -135,6 +135,9 @@ The shell cannot be started. A failure occurred during initialization: + + The shell cannot be started. An InitialSessionState object has been provided along with a -ConfigurationFile argument. Both configuration directives cannot be used at the same time. + An error has occurred that was not properly handled. Additional information is shown below. The PowerShell process will exit. diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx index 481a4216cf..24a6e78ad6 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx @@ -288,6 +288,14 @@ All parameters are case-insensitive. Example: "pwsh -ConfigurationName AdminRoles" +-ConfigurationFile + + Specifies a session configuration (.pssc) file path. The configuration + contained in the configuration file will be applied to the PowerShell + session. + + Example: "pwsh -ConfigurationFile "C:\ProgramData\PowerShell\MyConfig.pssc" + -CustomPipeName Specifies the name to use for an additional IPC server (named pipe) used diff --git a/src/System.Management.Automation/engine/InitialSessionState.cs b/src/System.Management.Automation/engine/InitialSessionState.cs index bfae1b3e6b..cb4126ed1f 100644 --- a/src/System.Management.Automation/engine/InitialSessionState.cs +++ b/src/System.Management.Automation/engine/InitialSessionState.cs @@ -1312,7 +1312,7 @@ namespace System.Management.Automation.Runspaces /// Creates an initial session state from a PSSC configuration file. /// /// The path to the PSSC session configuration file. - /// + /// InitialSessionState object. public static InitialSessionState CreateFromSessionConfigurationFile(string path) { return CreateFromSessionConfigurationFile(path, null); @@ -1327,10 +1327,48 @@ namespace System.Management.Automation.Runspaces /// target session. If you have a WindowsPrincipal for a user, for example, create a Function that /// checks windowsPrincipal.IsInRole(). /// - /// - public static InitialSessionState CreateFromSessionConfigurationFile(string path, Func roleVerifier) + /// InitialSessionState object. + public static InitialSessionState CreateFromSessionConfigurationFile( + string path, + Func roleVerifier) { - Remoting.DISCPowerShellConfiguration discConfiguration = new Remoting.DISCPowerShellConfiguration(path, roleVerifier); + return CreateFromSessionConfigurationFile(path, roleVerifier, validateFile: false); + } + + /// + /// Creates an initial session state from a PSSC configuration file. + /// + /// The path to the PSSC session configuration file. + /// + /// The verifier that PowerShell should call to determine if groups in the Role entry apply to the + /// target session. If you have a WindowsPrincipal for a user, for example, create a Function that + /// checks windowsPrincipal.IsInRole(). + /// + /// Validates the file contents for supported SessionState options. + /// InitialSessionState object. + public static InitialSessionState CreateFromSessionConfigurationFile( + string path, + Func roleVerifier, + bool validateFile) + { + if (path is null) + { + throw new PSArgumentNullException(nameof(path)); + } + + if (!File.Exists(path)) + { + throw new PSInvalidOperationException( + StringUtil.Format(ConsoleInfoErrorStrings.ConfigurationFileDoesNotExist, path)); + } + + if (!path.EndsWith(".pssc", StringComparison.OrdinalIgnoreCase)) + { + throw new PSInvalidOperationException( + StringUtil.Format(ConsoleInfoErrorStrings.NotConfigurationFile, path)); + } + + Remoting.DISCPowerShellConfiguration discConfiguration = new Remoting.DISCPowerShellConfiguration(path, roleVerifier, validateFile); return discConfiguration.GetInitialSessionState(null); } @@ -5241,7 +5279,6 @@ end { { "Enable-PSSessionConfiguration", new SessionStateCmdletEntry("Enable-PSSessionConfiguration", typeof(EnablePSSessionConfigurationCommand), helpFile) }, { "Get-PSSessionCapability", new SessionStateCmdletEntry("Get-PSSessionCapability", typeof(GetPSSessionCapabilityCommand), helpFile) }, { "Get-PSSessionConfiguration", new SessionStateCmdletEntry("Get-PSSessionConfiguration", typeof(GetPSSessionConfigurationCommand), helpFile) }, - { "New-PSSessionConfigurationFile", new SessionStateCmdletEntry("New-PSSessionConfigurationFile", typeof(NewPSSessionConfigurationFileCommand), helpFile) }, { "Receive-PSSession", new SessionStateCmdletEntry("Receive-PSSession", typeof(ReceivePSSessionCommand), helpFile) }, { "Register-PSSessionConfiguration", new SessionStateCmdletEntry("Register-PSSessionConfiguration", typeof(RegisterPSSessionConfigurationCommand), helpFile) }, { "Unregister-PSSessionConfiguration", new SessionStateCmdletEntry("Unregister-PSSessionConfiguration", typeof(UnregisterPSSessionConfigurationCommand), helpFile) }, @@ -5273,6 +5310,7 @@ end { { "New-ModuleManifest", new SessionStateCmdletEntry("New-ModuleManifest", typeof(NewModuleManifestCommand), helpFile) }, { "New-PSRoleCapabilityFile", new SessionStateCmdletEntry("New-PSRoleCapabilityFile", typeof(NewPSRoleCapabilityFileCommand), helpFile) }, { "New-PSSession", new SessionStateCmdletEntry("New-PSSession", typeof(NewPSSessionCommand), helpFile) }, + { "New-PSSessionConfigurationFile", new SessionStateCmdletEntry("New-PSSessionConfigurationFile", typeof(NewPSSessionConfigurationFileCommand), helpFile) }, { "New-PSSessionOption", new SessionStateCmdletEntry("New-PSSessionOption", typeof(NewPSSessionOptionCommand), helpFile) }, { "New-PSTransportOption", new SessionStateCmdletEntry("New-PSTransportOption", typeof(NewPSTransportOptionCommand), helpFile) }, { "Out-Default", new SessionStateCmdletEntry("Out-Default", typeof(OutDefaultCommand), helpFile) }, diff --git a/src/System.Management.Automation/engine/remoting/commands/NewPSSessionConfigurationFile.cs b/src/System.Management.Automation/engine/remoting/commands/NewPSSessionConfigurationFile.cs index 658876c1d8..e5aa6bef79 100644 --- a/src/System.Management.Automation/engine/remoting/commands/NewPSSessionConfigurationFile.cs +++ b/src/System.Management.Automation/engine/remoting/commands/NewPSSessionConfigurationFile.cs @@ -15,7 +15,6 @@ using System.Text; namespace Microsoft.PowerShell.Commands { -#if !UNIX /// /// New-PSSessionConfigurationFile command implementation /// @@ -1126,7 +1125,6 @@ namespace Microsoft.PowerShell.Commands #endregion } -#endif /// /// New-PSRoleCapabilityFile command implementation diff --git a/src/System.Management.Automation/engine/remoting/fanin/InitialSessionStateProvider.cs b/src/System.Management.Automation/engine/remoting/fanin/InitialSessionStateProvider.cs index d428e521fe..744e8ff8e1 100644 --- a/src/System.Management.Automation/engine/remoting/fanin/InitialSessionStateProvider.cs +++ b/src/System.Management.Automation/engine/remoting/fanin/InitialSessionStateProvider.cs @@ -22,6 +22,8 @@ using Dbg = System.Management.Automation.Diagnostics; namespace System.Management.Automation.Remoting { + #region WSMan endpoint configuration + /// /// This struct is used to represent contents from configuration xml. The /// XML is passed to plugins by WSMan API. @@ -56,6 +58,8 @@ namespace System.Management.Automation.Remoting #endregion + #region Fields + internal string StartupScript; // this field is used only by an Out-Of-Process (IPC) server process internal string InitializationScriptForOutOfProcessRunspace; @@ -71,6 +75,10 @@ namespace System.Management.Automation.Remoting internal PSSessionConfigurationData SessionConfigurationData; internal string ConfigFilePath; + #endregion + + #region Methods + /// /// Using optionName and optionValue updates the current object. /// @@ -324,6 +332,8 @@ namespace System.Management.Automation.Remoting throw PSTraceSource.NewArgumentException("typeToLoad", RemotingErrorIdStrings.UnableToLoadType, EndPointConfigurationTypeName, ConfigurationDataFromXML.INITPARAMETERSTOKEN); } + + #endregion } /// @@ -450,7 +460,8 @@ namespace System.Management.Automation.Remoting ... */ - internal static ConfigurationDataFromXML LoadEndPointConfiguration(string shellId, + internal static ConfigurationDataFromXML LoadEndPointConfiguration( + string shellId, string initializationParameters) { ConfigurationDataFromXML configData = null; @@ -798,6 +809,8 @@ namespace System.Management.Automation.Remoting /// internal sealed class DefaultRemotePowerShellConfiguration : PSSessionConfiguration { + #region Method overrides + /// /// /// @@ -852,9 +865,15 @@ namespace System.Management.Automation.Remoting return sessionState; } + + #endregion } - #region Declarative Initial Session Configuration + #endregion + + #region Declarative InitialSession Configuration + + #region Supporting types /// /// Specifies type of initial session state to use. Valid values are Empty and Default. @@ -898,6 +917,10 @@ namespace System.Management.Automation.Remoting } } + #endregion + + #region ConfigFileConstants + /// /// Configuration file constants. /// @@ -1355,6 +1378,8 @@ namespace System.Management.Automation.Remoting } } + #endregion + #region DISC Utilities /// @@ -1681,6 +1706,8 @@ namespace System.Management.Automation.Remoting #endregion + #region DISCPowerShellConfiguration + /// /// Creates an initial session state based on the configuration language for PSSC files. /// @@ -1706,7 +1733,11 @@ namespace System.Management.Automation.Remoting /// target session. If you have a WindowsPrincipal for a user, for example, create a Function that /// checks windowsPrincipal.IsInRole(). /// - internal DISCPowerShellConfiguration(string configFile, Func roleVerifier) + /// Validate file for supported configuration options. + internal DISCPowerShellConfiguration( + string configFile, + Func roleVerifier, + bool validateFile = false) { _configFile = configFile; if (roleVerifier == null) @@ -1726,6 +1757,12 @@ namespace System.Management.Automation.Remoting configFile, out scriptName); _configHash = DISCUtils.LoadConfigFile(Runspace.DefaultRunspace.ExecutionContext, script); + + if (validateFile) + { + DISCFileValidation.ValidateContents(_configHash); + } + MergeRoleRulesIntoConfigHash(roleVerifier); MergeRoleCapabilitiesIntoConfigHash(); @@ -2890,4 +2927,110 @@ namespace System.Management.Automation.Remoting } } #endregion + + #region DISCFileValidation + + internal static class DISCFileValidation + { + // Set of supported configuration options for a PowerShell InitialSessionState. +#if UNIX + private static readonly HashSet SupportedConfigOptions = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "AliasDefinitions", + "AssembliesToLoad", + "Author", + "CompanyName", + "Copyright", + "Description", + "EnvironmentVariables", + "FormatsToProcess", + "FunctionDefinitions", + "GUID", + "LanguageMode", + "ModulesToImport", + "MountUserDrive", + "SchemaVersion", + "ScriptsToProcess", + "SessionType", + "TranscriptDirectory", + "TypesToProcess", + "UserDriveMaximumSize", + "VisibleAliases", + "VisibleCmdlets", + "VariableDefinitions", + "VisibleExternalCommands", + "VisibleFunctions", + "VisibleProviders" + }; +#else + private static readonly HashSet SupportedConfigOptions = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "AliasDefinitions", + "AssembliesToLoad", + "Author", + "CompanyName", + "Copyright", + "Description", + "EnvironmentVariables", + "ExecutionPolicy", + "FormatsToProcess", + "FunctionDefinitions", + "GUID", + "LanguageMode", + "ModulesToImport", + "MountUserDrive", + "SchemaVersion", + "ScriptsToProcess", + "SessionType", + "TranscriptDirectory", + "TypesToProcess", + "UserDriveMaximumSize", + "VisibleAliases", + "VisibleCmdlets", + "VariableDefinitions", + "VisibleExternalCommands", + "VisibleFunctions", + "VisibleProviders" + }; +#endif + + // These are configuration options for WSMan (WinRM) endpoint configurations, that + // appearand in .pssc files, but are not part of PowerShell InitialSessionState. + private static readonly HashSet UnsupportedConfigOptions = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "GroupManagedServiceAccount", + "PowerShellVersion", + "RequiredGroups", + "RoleDefinitions", + "RunAsVirtualAccount", + "RunAsVirtualAccountGroups" + }; + + internal static void ValidateContents(Hashtable configHash) + { + foreach (var key in configHash.Keys) + { + if (key is not string keyName) + { + throw new PSInvalidOperationException(RemotingErrorIdStrings.DISCInvalidConfigKeyType); + } + + if (UnsupportedConfigOptions.Contains(keyName)) + { + throw new PSInvalidOperationException( + StringUtil.Format(RemotingErrorIdStrings.DISCUnsupportedConfigName, keyName)); + } + + if (!SupportedConfigOptions.Contains(keyName)) + { + throw new PSInvalidOperationException( + StringUtil.Format(RemotingErrorIdStrings.DISCUnknownConfigName, keyName)); + } + } + } + } + + #endregion + + #endregion } diff --git a/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs b/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs index 2883bfd68f..5636835e19 100644 --- a/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs +++ b/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs @@ -1980,10 +1980,10 @@ namespace System.Management.Automation.Remoting.Client break; } - if (data.StartsWith(System.Management.Automation.Remoting.Server.NamedPipeErrorTextWriter.ErrorPrefix, StringComparison.OrdinalIgnoreCase)) + if (data.StartsWith(System.Management.Automation.Remoting.Server.FormattedErrorTextWriter.ErrorPrefix, StringComparison.OrdinalIgnoreCase)) { // Error message from the server. - string errorData = data.Substring(System.Management.Automation.Remoting.Server.NamedPipeErrorTextWriter.ErrorPrefix.Length); + string errorData = data.Substring(System.Management.Automation.Remoting.Server.FormattedErrorTextWriter.ErrorPrefix.Length); HandleErrorDataReceived(errorData); } else @@ -2592,6 +2592,12 @@ namespace System.Management.Automation.Remoting.Server _stdOutWriter = outWriter; _stdErrWriter = errWriter; _cmdTransportManagers = new Dictionary(); + + this.WSManTransportErrorOccured += (object sender, TransportErrorOccuredEventArgs e) => + { + string msg = e.Exception.TransportMessage ?? e.Exception.InnerException?.Message ?? string.Empty; + _stdErrWriter.WriteLine(StringUtil.Format(RemotingErrorIdStrings.RemoteTransportError, msg)); + }; } #endregion diff --git a/src/System.Management.Automation/engine/remoting/fanin/WSManPlugin.cs b/src/System.Management.Automation/engine/remoting/fanin/WSManPlugin.cs index 9382beb9b3..79d583e063 100644 --- a/src/System.Management.Automation/engine/remoting/fanin/WSManPlugin.cs +++ b/src/System.Management.Automation/engine/remoting/fanin/WSManPlugin.cs @@ -306,10 +306,16 @@ namespace System.Management.Automation.Remoting PSOpcode.Connect, PSTask.None, PSKeyword.ManagedPlugin | PSKeyword.UseAlwaysAnalytic, requestDetails.ToString(), senderInfo.UserInfo.Identity.Name, requestDetails.resourceUri); - ServerRemoteSession remoteShellSession = ServerRemoteSession.CreateServerRemoteSession(senderInfo, - requestDetails.resourceUri, - extraInfo, - serverTransportMgr); + + ServerRemoteSession remoteShellSession = ServerRemoteSession.CreateServerRemoteSession( + senderInfo: senderInfo, + configurationProviderId: requestDetails.resourceUri, + initializationParameters: extraInfo, + transportManager: serverTransportMgr, + initialCommand: null, // Not used by WinRM endpoint. + configurationName: null, // Not used by WinRM endpoint, which has its own configuration. + configurationFile: null, // Same. + initialLocation: null); // Same. if (remoteShellSession == null) { diff --git a/src/System.Management.Automation/engine/remoting/server/OutOfProcServerMediator.cs b/src/System.Management.Automation/engine/remoting/server/OutOfProcServerMediator.cs index 510dbeea19..6fd6ae59cd 100644 --- a/src/System.Management.Automation/engine/remoting/server/OutOfProcServerMediator.cs +++ b/src/System.Management.Automation/engine/remoting/server/OutOfProcServerMediator.cs @@ -300,6 +300,7 @@ namespace System.Management.Automation.Remoting.Server protected OutOfProcessServerSessionTransportManager CreateSessionTransportManager( string configurationName, + string configurationFile, PSRemotingCryptoHelperServer cryptoHelper, string workingDirectory) { @@ -317,14 +318,20 @@ namespace System.Management.Automation.Remoting.Server senderInfo = new PSSenderInfo(userPrincipal, "http://localhost"); #endif - OutOfProcessServerSessionTransportManager tm = new OutOfProcessServerSessionTransportManager(originalStdOut, originalStdErr, cryptoHelper); + var tm = new OutOfProcessServerSessionTransportManager( + originalStdOut, + originalStdErr, + cryptoHelper); ServerRemoteSession.CreateServerRemoteSession( - senderInfo, - _initialCommand, - tm, - configurationName, - workingDirectory); + senderInfo: senderInfo, + configurationProviderId: "Microsoft.PowerShell", + initializationParameters: string.Empty, + transportManager: tm, + initialCommand: _initialCommand, + configurationName: configurationName, + configurationFile: configurationFile, + initialLocation: workingDirectory); return tm; } @@ -333,11 +340,16 @@ namespace System.Management.Automation.Remoting.Server string initialCommand, PSRemotingCryptoHelperServer cryptoHelper, string workingDirectory, - string configurationName) + string configurationName, + string configurationFile) { _initialCommand = initialCommand; - sessionTM = CreateSessionTransportManager(configurationName, cryptoHelper, workingDirectory); + sessionTM = CreateSessionTransportManager( + configurationName: configurationName, + configurationFile: configurationFile, + cryptoHelper: cryptoHelper, + workingDirectory: workingDirectory); try { @@ -348,7 +360,11 @@ namespace System.Management.Automation.Remoting.Server { if (sessionTM == null) { - sessionTM = CreateSessionTransportManager(configurationName, cryptoHelper, workingDirectory); + sessionTM = CreateSessionTransportManager( + configurationName: configurationName, + configurationFile: configurationFile, + cryptoHelper: cryptoHelper, + workingDirectory: workingDirectory); } } @@ -432,7 +448,8 @@ namespace System.Management.Automation.Remoting.Server /// It will replace StdIn,StdOut and StdErr stream with TextWriter.Null. This is /// to make sure these streams are totally used by our Mediator. /// - private StdIOProcessMediator() : base(true) + /// Redirects remoting errors to the Out stream. + private StdIOProcessMediator(bool combineErrOutStream) : base(exitProcessOnError: true) { // Create input stream reader from Console standard input stream. // We don't use the provided Console.In TextReader because it can have @@ -441,16 +458,23 @@ namespace System.Management.Automation.Remoting.Server // stream encoding. This way the stream encoding is determined by the // stream BOM as needed. originalStdIn = new StreamReader(Console.OpenStandardInput(), true); - Console.SetIn(TextReader.Null); - // replacing StdOut with Null so that no other app messes with the - // original stream + // Remoting errors can optionally be written to stdErr or stdOut with + // special formatting. originalStdOut = new OutOfProcessTextWriter(Console.Out); - Console.SetOut(TextWriter.Null); + if (combineErrOutStream) + { + originalStdErr = new FormattedErrorTextWriter(Console.Out); + } + else + { + originalStdErr = new OutOfProcessTextWriter(Console.Error); + } - // replacing StdErr with Null so that no other app messes with the - // original stream - originalStdErr = new OutOfProcessTextWriter(Console.Error); + // Replacing StdIn, StdOut, StdErr with Null so that no other app messes with the + // original streams. + Console.SetIn(TextReader.Null); + Console.SetOut(TextWriter.Null); Console.SetError(TextWriter.Null); } @@ -464,10 +488,14 @@ namespace System.Management.Automation.Remoting.Server /// Specifies the initialization script. /// Specifies the initial working directory. The working directory is set before the initial command. /// Specifies an optional configuration name that configures the endpoint session. + /// Specifies an optional path to a configuration (.pssc) file for the session. + /// Specifies the option to write remoting errors to stdOut stream, with special formatting. internal static void Run( string initialCommand, string workingDirectory, - string configurationName) + string configurationName, + string configurationFile, + bool combineErrOutStream) { lock (SyncObject) { @@ -477,14 +505,15 @@ namespace System.Management.Automation.Remoting.Server return; } - s_singletonInstance = new StdIOProcessMediator(); + s_singletonInstance = new StdIOProcessMediator(combineErrOutStream); } s_singletonInstance.Start( initialCommand: initialCommand, cryptoHelper: new PSRemotingCryptoHelperServer(), workingDirectory: workingDirectory, - configurationName: configurationName); + configurationName: configurationName, + configurationFile: configurationFile); } #endregion @@ -526,7 +555,7 @@ namespace System.Management.Automation.Remoting.Server // Create transport reader/writers from named pipe. originalStdIn = namedPipeServer.TextReader; originalStdOut = new OutOfProcessTextWriter(namedPipeServer.TextWriter); - originalStdErr = new NamedPipeErrorTextWriter(namedPipeServer.TextWriter); + originalStdErr = new FormattedErrorTextWriter(namedPipeServer.TextWriter); #if !UNIX // Flow impersonation as needed. @@ -557,17 +586,18 @@ namespace System.Management.Automation.Remoting.Server initialCommand: initialCommand, cryptoHelper: new PSRemotingCryptoHelperServer(), workingDirectory: null, - configurationName: namedPipeServer.ConfigurationName); + configurationName: namedPipeServer.ConfigurationName, + configurationFile: null); } #endregion } - internal sealed class NamedPipeErrorTextWriter : OutOfProcessTextWriter + internal sealed class FormattedErrorTextWriter : OutOfProcessTextWriter { #region Constructors - internal NamedPipeErrorTextWriter( + internal FormattedErrorTextWriter( TextWriter textWriter) : base(textWriter) { } @@ -575,6 +605,8 @@ namespace System.Management.Automation.Remoting.Server #region Base class overrides + // Write error data to stream with 'ErrorPrefix' prefix that will + // be interpreted by the client. public override void WriteLine(string data) { string dataToWrite = (data != null) ? ErrorPrefix + data : null; @@ -632,7 +664,8 @@ namespace System.Management.Automation.Remoting.Server initialCommand: initialCommand, cryptoHelper: new PSRemotingCryptoHelperServer(), workingDirectory: null, - configurationName: configurationName); + configurationName: configurationName, + configurationFile: null); } #endregion diff --git a/src/System.Management.Automation/engine/remoting/server/serverremotesession.cs b/src/System.Management.Automation/engine/remoting/server/serverremotesession.cs index fe31e0fad7..8bfc137f35 100644 --- a/src/System.Management.Automation/engine/remoting/server/serverremotesession.cs +++ b/src/System.Management.Automation/engine/remoting/server/serverremotesession.cs @@ -89,6 +89,10 @@ namespace System.Management.Automation.Remoting // Creates a pushed remote runspace session created with this configuration name. private string _configurationName; + // Specifies an optional .pssc configuration file path for out-of-proc session use. + // The .pssc file is used to configure the runspace for the endpoint session. + private string _configurationFile; + // Specifies an initial location of the powershell session. private string _initialLocation; @@ -173,7 +177,9 @@ namespace System.Management.Automation.Remoting /// xml. /// /// + /// Optional initial command used for OutOfProc sessions. /// Optional configuration endpoint name for OutOfProc sessions. + /// Optional configuration file (.pssc) path for OutOfProc sessions. /// Optional configuration initial location of the powershell session. /// /// @@ -192,8 +198,10 @@ namespace System.Management.Automation.Remoting string configurationProviderId, string initializationParameters, AbstractServerSessionTransportManager transportManager, - string configurationName = null, - string initialLocation = null) + string initialCommand, + string configurationName, + string configurationFile, + string initialLocation) { Dbg.Assert( (senderInfo != null) && (senderInfo.UserInfo != null), @@ -215,7 +223,9 @@ namespace System.Management.Automation.Remoting initializationParameters, transportManager) { + _initScriptForOutOfProcRS = initialCommand, _configurationName = configurationName, + _configurationFile = configurationFile, _initialLocation = initialLocation }; @@ -226,33 +236,6 @@ namespace System.Management.Automation.Remoting return result; } - /// - /// Used by OutOfProcessServerMediator to create a remote session. - /// - /// - /// - /// - /// - /// - /// - internal static ServerRemoteSession CreateServerRemoteSession( - PSSenderInfo senderInfo, - string initializationScriptForOutOfProcessRunspace, - AbstractServerSessionTransportManager transportManager, - string configurationName, - string initialLocation) - { - ServerRemoteSession result = CreateServerRemoteSession( - senderInfo, - "Microsoft.PowerShell", - string.Empty, - transportManager, - configurationName: configurationName, - initialLocation: initialLocation); - result._initScriptForOutOfProcRS = initializationScriptForOutOfProcessRunspace; - return result; - } - #endregion #region Overrides @@ -754,8 +737,7 @@ namespace System.Management.Automation.Remoting // Get Initial Session State from custom session config suppliers // like Exchange. ConfigurationDataFromXML configurationData = - PSSessionConfiguration.LoadEndPointConfiguration(_configProviderId, - _initParameters); + PSSessionConfiguration.LoadEndPointConfiguration(_configProviderId, _initParameters); // used by Out-Of-Proc (IPC) runspace. configurationData.InitializationScriptForOutOfProcessRunspace = _initScriptForOutOfProcRS; // start with data from configuration XML and then override with data @@ -763,8 +745,6 @@ namespace System.Management.Automation.Remoting _maxRecvdObjectSize = configurationData.MaxReceivedObjectSizeMB; _maxRecvdDataSizeCommand = configurationData.MaxReceivedCommandSizeMB; - DISCPowerShellConfiguration discProvider = null; - if (string.IsNullOrEmpty(configurationData.ConfigFilePath)) { _sessionConfigProvider = configurationData.CreateEndPointConfigurationInstance(); @@ -772,11 +752,8 @@ namespace System.Management.Automation.Remoting else { System.Security.Principal.WindowsPrincipal windowsPrincipal = new System.Security.Principal.WindowsPrincipal(_senderInfo.UserInfo.WindowsIdentity); - Func validator = (role) => windowsPrincipal.IsInRole(role); - - discProvider = new DISCPowerShellConfiguration(configurationData.ConfigFilePath, validator); - _sessionConfigProvider = discProvider; + _sessionConfigProvider = new DISCPowerShellConfiguration(configurationData.ConfigFilePath, validator); } // exchange of ApplicationArguments and ApplicationPrivateData is be done as early as possible @@ -787,6 +764,7 @@ namespace System.Management.Automation.Remoting if (configurationData.SessionConfigurationData != null) { + // Use the provided WinRM endpoint runspace configuration information. try { rsSessionStateToUse = @@ -797,8 +775,21 @@ namespace System.Management.Automation.Remoting rsSessionStateToUse = _sessionConfigProvider.GetInitialSessionState(_senderInfo); } } + else if (!string.IsNullOrEmpty(_configurationFile)) + { + // Use the optional _configurationFile parameter to create the endpoint runspace configuration. + // This parameter is only used by Out-Of-Proc transports (not WinRM transports). + var discConfiguration = new Remoting.DISCPowerShellConfiguration( + configFile: _configurationFile, + roleVerifier: null, + validateFile: true); + rsSessionStateToUse = discConfiguration.GetInitialSessionState(_senderInfo); + } else { + // Create a runspace configuration based on the provided PSSessionConfiguration provider. + // This can be either a 'default' configuration, or third party configuration PSSessionConfiguration provider object. + // So far, only Exchange provides a custom PSSessionConfiguration provider implementation. rsSessionStateToUse = _sessionConfigProvider.GetInitialSessionState(_senderInfo); } diff --git a/src/System.Management.Automation/resources/ConsoleInfoErrorStrings.resx b/src/System.Management.Automation/resources/ConsoleInfoErrorStrings.resx index 201dde0e4a..ba0809e901 100644 --- a/src/System.Management.Automation/resources/ConsoleInfoErrorStrings.resx +++ b/src/System.Management.Automation/resources/ConsoleInfoErrorStrings.resx @@ -234,4 +234,10 @@ The Save operation failed. Cannot remove the file {0}. + + The provided configuration file '{0}' does not exist. + + + The provided configuration file '{0}' must have a .pssc file extension. + diff --git a/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx b/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx index 429557a795..57d2a22a0a 100644 --- a/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx +++ b/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx @@ -1705,4 +1705,13 @@ SSH client process terminated before connection could be established. The Runspace argument to Create must be a non-null RemoteRunspace object. + + The session configuration hash table contains an invalid key type. Keys should be string types. + + + The session configuration file contains an unsupported configuration option: {0}. This is a remoting endpoint configuration option, that does not apply to PowerShell session state. + + + The session configuration file contains an unknown configuration option: {0}. + diff --git a/test/SSHRemoting/SSHRemoting.Basic.Tests.ps1 b/test/SSHRemoting/SSHRemoting.Basic.Tests.ps1 index 29e7096d49..1aa87ba3d6 100644 --- a/test/SSHRemoting/SSHRemoting.Basic.Tests.ps1 +++ b/test/SSHRemoting/SSHRemoting.Basic.Tests.ps1 @@ -230,6 +230,25 @@ Describe "SSHRemoting Basic Tests" -tags CI { VerifySession $script:sessions[1] Write-Verbose -Verbose "It Complete" } + + It "Verifies the 'pwshconfig' configured endpoint." { + Write-Verbose -Verbose "It Starting: Verifies the 'pwshconfig' configured endpoint." + $script:session = TryNewPSSession -HostName localhost -Subsystem 'pwshconfig' + $script:session | Should -Not -BeNullOrEmpty + # Configured session should be in ConstrainedLanguage mode. + $sessionLangMode = Invoke-Command -Session $script:session -ScriptBlock { "$($ExecutionContext.SessionState.LanguageMode)" } + $sessionLangMode | Should -BeExactly "ConstrainedLanguage" + Write-Verbose -Verbose "It Complete" + } + + <# + It "Verifes that 'pwshbroken' throws expected error for missing config file." { + Write-Verbose -Verbose "It Starting: Verifes that 'pwshbroken' throws expected error for missing config file." + { $script:session = TryNewPSSession -HostName localhost -Subsystem 'pwshbroken' } | Should -Throw + $script:session = $null + Write-Verbose -Verbose "It Complete" + } + #> } function TryCreateRunspace @@ -270,7 +289,7 @@ Describe "SSHRemoting Basic Tests" -tags CI { } } - if ($null -eq $rs) + if (($null -eq $rs) -or !($rs -is [runspace])) { $message = "Runspace open unable to connect to SSH remoting endpoint after two attempts. Error: $($connectionError.Message)" throw [System.Management.Automation.PSInvalidOperationException]::new($message) @@ -319,7 +338,7 @@ Describe "SSHRemoting Basic Tests" -tags CI { AfterEach { Write-Verbose -Verbose "Starting Runspace close AfterEach" - if ($script:rs -ne $null) { $script:rs.Dispose() } + if (($script:rs -ne $null) -and ($script:rs -is [runspace])) { $script:rs.Dispose() } Write-Verbose -Verbose "AfterEach complete" } diff --git a/test/hosting/test_HostingBasic.cs b/test/hosting/test_HostingBasic.cs index 6cff7978e4..e29b14e38c 100644 --- a/test/hosting/test_HostingBasic.cs +++ b/test/hosting/test_HostingBasic.cs @@ -183,6 +183,19 @@ namespace PowerShell.Hosting.SDK.Tests Assert.Equal(42, ret); } + /* Test disabled because CommandLineParser is static and can only be intialized once (above in TestConsoleShellScenario) + /// + /// ConsoleShell cannot start with both InitialSessionState and -ConfigurationFile argument configurations specified. + /// + [Fact] + public static void TestConsoleShellConfigConflictError() + { + var iss = System.Management.Automation.Runspaces.InitialSessionState.CreateDefault2(); + int ret = ConsoleShell.Start(iss, "BannerText", string.Empty, new string[] { @"-ConfigurationFile ""noneSuch""" }); + Assert.Equal(70, ret); // ExitCodeInitFailure. + } + */ + [Fact] public static void TestBuiltInModules() { diff --git a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 index e970d5a06a..aaa6cb432b 100644 --- a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 +++ b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 @@ -375,7 +375,7 @@ Describe "Verify approved aliases list" -Tags "CI" { "Cmdlet", "New-PSDrive", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "Low" "Cmdlet", "New-PSRoleCapabilityFile", "", $( $CoreWindows -or $CoreUnix), "", "", "None" "Cmdlet", "New-PSSession", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None" -"Cmdlet", "New-PSSessionConfigurationFile", "", $($FullCLR -or $CoreWindows ), "", "", "None" +"Cmdlet", "New-PSSessionConfigurationFile", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None" "Cmdlet", "New-PSSessionOption", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None" "Cmdlet", "New-PSTransportOption", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None" "Cmdlet", "New-Service", "", $($FullCLR -or $CoreWindows ), "", "", "Medium" diff --git a/test/tools/Modules/HelpersRemoting/HelpersRemoting.psm1 b/test/tools/Modules/HelpersRemoting/HelpersRemoting.psm1 index b554436426..5c88a4c053 100644 --- a/test/tools/Modules/HelpersRemoting/HelpersRemoting.psm1 +++ b/test/tools/Modules/HelpersRemoting/HelpersRemoting.psm1 @@ -556,7 +556,28 @@ function Install-SSHRemotingOnLinux Write-Verbose -Verbose "Running Enable-SSHRemoting ..." Write-Verbose -Verbose "PSScriptRoot: $PSScriptRoot" $modulePath = "${PSScriptRoot}\..\Microsoft.PowerShell.RemotingTools\Microsoft.PowerShell.RemotingTools.psd1" - $cmdLine = "Import-Module ${modulePath}; Enable-SSHRemoting -SSHDConfigFilePath /etc/ssh/sshd_config -PowerShellFilePath $PowerShellPath -Force" + $sshdFilePath = '/etc/ssh/sshd_config' + + # First create a default 'powershell' named endpoint. + $cmdLine = "Import-Module ${modulePath}; Enable-SSHRemoting -SSHDConfigFilePath $sshdFilePath -PowerShellFilePath $PowerShellPath -Force" + Write-Verbose -Verbose "CmdLine: $cmdLine" + sudo pwsh -c $cmdLine + + # Next create a 'pwshconfig' named configured endpoint. + # Configuration file: + $configFilePath = Join-Path -Path "$env:HOME" -ChildPath 'PSTestConfig.pssc' + '@{ + GUID = "4d667b90-25f8-47d5-9c90-619b27954748" + Author = "Microsoft" + Description = "Test local PowerShell session configuration" + LanguageMode = "ConstrainedLanguage" + }' | Out-File -FilePath $configFilePath + $cmdLine = "Import-Module ${modulePath}; Enable-SSHRemoting -SSHDConfigFilePath $sshdFilePath -PowerShellFilePath $PowerShellPath -ConfigFilePath $configFilePath -SubsystemName 'pwshconfig' -Force" + Write-Verbose -Verbose "CmdLine: $cmdLine" + sudo pwsh -c $cmdLine + + # Finally create a 'pwshbroken' named configured endpoint. + $cmdLine = "Import-Module ${modulePath}; Enable-SSHRemoting -SSHDConfigFilePath $sshdFilePath -PowerShellFilePath $PowerShellPath -ConfigFilePath '$HOME/NoSuch.pssc' -SubsystemName 'pwshbroken' -Force" Write-Verbose -Verbose "CmdLine: $cmdLine" sudo pwsh -c $cmdLine diff --git a/test/tools/Modules/Microsoft.PowerShell.RemotingTools/Microsoft.PowerShell.RemotingTools.psm1 b/test/tools/Modules/Microsoft.PowerShell.RemotingTools/Microsoft.PowerShell.RemotingTools.psm1 index 737d0e2930..41a57772fd 100644 --- a/test/tools/Modules/Microsoft.PowerShell.RemotingTools/Microsoft.PowerShell.RemotingTools.psm1 +++ b/test/tools/Modules/Microsoft.PowerShell.RemotingTools/Microsoft.PowerShell.RemotingTools.psm1 @@ -85,14 +85,17 @@ class SSHRemotingConfig [PlatformInfo] $platformInfo [SSHSubSystemEntry[]] $psSubSystemEntries = @() [string] $configFilePath + [string] $subsystemName $configComponents = @() SSHRemotingConfig( [PlatformInfo] $platInfo, - [string] $configFilePath) + [string] $configFilePath, + [string] $subsystemName) { $this.platformInfo = $platInfo $this.configFilePath = $configFilePath + $this.subsystemName = $subsystemName $this.ParseSSHRemotingConfig() } @@ -121,7 +124,7 @@ class SSHRemotingConfig $components = $this.SplitConfigLine($line) $this.configComponents += @{ Line = $line; Components = $components } - if (($components[0] -eq "Subsystem") -and ($components[1] -eq "powershell")) + if (($components[0] -eq "Subsystem") -and ($components[1] -eq $this.subsystemName)) { $entry = [SSHSubSystemEntry]::New() $entry.subSystemLine = $line @@ -143,7 +146,9 @@ function UpdateConfiguration { param ( [SSHRemotingConfig] $config, - [string] $PowerShellPath + [string] $PowerShellPath, + [string] $SubsystemName, + [string] $ConfigFilePath ) # @@ -152,15 +157,19 @@ function UpdateConfiguration # Subsystem [System.Collections.Generic.List[string]] $newContents = [System.Collections.Generic.List[string]]::new() - $psSubSystemEntry = "Subsystem powershell {0} {1} {2} {3}" -f $powerShellPath, "-SSHS", "-NoProfile", "-NoLogo" - $subSystemAdded = $false + $psSubSystemEntry = "Subsystem {0} {1} -SSHS -NoProfile -NoLogo" -f $SubsystemName, $powerShellPath + if (![string]::IsNullOrEmpty($ConfigFilePath)) + { + $psSubSystemEntry += " -ConfigurationFile {0}" -f $ConfigFilePath + } + $subSystemAdded = $false foreach ($lineItem in $config.configComponents) { $line = $lineItem.Line $components = $lineItem.Components - if ($components[0] -eq "SubSystem") + if ($components[0] -eq "Subsystem") { if (! $subSystemAdded) { @@ -169,7 +178,7 @@ function UpdateConfiguration $subSystemAdded = $true } - if ($components[1] -eq "powershell") + if ($components[1] -eq $SubsystemName) { # Remove all existing powershell subsystem entries continue @@ -324,6 +333,10 @@ function Enable-SSHRemoting [string] $PowerShellFilePath, + [string] $SubsystemName = "powershell", + + [string] $ConfigFilePath, + [switch] $Force ) @@ -475,7 +488,7 @@ function Enable-SSHRemoting WriteLine "$SSHDConfigFilePath" -AppendLines 1 # Get the SSHD configurtion - $sshdConfig = [SSHRemotingConfig]::new($platformInfo, $SSHDConfigFilePath) + $sshdConfig = [SSHRemotingConfig]::new($platformInfo, $SSHDConfigFilePath, $SubsystemName) if ($sshdConfig.psSubSystemEntries.Count -gt 0) { @@ -499,7 +512,7 @@ function Enable-SSHRemoting { WriteLine "Updating configuration file ..." -PrependLines 1 -AppendLines 1 - UpdateConfiguration $sshdConfig $PowerShellToUse + UpdateConfiguration $sshdConfig $PowerShellToUse $SubsystemName $ConfigFilePath WriteLine "The configuration file has been updated:" -PrependLines 1 WriteLine $sshdConfig.configFilePath -AppendLines 1 diff --git a/test/xUnit/csharp/test_CommandLineParser.cs b/test/xUnit/csharp/test_CommandLineParser.cs index cf2a128121..70621dfdf0 100644 --- a/test/xUnit/csharp/test_CommandLineParser.cs +++ b/test/xUnit/csharp/test_CommandLineParser.cs @@ -24,6 +24,7 @@ namespace PSTests.Parallel Assert.False(cpp.AbortStartup); Assert.Empty(cpp.Args); Assert.Null(cpp.ConfigurationName); + Assert.Null(cpp.ConfigurationFile); Assert.Null(cpp.CustomPipeName); Assert.Null(cpp.ErrorMessage); Assert.Null(cpp.ExecutionPolicy); @@ -437,6 +438,38 @@ namespace PSTests.Parallel Assert.Null(cpp.ErrorMessage); } + [Theory] + [InlineData("-configurationfile")] + public static void TestParameter_ConfigurationFile_No_Name(params string[] commandLine) + { + var cpp = new CommandLineParameterParser(); + + cpp.Parse(commandLine); + + Assert.True(cpp.AbortStartup); + Assert.True(cpp.NoExit); + Assert.False(cpp.ShowShortHelp); + Assert.False(cpp.ShowBanner); + Assert.Equal((uint)ConsoleHost.ExitCodeBadCommandLineParameter, cpp.ExitCode); + Assert.Equal(CommandLineParameterParserStrings.MissingConfigurationFileArgument, cpp.ErrorMessage); + } + + [Theory] + [InlineData("-configurationfile", "qwerty")] + public static void TestParameter_ConfigurationFile_With_Name(params string[] commandLine) + { + var cpp = new CommandLineParameterParser(); + + cpp.Parse(commandLine); + + Assert.False(cpp.AbortStartup); + Assert.True(cpp.NoExit); + Assert.False(cpp.ShowShortHelp); + Assert.True(cpp.ShowBanner); + Assert.Equal("qwerty", cpp.ConfigurationFile); + Assert.Null(cpp.ErrorMessage); + } + [Theory] [InlineData("-custompipename")] [InlineData("-cus")]