2020-03-24 11:08:37 -07:00
// Copyright (c) Microsoft Corporation.
2018-02-13 09:23:53 -08:00
// Licensed under the MIT License.
2016-07-19 14:37:20 -07:00
using System ;
2018-12-05 12:20:58 -08:00
using System.Collections.Generic ;
2016-07-19 14:37:20 -07:00
using System.IO ;
2019-04-30 10:25:11 +05:00
using System.Management.Automation.Internal ;
2016-07-19 14:37:20 -07:00
using System.Text ;
2016-07-22 14:41:24 -07:00
using System.Threading ;
2019-04-30 10:25:11 +05:00
2016-07-26 16:01:31 -07:00
using Newtonsoft.Json ;
using Newtonsoft.Json.Linq ;
2017-08-03 19:34:21 +04:00
2017-12-21 14:27:49 -08:00
namespace System.Management.Automation.Configuration
2016-07-19 14:37:20 -07:00
{
2018-12-05 12:20:58 -08:00
/// <summary>
/// The scope of the configuration file.
/// </summary>
public enum ConfigScope
2018-01-03 12:38:38 -08:00
{
2018-12-05 12:20:58 -08:00
/// <summary>
/// AllUsers configuration applies to all users.
/// </summary>
AllUsers = 0 ,
2018-01-03 12:38:38 -08:00
2018-12-05 12:20:58 -08:00
/// <summary>
/// CurrentUser configuration applies to the current user.
/// </summary>
2018-01-03 12:38:38 -08:00
CurrentUser = 1
}
2016-07-19 14:37:20 -07:00
/// <summary>
2018-01-03 12:38:38 -08:00
/// Reads from and writes to the JSON configuration files.
/// The config values were originally stored in the Windows registry.
2016-07-19 14:37:20 -07:00
/// </summary>
2019-10-30 14:44:34 -07:00
/// <remarks>
/// The config file access APIs are designed to avoid hitting the disk as much as possible.
/// - For the first read request targeting a config file, the config data is read from the file and then cached as a 'JObject' instance;
/// * the first read request happens very early during the startup of 'pwsh'.
/// - For the subsequent read requests targeting the same config file, they will then work with that 'JObject' instance;
/// - For the write request targeting a config file, the cached config data corresponding to that config file will be refreshed after the write operation is successfully done.
///
/// To summarize the expected behavior:
/// Once a 'pwsh' process starts up -
/// 1. any changes made to the config file from outside this 'pwsh' process is not guaranteed to be seen by it (most likely won't be seen).
/// 2. any changes to the config file by this 'pwsh' process via the config file access APIs will be seen by it, if it chooses to read those changes afterwards.
/// </remarks>
2017-12-21 14:27:49 -08:00
internal sealed class PowerShellConfig
2016-07-19 14:37:20 -07:00
{
2019-11-05 19:46:32 -08:00
private const string ConfigFileName = "powershell.config.json" ;
private const string ExecutionPolicyDefaultShellKey = "Microsoft.PowerShell:ExecutionPolicy" ;
2020-01-03 10:32:00 -08:00
private const string DisableImplicitWinCompatKey = "DisableImplicitWinCompat" ;
2020-02-04 16:33:26 -08:00
private const string WindowsPowerShellCompatibilityModuleDenyListKey = "WindowsPowerShellCompatibilityModuleDenyList" ;
2020-04-16 16:57:55 -07:00
private const string WindowsPowerShellCompatibilityNoClobberModuleListKey = "WindowsPowerShellCompatibilityNoClobberModuleList" ;
2019-10-30 14:44:34 -07:00
2017-12-21 14:27:49 -08:00
// Provide a singleton
2019-10-30 14:44:34 -07:00
internal static readonly PowerShellConfig Instance = new PowerShellConfig ();
2016-07-26 16:01:31 -07:00
2018-01-24 22:12:15 -08:00
// The json file containing system-wide configuration settings.
2019-10-30 14:44:34 -07:00
// When passed as a pwsh command-line option, overrides the system wide configuration file.
2018-01-24 22:12:15 -08:00
private string systemWideConfigFile ;
private string systemWideConfigDirectory ;
// The json file containing the per-user configuration settings.
2019-10-30 14:44:34 -07:00
private readonly string perUserConfigFile ;
private readonly string perUserConfigDirectory ;
2018-01-24 22:12:15 -08:00
2019-10-30 14:44:34 -07:00
// Note: JObject and JsonSerializer are thread safe.
// Root Json objects corresponding to the configuration file for 'AllUsers' and 'CurrentUser' respectively.
// They are used as a cache to avoid hitting the disk for every read operation.
private readonly JObject [] configRoots ;
private readonly JObject emptyConfig ;
private readonly JsonSerializer serializer ;
2016-07-19 14:37:20 -07:00
2016-08-03 14:16:37 -07:00
/// <summary>
2017-12-21 14:27:49 -08:00
/// Lock used to enable multiple concurrent readers and singular write locks within a single process.
/// TODO: This solution only works for IO from a single process.
/// A more robust solution is needed to enable ReaderWriterLockSlim behavior between processes.
2016-08-03 14:16:37 -07:00
/// </summary>
2019-10-30 14:44:34 -07:00
private readonly ReaderWriterLockSlim fileLock ;
2016-08-03 14:16:37 -07:00
2017-12-21 14:27:49 -08:00
private PowerShellConfig ()
2016-07-19 14:37:20 -07:00
{
2018-01-24 22:12:15 -08:00
// Sets the system-wide configuration file.
systemWideConfigDirectory = Utils . DefaultPowerShellAppBase ;
2019-11-05 19:46:32 -08:00
systemWideConfigFile = Path . Combine ( systemWideConfigDirectory , ConfigFileName );
2016-07-19 14:37:20 -07:00
2016-08-08 21:05:48 -07:00
// Sets the per-user configuration directory
2019-10-30 14:44:34 -07:00
// Note: This directory may or may not exist depending upon the execution scenario.
// Writes will attempt to create the directory if it does not already exist.
2019-10-07 02:15:01 -07:00
perUserConfigDirectory = Platform . ConfigDirectory ;
2019-11-05 19:46:32 -08:00
perUserConfigFile = Path . Combine ( perUserConfigDirectory , ConfigFileName );
2019-10-30 14:44:34 -07:00
emptyConfig = new JObject ();
configRoots = new JObject [ 2 ];
serializer = JsonSerializer . Create ( new JsonSerializerSettings { TypeNameHandling = TypeNameHandling . None , MaxDepth = 10 });
fileLock = new ReaderWriterLockSlim ();
2018-01-24 22:12:15 -08:00
}
private string GetConfigFilePath ( ConfigScope scope )
{
return ( scope == ConfigScope . CurrentUser ) ? perUserConfigFile : systemWideConfigFile ;
}
/// <summary>
/// Sets the system wide configuration file path.
/// </summary>
/// <param name="value">A fully qualified path to the system wide configuration file.</param>
/// <exception cref="FileNotFoundException"><paramref name="value"/> is a null reference or the associated file does not exist.</exception>
/// <remarks>
/// This method is for use when processing the -SettingsFile configuration setting and should not be used for any other purpose.
/// </remarks>
internal void SetSystemConfigFilePath ( string value )
{
if (! string . IsNullOrEmpty ( value ) && ! File . Exists ( value ))
{
throw new FileNotFoundException ( value );
}
2018-12-23 22:20:06 -08:00
2018-01-24 22:12:15 -08:00
FileInfo info = new FileInfo ( value );
systemWideConfigFile = info . FullName ;
systemWideConfigDirectory = info . Directory . FullName ;
2016-07-19 14:37:20 -07:00
}
2016-07-25 14:38:33 -07:00
/// <summary>
2017-12-21 14:27:49 -08:00
/// Existing Key = HKLM:\System\CurrentControlSet\Control\Session Manager\Environment
/// Proposed value = %ProgramFiles%\PowerShell\Modules by default
/// Note: There is no setter because this value is immutable.
2016-07-25 14:38:33 -07:00
/// </summary>
2017-12-21 14:27:49 -08:00
/// <param name="scope">Whether this is a system-wide or per-user setting.</param>
2016-08-03 11:52:07 -07:00
/// <returns>Value if found, null otherwise. The behavior matches ModuleIntrinsics.GetExpandedEnvironmentVariable().</returns>
2017-12-21 14:46:35 -08:00
internal string GetModulePath ( ConfigScope scope )
2016-07-19 14:37:20 -07:00
{
2018-01-24 22:12:15 -08:00
string modulePath = ReadValueFromFile < string >( scope , Constants . PSModulePathEnvVar );
2016-08-03 11:52:07 -07:00
if (! string . IsNullOrEmpty ( modulePath ))
2016-07-25 14:38:33 -07:00
{
2016-08-03 11:52:07 -07:00
modulePath = Environment . ExpandEnvironmentVariables ( modulePath );
2016-07-25 14:38:33 -07:00
}
2018-12-23 22:20:06 -08:00
2016-08-03 11:52:07 -07:00
return modulePath ;
2016-07-19 14:37:20 -07:00
}
/// <summary>
/// Existing Key = HKCU and HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell
/// Proposed value = Existing default execution policy if not already specified
2017-01-16 13:31:14 -08:00
///
2016-07-22 10:14:52 -07:00
/// Schema:
/// {
2017-12-21 14:27:49 -08:00
/// "shell-ID-string:ExecutionPolicy" : "execution policy string"
2016-07-22 10:14:52 -07:00
/// }
2017-01-16 13:31:14 -08:00
///
2016-07-26 16:01:31 -07:00
/// TODO: In a single config file, it might be better to nest this. It is unnecessary complexity until a need arises for more nested values.
2016-07-19 14:37:20 -07:00
/// </summary>
/// <param name="scope">Whether this is a system-wide or per-user setting.</param>
2018-12-28 16:39:06 +05:00
/// <param name="shellId">The shell associated with this policy. Typically, it is "Microsoft.PowerShell".</param>
2016-07-26 15:48:33 -07:00
/// <returns>The execution policy if found. Null otherwise.</returns>
2017-12-21 14:46:35 -08:00
internal string GetExecutionPolicy ( ConfigScope scope , string shellId )
2016-07-19 14:37:20 -07:00
{
2019-11-05 19:46:32 -08:00
string key = GetExecutionPolicySettingKey ( shellId );
string execPolicy = ReadValueFromFile < string >( scope , key );
return string . IsNullOrEmpty ( execPolicy ) ? null : execPolicy ;
2016-07-19 14:37:20 -07:00
}
2017-12-21 14:46:35 -08:00
internal void RemoveExecutionPolicy ( ConfigScope scope , string shellId )
2016-07-19 14:37:20 -07:00
{
2019-11-05 19:46:32 -08:00
string key = GetExecutionPolicySettingKey ( shellId );
RemoveValueFromFile < string >( scope , key );
2016-07-19 14:37:20 -07:00
}
2017-12-21 14:46:35 -08:00
internal void SetExecutionPolicy ( ConfigScope scope , string shellId , string executionPolicy )
2016-07-19 14:37:20 -07:00
{
2019-11-05 19:46:32 -08:00
string key = GetExecutionPolicySettingKey ( shellId );
WriteValueToFile < string >( scope , key , executionPolicy );
2016-07-19 14:37:20 -07:00
}
2020-11-27 08:10:43 +00:00
private static string GetExecutionPolicySettingKey ( string shellId )
2016-07-19 14:37:20 -07:00
{
2019-11-05 19:46:32 -08:00
return string . Equals ( shellId , Utils . DefaultPowerShellShellID , StringComparison . Ordinal )
? ExecutionPolicyDefaultShellKey
: string . Concat ( shellId , ":" , "ExecutionPolicy" );
2016-07-19 14:37:20 -07:00
}
2018-06-02 14:13:54 -07:00
/// Get the names of experimental features enabled in the config file.
/// </summary>
internal string [] GetExperimentalFeatures ()
{
2019-10-30 14:44:34 -07:00
string [] features = ReadValueFromFile ( ConfigScope . CurrentUser , "ExperimentalFeatures" , Array . Empty < string >());
2018-12-05 12:20:58 -08:00
if ( features . Length == 0 )
{
2019-10-30 14:44:34 -07:00
features = ReadValueFromFile ( ConfigScope . AllUsers , "ExperimentalFeatures" , Array . Empty < string >());
2018-12-05 12:20:58 -08:00
}
return features ;
}
/// <summary>
/// Set the enabled list of experimental features in the config file.
/// </summary>
/// <param name="scope">The ConfigScope of the configuration file to update.</param>
/// <param name="featureName">The name of the experimental feature to change in the configuration.</param>
/// <param name="setEnabled">If true, add to configuration; otherwise, remove from configuration.</param>
internal void SetExperimentalFeatures ( ConfigScope scope , string featureName , bool setEnabled )
{
var features = new List < string >( GetExperimentalFeatures ());
bool containsFeature = features . Contains ( featureName );
if ( setEnabled && ! containsFeature )
{
features . Add ( featureName );
WriteValueToFile < string []>( scope , "ExperimentalFeatures" , features . ToArray ());
}
else if (! setEnabled && containsFeature )
{
features . Remove ( featureName );
WriteValueToFile < string []>( scope , "ExperimentalFeatures" , features . ToArray ());
}
2018-06-02 14:13:54 -07:00
}
2020-01-03 10:32:00 -08:00
internal bool IsImplicitWinCompatEnabled ()
{
2020-05-31 06:01:08 +01:00
bool settingValue = ReadValueFromFile < bool? >( ConfigScope . CurrentUser , DisableImplicitWinCompatKey )
?? ReadValueFromFile < bool? >( ConfigScope . AllUsers , DisableImplicitWinCompatKey )
?? false ;
2020-01-03 10:32:00 -08:00
2020-05-31 06:01:08 +01:00
return ! settingValue ;
2020-01-03 10:32:00 -08:00
}
2020-02-04 16:33:26 -08:00
internal string [] GetWindowsPowerShellCompatibilityModuleDenyList ()
{
2020-05-31 06:01:08 +01:00
return ReadValueFromFile < string []>( ConfigScope . CurrentUser , WindowsPowerShellCompatibilityModuleDenyListKey )
?? ReadValueFromFile < string []>( ConfigScope . AllUsers , WindowsPowerShellCompatibilityModuleDenyListKey );
2020-02-04 16:33:26 -08:00
}
2020-04-16 16:57:55 -07:00
internal string [] GetWindowsPowerShellCompatibilityNoClobberModuleList ()
{
2020-05-31 06:01:08 +01:00
return ReadValueFromFile < string []>( ConfigScope . CurrentUser , WindowsPowerShellCompatibilityNoClobberModuleListKey )
?? ReadValueFromFile < string []>( ConfigScope . AllUsers , WindowsPowerShellCompatibilityNoClobberModuleListKey );
2020-04-16 16:57:55 -07:00
}
2016-07-19 14:37:20 -07:00
/// <summary>
2019-01-06 04:40:24 +05:00
/// Corresponding settings of the original Group Policies.
2016-07-19 14:37:20 -07:00
/// </summary>
2018-01-03 12:38:38 -08:00
internal PowerShellPolicies GetPowerShellPolicies ( ConfigScope scope )
2016-07-19 14:37:20 -07:00
{
2018-01-24 22:12:15 -08:00
return ReadValueFromFile < PowerShellPolicies >( scope , nameof ( PowerShellPolicies ));
2016-07-19 14:37:20 -07:00
}
2017-11-06 08:32:29 -08:00
#if UNIX
/// <summary>
/// Gets the identity name to use for writing to syslog.
/// </summary>
/// <returns>
2017-12-21 14:27:49 -08:00
/// The string identity to use for writing to syslog. The default value is 'powershell'.
2017-11-06 08:32:29 -08:00
/// </returns>
2017-12-21 14:27:49 -08:00
internal string GetSysLogIdentity ()
2017-11-06 08:32:29 -08:00
{
2018-12-05 12:20:58 -08:00
string identity = ReadValueFromFile < string >( ConfigScope . AllUsers , "LogIdentity" );
2017-11-06 08:32:29 -08:00
if ( string . IsNullOrEmpty ( identity ) ||
identity . Equals ( LogDefaultValue , StringComparison . OrdinalIgnoreCase ))
{
identity = "powershell" ;
}
2018-12-23 22:20:06 -08:00
2017-11-06 08:32:29 -08:00
return identity ;
}
/// <summary>
/// Gets the log level filter.
/// </summary>
2017-12-21 14:27:49 -08:00
/// <returns>
/// One of the PSLevel values indicating the level to log. The default value is PSLevel.Informational.
2017-11-06 08:32:29 -08:00
/// </returns>
2017-12-21 14:27:49 -08:00
internal PSLevel GetLogLevel ()
2017-11-06 08:32:29 -08:00
{
2018-12-05 12:20:58 -08:00
string levelName = ReadValueFromFile < string >( ConfigScope . AllUsers , "LogLevel" );
2017-11-06 08:32:29 -08:00
PSLevel level ;
if ( string . IsNullOrEmpty ( levelName ) ||
levelName . Equals ( LogDefaultValue , StringComparison . OrdinalIgnoreCase ) ||
! Enum . TryParse < PSLevel >( levelName , true , out level ))
{
level = PSLevel . Informational ;
}
2018-12-23 22:20:06 -08:00
2017-11-06 08:32:29 -08:00
return level ;
}
/// <summary>
/// The supported separator characters for listing channels and keywords in configuration.
/// </summary>
2020-11-09 04:37:25 +00:00
private static readonly char [] s_valueSeparators = new char [] { ' ' , ',' , '|' };
2017-11-06 08:32:29 -08:00
/// <summary>
/// Provides a string name to indicate the default for a configuration setting.
/// </summary>
2020-11-09 04:37:25 +00:00
private const string LogDefaultValue = "default" ;
2017-11-06 08:32:29 -08:00
/// <summary>
/// Gets the bitmask of the PSChannel values to log.
/// </summary>
/// <returns>
2017-12-21 14:27:49 -08:00
/// A bitmask of PSChannel.Operational and/or PSChannel.Analytic. The default value is PSChannel.Operational.
2017-11-06 08:32:29 -08:00
/// </returns>
2017-12-21 14:27:49 -08:00
internal PSChannel GetLogChannels ()
2017-11-06 08:32:29 -08:00
{
2018-12-05 12:20:58 -08:00
string values = ReadValueFromFile < string >( ConfigScope . AllUsers , "LogChannels" );
2017-11-06 08:32:29 -08:00
PSChannel result = 0 ;
if (! string . IsNullOrEmpty ( values ))
{
string [] names = values . Split ( s_valueSeparators , StringSplitOptions . RemoveEmptyEntries );
foreach ( string name in names )
{
if ( name . Equals ( LogDefaultValue , StringComparison . OrdinalIgnoreCase ))
{
result = 0 ;
break ;
}
PSChannel value ;
if ( Enum . TryParse < PSChannel >( name , true , out value ))
{
result |= value ;
}
}
}
if ( result == 0 )
{
2018-02-02 10:49:09 -08:00
result = System . Management . Automation . Tracing . PSSysLogProvider . DefaultChannels ;
2017-11-06 08:32:29 -08:00
}
return result ;
}
/// <summary>
/// Gets the bitmask of keywords to log.
/// </summary>
/// <returns>
2017-12-21 14:27:49 -08:00
/// A bitmask of PSKeyword values. The default value is all keywords other than UseAlwaysAnalytic.
2017-11-06 08:32:29 -08:00
/// </returns>
2017-12-21 14:27:49 -08:00
internal PSKeyword GetLogKeywords ()
2017-11-06 08:32:29 -08:00
{
2018-12-05 12:20:58 -08:00
string values = ReadValueFromFile < string >( ConfigScope . AllUsers , "LogKeywords" );
2017-11-06 08:32:29 -08:00
PSKeyword result = 0 ;
if (! string . IsNullOrEmpty ( values ))
{
string [] names = values . Split ( s_valueSeparators , StringSplitOptions . RemoveEmptyEntries );
foreach ( string name in names )
{
if ( name . Equals ( LogDefaultValue , StringComparison . OrdinalIgnoreCase ))
{
result = 0 ;
break ;
}
PSKeyword value ;
if ( Enum . TryParse < PSKeyword >( name , true , out value ))
{
result |= value ;
}
}
}
if ( result == 0 )
{
2018-02-02 10:49:09 -08:00
result = System . Management . Automation . Tracing . PSSysLogProvider . DefaultKeywords ;
2017-11-06 08:32:29 -08:00
}
return result ;
}
#endif // UNIX
2018-01-24 22:12:15 -08:00
/// <summary>
/// Read a value from the configuration file.
/// </summary>
/// <typeparam name="T">The type of the value</typeparam>
/// <param name="scope">The ConfigScope of the configuration file to update.</param>
/// <param name="key">The string key of the value.</param>
/// <param name="defaultValue">The default value to return if the key is not present.</param>
2019-10-30 14:44:34 -07:00
private T ReadValueFromFile < T >( ConfigScope scope , string key , T defaultValue = default )
2016-07-19 14:37:20 -07:00
{
2018-01-24 22:12:15 -08:00
string fileName = GetConfigFilePath ( scope );
2019-10-30 14:44:34 -07:00
JObject configData = configRoots [( int ) scope ];
2018-01-03 12:38:38 -08:00
2020-07-30 16:06:38 -07:00
if ( configData == null )
2016-07-22 10:14:52 -07:00
{
2019-10-30 14:44:34 -07:00
if ( File . Exists ( fileName ))
2016-07-22 10:14:52 -07:00
{
2019-10-30 14:44:34 -07:00
try
2016-07-26 15:48:33 -07:00
{
2019-10-30 14:44:34 -07:00
// Open file for reading, but allow multiple readers
fileLock . EnterReadLock ();
using var stream = OpenFileStreamWithRetry ( fileName , FileMode . Open , FileAccess . Read , FileShare . ReadWrite );
using var jsonReader = new JsonTextReader ( new StreamReader ( stream ));
configData = serializer . Deserialize < JObject >( jsonReader ) ?? emptyConfig ;
}
2020-11-14 06:22:26 +05:00
catch ( Exception exc )
{
throw PSTraceSource . NewInvalidOperationException ( exc , PSConfigurationStrings . CanNotConfigurationFile , args : fileName );
}
2019-10-30 14:44:34 -07:00
finally
{
fileLock . ExitReadLock ();
2016-07-26 15:48:33 -07:00
}
2016-07-22 10:14:52 -07:00
}
2019-10-30 14:44:34 -07:00
else
{
configData = emptyConfig ;
}
// Set the configuration cache.
JObject originalValue = Interlocked . CompareExchange ( ref configRoots [( int ) scope ], configData , null );
if ( originalValue != null )
{
configData = originalValue ;
}
2016-07-22 10:14:52 -07:00
}
2019-10-30 14:44:34 -07:00
if ( configData != emptyConfig && configData . TryGetValue ( key , StringComparison . OrdinalIgnoreCase , out JToken jToken ))
2016-08-03 14:16:37 -07:00
{
2019-11-01 22:39:41 +05:00
return jToken . ToObject < T >( serializer ) ?? defaultValue ;
2016-08-03 14:16:37 -07:00
}
2016-07-22 10:14:52 -07:00
2018-01-03 12:38:38 -08:00
return defaultValue ;
2016-07-22 10:14:52 -07:00
}
2019-10-30 14:44:34 -07:00
private static FileStream OpenFileStreamWithRetry ( string fullPath , FileMode mode , FileAccess access , FileShare share )
2018-11-28 23:09:56 +05:00
{
const int MaxTries = 5 ;
for ( int numTries = 0 ; numTries < MaxTries ; numTries ++)
{
try
{
return new FileStream ( fullPath , mode , access , share );
}
catch ( IOException )
{
if ( numTries == ( MaxTries - 1 ))
{
throw ;
}
Thread . Sleep ( 50 );
}
}
2019-10-30 14:44:34 -07:00
throw new IOException ( nameof ( OpenFileStreamWithRetry ));
2018-11-28 23:09:56 +05:00
}
2016-07-22 10:14:52 -07:00
/// <summary>
2018-01-24 22:12:15 -08:00
/// Update a value in the configuration file.
2016-07-26 15:48:33 -07:00
/// </summary>
2018-01-24 22:12:15 -08:00
/// <typeparam name="T">The type of the value</typeparam>
/// <param name="scope">The ConfigScope of the configuration file to update.</param>
/// <param name="key">The string key of the value.</param>
/// <param name="value">The value to set.</param>
2018-12-28 16:39:06 +05:00
/// <param name="addValue">Whether the key-value pair should be added to or removed from the file.</param>
2018-01-24 22:12:15 -08:00
private void UpdateValueInFile < T >( ConfigScope scope , string key , T value , bool addValue )
2016-07-26 15:48:33 -07:00
{
2016-08-03 14:16:37 -07:00
try
{
2019-10-30 14:44:34 -07:00
string fileName = GetConfigFilePath ( scope );
fileLock . EnterWriteLock ();
// Since multiple properties can be in a single file, replacement is required instead of overwrite if a file already exists.
// Handling the read and write operations within a single FileStream prevents other processes from reading or writing the file while
// the update is in progress. It also locks out readers during write operations.
JObject jsonObject = null ;
using FileStream fs = OpenFileStreamWithRetry ( fileName , FileMode . OpenOrCreate , FileAccess . ReadWrite , FileShare . None );
// UTF8, BOM detection, and bufferSize are the same as the basic stream constructor.
// The most important parameter here is the last one, which keeps underlying stream open after StreamReader is disposed
// so that it can be reused for the subsequent write operation.
using ( StreamReader streamRdr = new StreamReader ( fs , Encoding . UTF8 , detectEncodingFromByteOrderMarks : true , bufferSize : 1024 , leaveOpen : true ))
using ( JsonTextReader jsonReader = new JsonTextReader ( streamRdr ))
2016-07-26 15:48:33 -07:00
{
2019-10-30 14:44:34 -07:00
// Safely determines whether there is content to read from the file
bool isReadSuccess = jsonReader . Read ();
if ( isReadSuccess )
2016-08-03 14:16:37 -07:00
{
2019-10-30 14:44:34 -07:00
// Read the stream into a root JObject for manipulation
jsonObject = serializer . Deserialize < JObject >( jsonReader );
JProperty propertyToModify = jsonObject . Property ( key );
2016-08-03 14:16:37 -07:00
2020-07-30 16:06:38 -07:00
if ( propertyToModify == null )
2019-10-30 14:44:34 -07:00
{
// The property doesn't exist, so add it
if ( addValue )
2016-07-26 15:48:33 -07:00
{
2019-10-30 14:44:34 -07:00
jsonObject . Add ( new JProperty ( key , value ));
2016-07-26 16:01:31 -07:00
}
2019-10-30 14:44:34 -07:00
// else the property doesn't exist so there is nothing to remove
2016-07-26 16:01:31 -07:00
}
else
{
2019-10-30 14:44:34 -07:00
// The property exists
2016-07-26 16:01:31 -07:00
if ( addValue )
{
2019-10-30 14:44:34 -07:00
propertyToModify . Replace ( new JProperty ( key , value ));
2016-07-26 15:48:33 -07:00
}
else
{
2019-10-30 14:44:34 -07:00
propertyToModify . Remove ();
2016-07-26 15:48:33 -07:00
}
}
}
2019-10-30 14:44:34 -07:00
else
2016-07-26 15:48:33 -07:00
{
2019-10-30 14:44:34 -07:00
// The file doesn't already exist and we want to write to it or it exists with no content.
// A new file will be created that contains only this value.
// If the file doesn't exist and a we don't want to write to it, no action is needed.
if ( addValue )
{
jsonObject = new JObject ( new JProperty ( key , value ));
}
else
{
return ;
}
2016-07-26 15:48:33 -07:00
}
}
2019-10-30 14:44:34 -07:00
// Reset the stream position to the beginning so that the
// changes to the file can be written to disk
fs . Seek ( 0 , SeekOrigin . Begin );
// Update the file with new content
using ( StreamWriter streamWriter = new StreamWriter ( fs ))
using ( JsonTextWriter jsonWriter = new JsonTextWriter ( streamWriter ))
{
// The entire document exists within the root JObject.
// I just need to write that object to produce the document.
jsonObject . WriteTo ( jsonWriter );
// This trims the file if the file shrank. If the file grew,
// it is a no-op. The purpose is to trim extraneous characters
// from the file stream when the resultant JObject is smaller
// than the input JObject.
fs . SetLength ( fs . Position );
}
// Refresh the configuration cache.
Interlocked . Exchange ( ref configRoots [( int ) scope ], jsonObject );
2016-08-03 14:16:37 -07:00
}
finally
{
fileLock . ExitWriteLock ();
2016-07-26 15:48:33 -07:00
}
}
/// <summary>
/// TODO: Should this return success, fail, or throw?
2016-07-22 10:14:52 -07:00
/// </summary>
2018-01-24 22:12:15 -08:00
/// <typeparam name="T">The type of value to write.</typeparam>
/// <param name="scope">The ConfigScope of the file to update.</param>
/// <param name="key">The string key of the value.</param>
/// <param name="value">The value to write.</param>
private void WriteValueToFile < T >( ConfigScope scope , string key , T value )
2016-07-22 10:14:52 -07:00
{
2020-11-20 12:26:46 +00:00
if ( scope == ConfigScope . CurrentUser && ! Directory . Exists ( perUserConfigDirectory ))
2018-12-05 12:20:58 -08:00
{
Directory . CreateDirectory ( perUserConfigDirectory );
}
2018-01-24 22:12:15 -08:00
UpdateValueInFile < T >( scope , key , value , true );
2016-07-26 15:48:33 -07:00
}
2016-07-22 10:14:52 -07:00
2016-07-26 15:48:33 -07:00
/// <summary>
/// TODO: Should this return success, fail, or throw?
/// </summary>
2018-01-24 22:12:15 -08:00
/// <typeparam name="T">The type of value to remove.</typeparam>
/// <param name="scope">The ConfigScope of the file to update.</param>
/// <param name="key">The string key of the value.</param>
private void RemoveValueFromFile < T >( ConfigScope scope , string key )
2016-07-26 15:48:33 -07:00
{
2018-01-24 22:12:15 -08:00
string fileName = GetConfigFilePath ( scope );
2016-09-27 10:02:41 -07:00
// Optimization: If the file doesn't exist, there is nothing to remove
if ( File . Exists ( fileName ))
{
2018-01-24 22:12:15 -08:00
UpdateValueInFile < T >( scope , key , default ( T ), false );
2016-09-27 10:02:41 -07:00
}
2016-07-19 14:37:20 -07:00
}
}
2018-01-03 12:38:38 -08:00
#region GroupPolicy Configs
/// <summary>
/// The GroupPolicy related settings used in PowerShell are as follows in Registry:
2018-01-08 09:54:28 -08:00
/// - Software\Policies\Microsoft\PowerShellCore -- { EnableScripts (0 or 1); ExecutionPolicy (string) }
2018-01-03 12:38:38 -08:00
/// SubKeys Name-Value-Pairs
/// - ScriptBlockLogging { EnableScriptBlockLogging (0 or 1); EnableScriptBlockInvocationLogging (0 or 1) }
/// - ModuleLogging { EnableModuleLogging (0 or 1); ModuleNames (string[]) }
/// - Transcription { EnableTranscripting (0 or 1); OutputDirectory (string); EnableInvocationHeader (0 or 1) }
/// - UpdatableHelp { DefaultSourcePath (string) }
/// - ConsoleSessionConfiguration { EnableConsoleSessionConfiguration (0 or 1); ConsoleSessionConfigurationName (string) }
/// - Software\Policies\Microsoft\Windows\EventLog
/// SubKeys Name-Value-Pairs
/// - ProtectedEventLogging { EnableProtectedEventLogging (0 or 1); EncryptionCertificate (string[]) }
///
/// The JSON representation is in sync with the 'PowerShellPolicies' type. Here is an example:
/// {
/// "PowerShellPolicies": {
/// "ScriptExecution": {
/// "ExecutionPolicy": "RemoteSigned"
/// },
/// "ScriptBlockLogging": {
/// "EnableScriptBlockInvocationLogging": true,
/// "EnableScriptBlockLogging": false
/// },
/// "ProtectedEventLogging": {
/// "EnableProtectedEventLogging": false,
/// "EncryptionCertificate": [
/// "Joe"
/// ]
/// },
/// "Transcription": {
/// "EnableTranscripting": true,
/// "EnableInvocationHeader": true,
2018-01-08 09:54:28 -08:00
/// "OutputDirectory": "c:\\tmp"
2018-01-03 12:38:38 -08:00
/// },
/// "UpdatableHelp": {
2018-01-08 09:54:28 -08:00
/// "DefaultSourcePath": "f:\\temp"
2018-01-03 12:38:38 -08:00
/// },
/// "ConsoleSessionConfiguration": {
/// "EnableConsoleSessionConfiguration": true,
/// "ConsoleSessionConfigurationName": "name"
/// }
/// }
/// }
/// </summary>
internal sealed class PowerShellPolicies
{
public ScriptExecution ScriptExecution { get ; set ; }
2020-11-20 11:11:36 +00:00
2018-01-03 12:38:38 -08:00
public ScriptBlockLogging ScriptBlockLogging { get ; set ; }
2020-11-20 11:11:36 +00:00
2018-01-03 12:38:38 -08:00
public ModuleLogging ModuleLogging { get ; set ; }
2020-11-20 11:11:36 +00:00
2018-01-03 12:38:38 -08:00
public ProtectedEventLogging ProtectedEventLogging { get ; set ; }
2020-11-20 11:11:36 +00:00
2018-01-03 12:38:38 -08:00
public Transcription Transcription { get ; set ; }
2020-11-20 11:11:36 +00:00
2018-01-03 12:38:38 -08:00
public UpdatableHelp UpdatableHelp { get ; set ; }
2020-11-20 11:11:36 +00:00
2018-01-03 12:38:38 -08:00
public ConsoleSessionConfiguration ConsoleSessionConfiguration { get ; set ; }
}
internal abstract class PolicyBase { }
/// <summary>
2019-01-06 04:40:24 +05:00
/// Setting about ScriptExecution.
2018-01-03 12:38:38 -08:00
/// </summary>
internal sealed class ScriptExecution : PolicyBase
{
public string ExecutionPolicy { get ; set ; }
2020-11-20 11:11:36 +00:00
2018-01-03 12:38:38 -08:00
public bool? EnableScripts { get ; set ; }
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Setting about ScriptBlockLogging.
2018-01-03 12:38:38 -08:00
/// </summary>
internal sealed class ScriptBlockLogging : PolicyBase
{
public bool? EnableScriptBlockInvocationLogging { get ; set ; }
2020-11-20 11:11:36 +00:00
2018-01-03 12:38:38 -08:00
public bool? EnableScriptBlockLogging { get ; set ; }
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Setting about ModuleLogging.
2018-01-03 12:38:38 -08:00
/// </summary>
internal sealed class ModuleLogging : PolicyBase
{
public bool? EnableModuleLogging { get ; set ; }
2020-11-20 11:11:36 +00:00
2018-01-03 12:38:38 -08:00
public string [] ModuleNames { get ; set ; }
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Setting about Transcription.
2018-01-03 12:38:38 -08:00
/// </summary>
internal sealed class Transcription : PolicyBase
{
public bool? EnableTranscripting { get ; set ; }
2020-11-20 11:11:36 +00:00
2018-01-03 12:38:38 -08:00
public bool? EnableInvocationHeader { get ; set ; }
2020-11-20 11:11:36 +00:00
2018-01-03 12:38:38 -08:00
public string OutputDirectory { get ; set ; }
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Setting about UpdatableHelp.
2018-01-03 12:38:38 -08:00
/// </summary>
internal sealed class UpdatableHelp : PolicyBase
{
2018-01-05 14:33:37 -08:00
public bool? EnableUpdateHelpDefaultSourcePath { get ; set ; }
2020-11-20 11:11:36 +00:00
2018-01-03 12:38:38 -08:00
public string DefaultSourcePath { get ; set ; }
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Setting about ConsoleSessionConfiguration.
2018-01-03 12:38:38 -08:00
/// </summary>
internal sealed class ConsoleSessionConfiguration : PolicyBase
{
public bool? EnableConsoleSessionConfiguration { get ; set ; }
2020-11-20 11:11:36 +00:00
2018-01-03 12:38:38 -08:00
public string ConsoleSessionConfigurationName { get ; set ; }
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Setting about ProtectedEventLogging.
2018-01-03 12:38:38 -08:00
/// </summary>
internal sealed class ProtectedEventLogging : PolicyBase
{
public bool? EnableProtectedEventLogging { get ; set ; }
2020-11-20 11:11:36 +00:00
2018-01-03 12:38:38 -08:00
public string [] EncryptionCertificate { get ; set ; }
}
#endregion
}