commit 1ca81fc3629cd5625af5afaa34a15ee198ebdf7b parent 27f90003f72825b27ccf1bbff07aaa6a47c469dd Author: JohnMcPMS <johnmcp@microsoft.com> Date: Mon, 7 Oct 2024 14:53:03 -0700 Improve configuration self elevation flow (#4844) ## Change This change adds the ability to use schema 0.3 configuration files and set the module path when using the self-elevating configuration feature. This required implementing the 0.3 serializer as well. It also introduces an error when secure parameters would be passed across an integrity boundary. Diffstat:
34 files changed, 768 insertions(+), 189 deletions(-)
diff --git a/doc/windows/package-manager/winget/returnCodes.md b/doc/windows/package-manager/winget/returnCodes.md @@ -205,6 +205,8 @@ Installation failed. Restart your PC then try again. | | 0x8A15C00F | -1978286065 | WINGET_CONFIG_ERROR_TEST_FAILED | Some of the configuration units failed while testing their state. | | 0x8A15C010 | -1978286064 | WINGET_CONFIG_ERROR_TEST_NOT_RUN | Configuration state was not tested. | | 0x8A15C011 | -1978286063 | WINGET_CONFIG_ERROR_GET_FAILED | The configuration unit failed getting its properties. | +| 0x8A15C012 | -1978286062 | WINGET_CONFIG_ERROR_HISTORY_ITEM_NOT_FOUND | The specified configuration could not be found. | +| 0x8A15C013 | -1978286061 | WINGET_CONFIG_ERROR_PARAMETER_INTEGRITY_BOUNDARY | Parameter cannot be passed across integrity boundary. | ## Configuration Processor Errors diff --git a/src/AppInstallerCLICore/Commands/DebugCommand.cpp b/src/AppInstallerCLICore/Commands/DebugCommand.cpp @@ -102,6 +102,9 @@ namespace AppInstaller::CLI OutputProxyStubInterfaceRegistration<winrt::Windows::Foundation::Collections::IIterable<winrt::Microsoft::Management::Configuration::TestConfigurationUnitResult>>(context); OutputProxyStubInterfaceRegistration<winrt::Windows::Foundation::Collections::IIterable<winrt::Microsoft::Management::Configuration::IApplyGroupMemberSettingsResult>>(context); OutputProxyStubInterfaceRegistration<winrt::Windows::Foundation::Collections::IIterable<winrt::Microsoft::Management::Configuration::ITestSettingsResult>>(context); + OutputProxyStubInterfaceRegistration<winrt::Microsoft::Management::Configuration::IConfigurationUnitProcessorDetails2>(context); + OutputProxyStubInterfaceRegistration<winrt::Microsoft::Management::Configuration::IGetAllSettingsConfigurationUnitProcessor>(context); + OutputProxyStubInterfaceRegistration<winrt::Microsoft::Management::Configuration::IConfigurationStatics2>(context); // TODO: Fix the layering inversion created by the COM deployment API (probably in order to operate winget.exe against the COM server). // Then this code can just have a CppWinRT reference to the deployment API and spit out the interface registrations just like for configuration. @@ -127,6 +130,7 @@ namespace AppInstaller::CLI void DumpInterestingIIDsCommand::ExecuteInternal(Execution::Context& context) const { OutputIIDMapping<winrt::Microsoft::Management::Configuration::IConfigurationStatics>(context); + OutputIIDMapping<winrt::Microsoft::Management::Configuration::IConfigurationStatics2>(context); } Resource::LocString DumpErrorResourceCommand::ShortDescription() const diff --git a/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp b/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp @@ -2,9 +2,12 @@ // Licensed under the MIT License. #include "pch.h" #include "Public/ConfigurationSetProcessorFactoryRemoting.h" +#include <AppInstallerErrors.h> +#include <AppInstallerLanguageUtilities.h> #include <AppInstallerStrings.h> #include <winget/ILifetimeWatcher.h> #include <winget/Security.h> +#include <winrt/Microsoft.Management.Configuration.SetProcessorFactory.h> using namespace winrt::Windows::Foundation; using namespace winrt::Microsoft::Management::Configuration; @@ -40,7 +43,7 @@ namespace AppInstaller::CLI::ConfigurationRemoting // have this implementation leverage that one with an event handler for the packaged specifics. // TODO: Add SetProcessorFactory::IPwshConfigurationSetProcessorFactoryProperties and pass values along to sets on creation // In turn, any properties must only be set via the command line (or eventual UI requests to the user). - struct DynamicFactory : winrt::implements<DynamicFactory, IConfigurationSetProcessorFactory, winrt::cloaked<WinRT::ILifetimeWatcher>>, WinRT::LifetimeWatcherBase + struct DynamicFactory : winrt::implements<DynamicFactory, IConfigurationSetProcessorFactory, SetProcessorFactory::IPwshConfigurationSetProcessorFactoryProperties, winrt::cloaked<WinRT::ILifetimeWatcher>>, WinRT::LifetimeWatcherBase { DynamicFactory(); @@ -58,12 +61,58 @@ namespace AppInstaller::CLI::ConfigurationRemoting void SendDiagnostics(const IDiagnosticInformation& information); + Collections::IVectorView<winrt::hstring> AdditionalModulePaths() const + { + THROW_HR(E_NOTIMPL); + } + + void AdditionalModulePaths(const Collections::IVectorView<winrt::hstring>&) + { + THROW_HR(E_NOTIMPL); + } + + SetProcessorFactory::PwshConfigurationProcessorPolicy Policy() const + { + THROW_HR(E_NOTIMPL); + } + + void Policy(SetProcessorFactory::PwshConfigurationProcessorPolicy) + { + THROW_HR(E_NOTIMPL); + } + + SetProcessorFactory::PwshConfigurationProcessorLocation Location() const + { + return m_location; + } + + void Location(SetProcessorFactory::PwshConfigurationProcessorLocation value) + { + auto pwshFactory = m_defaultRemoteFactory.as<SetProcessorFactory::IPwshConfigurationSetProcessorFactoryProperties>(); + pwshFactory.Location(value); + m_location = value; + } + + winrt::hstring CustomLocation() const + { + return m_customLocation; + } + + void CustomLocation(winrt::hstring value) + { + auto pwshFactory = m_defaultRemoteFactory.as<SetProcessorFactory::IPwshConfigurationSetProcessorFactoryProperties>(); + pwshFactory.CustomLocation(value); + m_customLocation = value; + } + private: IConfigurationSetProcessorFactory m_defaultRemoteFactory; winrt::event<EventHandler<IDiagnosticInformation>> m_diagnostics; IConfigurationSetProcessorFactory::Diagnostics_revoker m_factoryDiagnosticsEventRevoker; std::mutex m_diagnosticsMutex; DiagnosticLevel m_minimumLevel = DiagnosticLevel::Informational; + SetProcessorFactory::PwshConfigurationProcessorLocation m_location = SetProcessorFactory::PwshConfigurationProcessorLocation::Default; + winrt::hstring m_customLocation; }; struct DynamicProcessorInfo @@ -90,6 +139,33 @@ namespace AppInstaller::CLI::ConfigurationRemoting m_currentIntegrityLevel = Security::GetEffectiveIntegrityLevel(); #endif + // Check for multiple integrity level requirements + bool multipleIntegrityLevels = false; + bool higherIntegrityLevelsThanCurrent = false; + for (const auto& existingUnit : m_configurationSet.Units()) + { + auto integrityLevel = GetIntegrityLevelForUnit(existingUnit); + if (integrityLevel != m_currentIntegrityLevel) + { + multipleIntegrityLevels = true; + + if (ToIntegral(m_currentIntegrityLevel) < ToIntegral(integrityLevel)) + { + higherIntegrityLevelsThanCurrent = true; + break; + } + } + } + + // Prevent supplied parameters from crossing integrity levels + for (const auto& parameter : m_configurationSet.Parameters()) + { + if (parameter.ProvidedValue() != nullptr) + { + THROW_HR_IF(WINGET_CONFIG_ERROR_PARAMETER_INTEGRITY_BOUNDARY, higherIntegrityLevelsThanCurrent || (multipleIntegrityLevels && parameter.IsSecure())); + } + } + m_setProcessors.emplace(m_currentIntegrityLevel, DynamicProcessorInfo{ m_dynamicFactory->DefaultFactory(), defaultRemoteSetProcessor}); } @@ -194,7 +270,30 @@ namespace AppInstaller::CLI::ConfigurationRemoting std::string SerializeSetProperties() { Json::Value json{ Json::ValueType::objectValue }; + json["path"] = winrt::to_string(m_configurationSet.Path()); + + std::string locationString; + switch (m_dynamicFactory->Location()) + { + case SetProcessorFactory::PwshConfigurationProcessorLocation::AllUsers: + locationString = "AllUsers"; + break; + case SetProcessorFactory::PwshConfigurationProcessorLocation::CurrentUser: + locationString = "CurrentUser"; + break; + case SetProcessorFactory::PwshConfigurationProcessorLocation::Custom: + locationString = Utility::ConvertToUTF8(m_dynamicFactory->CustomLocation()); + break; + case SetProcessorFactory::PwshConfigurationProcessorLocation::Default: + break; + } + + if (!locationString.empty()) + { + json["modulePath"] = locationString; + } + Json::StreamWriterBuilder writerBuilder; writerBuilder.settings_["indentation"] = "\t"; return Json::writeString(writerBuilder, json); @@ -207,9 +306,10 @@ namespace AppInstaller::CLI::ConfigurationRemoting std::string SerializeHighIntegrityLevelSet() { ConfigurationSet highIntegritySet; - - // TODO: Currently we only support schema version 0.2 for handling elevated integrity levels. - highIntegritySet.SchemaVersion(L"0.2"); + highIntegritySet.SchemaVersion(m_configurationSet.SchemaVersion()); + highIntegritySet.Metadata(m_configurationSet.Metadata()); + highIntegritySet.Parameters(m_configurationSet.Parameters()); + highIntegritySet.Variables(m_configurationSet.Variables()); std::vector<ConfigurationUnit> highIntegrityUnits; auto units = m_configurationSet.Units(); diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -105,14 +105,13 @@ namespace AppInstaller::CLI::Workflow if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::ConfigureSelfElevation) && !Runtime::IsRunningAsAdmin()) { factory = ConfigurationRemoting::CreateDynamicRuntimeFactory(); - // TODO: Implement SetProcessorFactory::IPwshConfigurationSetProcessorFactoryProperties on dynamic factory } else { factory = ConfigurationRemoting::CreateOutOfProcessFactory(); - Configuration::SetModulePath(context, factory); } + Configuration::SetModulePath(context, factory); return factory; } diff --git a/src/AppInstallerCLIE2ETests/ConfigureCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureCommand.cs @@ -23,6 +23,8 @@ namespace AppInstallerCLIE2ETests [OneTimeSetUp] public void OneTimeSetup() { + WinGetSettingsHelper.ConfigureFeature("configuration03", true); + WinGetSettingsHelper.ConfigureFeature("configureSelfElevate", true); this.DeleteTxtFiles(); } @@ -32,6 +34,8 @@ namespace AppInstallerCLIE2ETests [OneTimeTearDown] public void OneTimeTeardown() { + WinGetSettingsHelper.ConfigureFeature("configuration03", false); + WinGetSettingsHelper.ConfigureFeature("configureSelfElevate", false); this.DeleteTxtFiles(); } @@ -207,6 +211,24 @@ namespace AppInstallerCLIE2ETests Assert.AreEqual("Contents!", File.ReadAllText(targetFilePath)); } + /// <summary> + /// Specifies the module path to an "elevated" server. + /// </summary> + [Test] + public void SpecifyModulePathToHighIntegrityServer() + { + string configFile = TestCommon.GetTestDataFile("Configuration\\GetPSModulePath.yml"); + string testDirectory = TestCommon.GetRandomTestDir(); + + var result = TestCommon.RunAICLICommand(CommandAndAgreementsAndVerbose, $"{configFile} --module-path \"{testDirectory}\""); + Assert.AreEqual(0, result.ExitCode); + + string testFile = Path.Join(TestCommon.GetTestDataFile("Configuration"), "PSModulePath.txt"); + Assert.True(File.Exists(testFile)); + string testFileContents = File.ReadAllText(testFile); + Assert.True(testFileContents.StartsWith(testDirectory)); + } + private void DeleteTxtFiles() { // Delete all .txt files in the test directory; they are placed there by the tests diff --git a/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs @@ -81,6 +81,8 @@ namespace AppInstallerCLIE2ETests [Test] public void ShowDetails_Schema0_3_Fails() { + WinGetSettingsHelper.ConfigureFeature("configuration03", false); + var result = TestCommon.RunAICLICommand("configure show", TestCommon.GetTestDataFile("Configuration\\ShowDetails_TestRepo_0_3.yml")); Assert.AreEqual(Constants.ErrorCode.ERROR_EXPERIMENTAL_FEATURE_DISABLED, result.ExitCode); } diff --git a/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs @@ -52,6 +52,10 @@ namespace AppInstallerCLIE2ETests.Helpers var settingsJson = new Hashtable() { { + "$schema", + "https://aka.ms/winget-settings.schema.json" + }, + { "experimentalFeatures", experimentalFeatures }, diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/GetPSModulePath.yml b/src/AppInstallerCLIE2ETests/TestData/Configuration/GetPSModulePath.yml @@ -0,0 +1,11 @@ +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json +metadata: + 1e62d683-2999-44e7-81f7-6f8f35e8d731: true +resources: + - name: Name1 + type: xE2ETestResource/E2ETestResourcePSModulePath + metadata: + repository: AppInstallerCLIE2ETestsRepo + securityContext: elevated + properties: + outputPath: ${WinGetConfigRoot}\PSModulePath.txt diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2ETestResource/xE2ETestResource.psd1 b/src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2ETestResource/xE2ETestResource.psd1 @@ -23,6 +23,7 @@ DscResourcesToExport = @( 'E2ETestResourceTypes' 'E2ETestResourceCrash' 'E2ETestResourcePID' + 'E2ETestResourcePSModulePath' ) HelpInfoURI = 'https://www.contoso.com/help' diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2ETestResource/xE2ETestResource.psm1 b/src/AppInstallerCLIE2ETests/TestData/Configuration/Modules/xE2ETestResource/xE2ETestResource.psm1 @@ -289,7 +289,7 @@ class E2ETestResourceCrash } } -# This resource writes the current PID to the provided file path. +# This resource writes the current PID to the provided file path. [DscResource()] class E2ETestResourcePID { @@ -324,3 +324,34 @@ class E2ETestResourcePID } } } + +# This resource writes the current PSModulePath to the provided file path. +[DscResource()] +class E2ETestResourcePSModulePath +{ + [DscProperty(Key)] + [string] $key + + [DscProperty(Mandatory)] + [string] $outputPath + + [E2ETestResourcePSModulePath] Get() + { + $result = @{ + key = "E2ETestResourcePSModulePath" + outputPath = $this.outputPath + } + + return $result + } + + [bool] Test() + { + return $false + } + + [void] Set() + { + Set-Content -Path $this.outputPath -Value $env:PSModulePath -Force + } +} diff --git a/src/AppInstallerCLIPackage/Package.appxmanifest b/src/AppInstallerCLIPackage/Package.appxmanifest @@ -109,6 +109,9 @@ <Interface Name="Windows.Foundation.Collections.IIterable`1<Microsoft.Management.Configuration.TestConfigurationUnitResult>" InterfaceId="73848262-86D4-5FFC-8353-8408C4E649DE" /> <Interface Name="Windows.Foundation.Collections.IIterable`1<Microsoft.Management.Configuration.IApplyGroupMemberSettingsResult>" InterfaceId="5086070C-F468-5B00-8352-50FB420BA8B0" /> <Interface Name="Windows.Foundation.Collections.IIterable`1<Microsoft.Management.Configuration.ITestSettingsResult>" InterfaceId="2D28E6AA-7036-5D78-9B58-9456F1E332FE" /> + <Interface Name="Microsoft.Management.Configuration.IConfigurationUnitProcessorDetails2" InterfaceId="E89623ED-76E2-5145-B920-D09659554E35" /> + <Interface Name="Microsoft.Management.Configuration.IGetAllSettingsConfigurationUnitProcessor" InterfaceId="72EB8304-D8D3-57D4-9940-7C1C4AD8C40C" /> + <Interface Name="Microsoft.Management.Configuration.IConfigurationStatics2" InterfaceId="540BE073-F2EF-5375-83AA-8E23086B0669" /> </ProxyStub> </Extension> <!-- This entry forces the package registration to process the windows.activatableClass.proxyStub extension above. --> diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3130,4 +3130,7 @@ Please specify one of them using the --source option to proceed.</value> <data name="APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED_FORBIDDEN" xml:space="preserve"> <value>Failed to retrieve Microsoft Store package license. The Microsoft Entra Id account does not have required privilege.</value> </data> -</root> + <data name="WINGET_CONFIG_ERROR_PARAMETER_INTEGRITY_BOUNDARY" xml:space="preserve"> + <value>Parameter cannot be passed across integrity boundary.</value> + </data> +</root>+ \ No newline at end of file diff --git a/src/AppInstallerSharedLib/Errors.cpp b/src/AppInstallerSharedLib/Errors.cpp @@ -271,6 +271,7 @@ namespace AppInstaller WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_TEST_NOT_RUN, "Configuration state was not tested."), WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_GET_FAILED, "The configuration unit failed getting its properties."), WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_HISTORY_ITEM_NOT_FOUND, "The specified configuration could not be found."), + WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_PARAMETER_INTEGRITY_BOUNDARY, "Parameter cannot be passed across integrity boundary."), // Configuration Processor Errors WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_UNIT_NOT_INSTALLED, "The configuration unit was not installed."), diff --git a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h @@ -206,6 +206,7 @@ #define WINGET_CONFIG_ERROR_TEST_NOT_RUN ((HRESULT)0x8A15C010) #define WINGET_CONFIG_ERROR_GET_FAILED ((HRESULT)0x8A15C011) #define WINGET_CONFIG_ERROR_HISTORY_ITEM_NOT_FOUND ((HRESULT)0x8A15C012) +#define WINGET_CONFIG_ERROR_PARAMETER_INTEGRITY_BOUNDARY ((HRESULT)0x8A15C013) // Configuration Processor Errors #define WINGET_CONFIG_ERROR_UNIT_NOT_INSTALLED ((HRESULT)0x8A15C101) diff --git a/src/ConfigurationRemotingServer/Program.cs b/src/ConfigurationRemotingServer/Program.cs @@ -147,6 +147,20 @@ namespace ConfigurationRemotingServer if (metadataJson != null) { limitationSet.Path = metadataJson.Path; + + if (metadataJson.ModulePath != null) + { + PowerShellConfigurationProcessorLocation parsedLocation = PowerShellConfigurationProcessorLocation.Default; + if (Enum.TryParse<PowerShellConfigurationProcessorLocation>(metadataJson.ModulePath, out parsedLocation)) + { + factory.Location = parsedLocation; + } + else + { + factory.Location = PowerShellConfigurationProcessorLocation.Custom; + factory.CustomLocation = metadataJson.ModulePath; + } + } } // Set the limitation set in factory. @@ -168,6 +182,9 @@ namespace ConfigurationRemotingServer { [JsonPropertyName("path")] public string Path { get; set; } = string.Empty; + + [JsonPropertyName("modulePath")] + public string? ModulePath { get; set; } = null; } private static string GetExternalModulesPath() diff --git a/src/Microsoft.Management.Configuration.UnitTests/Fixtures/UnitTestFixture.cs b/src/Microsoft.Management.Configuration.UnitTests/Fixtures/UnitTestFixture.cs @@ -15,6 +15,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Fixtures using Microsoft.Management.Configuration.Processor.ProcessorEnvironments; using Microsoft.Management.Configuration.Processor.Runspaces; using Moq; + using WinRT; using Xunit.Abstractions; using static Microsoft.Management.Configuration.Processor.Constants.PowerShellConstants; @@ -66,7 +67,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Fixtures throw new DirectoryNotFoundException(this.ExternalModulesPath); } - this.ConfigurationStatics = new ConfigurationStaticFunctions(); + this.ConfigurationStatics = new ConfigurationStaticFunctions().As<IConfigurationStatics2>(); } /// <summary> @@ -92,7 +93,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Fixtures /// <summary> /// Gets the configuration statics object to use. /// </summary> - public IConfigurationStatics ConfigurationStatics { get; private init; } + public IConfigurationStatics2 ConfigurationStatics { get; private init; } /// <summary> /// Creates a runspace adding the test module path. diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/ConfigurationProcessorTestBase.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/ConfigurationProcessorTestBase.cs @@ -111,6 +111,15 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers } /// <summary> + /// Creates a configuration parameter via the configuration statics object. + /// </summary> + /// <returns>A new configuration parameter.</returns> + protected ConfigurationParameter ConfigurationParameter() + { + return this.Fixture.ConfigurationStatics.CreateConfigurationParameter(); + } + + /// <summary> /// Creates a configuration set via the configuration statics object. /// </summary> /// <returns>A new configuration set.</returns> diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/Errors.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/Errors.cs @@ -46,6 +46,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers public static readonly int WINGET_CONFIG_ERROR_UNIT_SETTING_CONFIG_ROOT = unchecked((int)0x8A15C110); public static readonly int WINGET_CONFIG_ERROR_UNIT_IMPORT_MODULE_ADMIN = unchecked((int)0x8A15C111); public static readonly int WINGET_CONFIG_ERROR_NOT_SUPPORTED_BY_PROCESSOR = unchecked((int)0x8A15C112); + public static readonly int WINGET_CONFIG_ERROR_PARAMETER_INTEGRITY_BOUNDARY = unchecked((int)0x8A15C013); // Limitation Set Errors public static readonly int CORE_INVALID_OPERATION = unchecked((int)0x80131509); diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationMixedElevationTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationMixedElevationTests.cs @@ -51,6 +51,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Directory.CreateDirectory(tempDirectory); ConfigurationSet configurationSet = this.ConfigurationSet(); + configurationSet.SchemaVersion = "0.2"; configurationSet.Metadata.Add(Helpers.Constants.EnableDynamicFactoryTestMode, true); ConfigurationUnit unit = this.ConfigurationUnit(); @@ -112,6 +113,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Version version = new Version("0.0.0.1"); ConfigurationSet configurationSet = this.ConfigurationSet(); + configurationSet.SchemaVersion = "0.2"; configurationSet.Metadata.Add(Helpers.Constants.EnableDynamicFactoryTestMode, true); configurationSet.Metadata.Add(Helpers.Constants.ForceHighIntegrityLevelUnitsTestGuid, true); configurationSet.Metadata.Add(Helpers.Constants.EnableRestrictedIntegrityLevelTestGuid, true); @@ -159,5 +161,49 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.Null(elevatedUnitResult.ResultInformation.ResultCode); Assert.Equal(ConfigurationUnitResultSource.None, elevatedUnitResult.ResultInformation.ResultSource); } + + /// <summary> + /// Verifies that attempting to pass a secure parameter across the integrity boundary fails. + /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> + [Fact] + public async Task SecureParameterAcrossIntegrityBoundaryFails() + { + string resourceName = "E2ETestResourcePID"; + string moduleName = "xE2ETestResource"; + Version version = new Version("0.0.0.1"); + + string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDirectory); + + ConfigurationSet configurationSet = this.ConfigurationSet(); + configurationSet.Metadata.Add(Helpers.Constants.EnableDynamicFactoryTestMode, true); + + ConfigurationUnit elevatedUnit = this.ConfigurationUnit(); + elevatedUnit.Metadata.Add("version", version.ToString()); + elevatedUnit.Metadata.Add("module", moduleName); + elevatedUnit.Metadata.Add("securityContext", "elevated"); + elevatedUnit.Settings.Add("directoryPath", tempDirectory); + elevatedUnit.Type = resourceName; + elevatedUnit.Intent = ConfigurationUnitIntent.Apply; + + configurationSet.Units = new ConfigurationUnit[] { elevatedUnit }; + + ConfigurationParameter parameter = this.ConfigurationParameter(); + parameter.Name = "param"; + parameter.Type = Windows.Foundation.PropertyType.String; + parameter.IsSecure = true; + parameter.ProvidedValue = "secrets"; + + configurationSet.Parameters = new ConfigurationParameter[] { parameter }; + + IConfigurationSetProcessorFactory dynamicFactory = await this.fixture.ConfigurationStatics.CreateConfigurationSetProcessorFactoryAsync(Helpers.Constants.DynamicRuntimeHandlerIdentifier); + + ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(dynamicFactory); + + // While parameters are not supported, we expect to get a not implemented exception. + // Once they are implemented, swap to the appropriate error mechanism for the parameter integrity boundary. + Assert.Throws<NotImplementedException>(() => processor.ApplySet(configurationSet, ApplyConfigurationSetFlags.None)); + } } } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs @@ -8,13 +8,10 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests { using System; using System.Collections.Generic; - using System.Linq; - using System.Runtime.InteropServices; using Microsoft.Management.Configuration.Processor.Extensions; using Microsoft.Management.Configuration.UnitTests.Fixtures; using Microsoft.Management.Configuration.UnitTests.Helpers; using Microsoft.VisualBasic; - using Newtonsoft.Json.Linq; using Windows.Foundation.Collections; using Windows.Storage.Streams; using WinRT; @@ -545,6 +542,93 @@ properties: } /// <summary> + /// Verifies that the configuration set (0.3) can be serialized and reopened correctly. + /// </summary> + [Fact] + public void TestSet_Serialize_0_3() + { + ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(); + + OpenConfigurationSetResult openResult = processor.OpenConfigurationSet(this.CreateStream(@" +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json +metadata: + description: FakeSetDescription +variables: + var1: Test1 + var2: 42 +parameters: + param1: + type: securestring + param2: + type: int + defaultValue: 89 +resources: + - type: FakeModule/FakeResource + name: TestId + metadata: + description: FakeDescription + allowPrerelease: true + securityContext: elevated + properties: + TestString: Hello + TestBool: false + TestInt: 1234 + - type: FakeModule2/FakeResource2 + name: TestId2 + dependsOn: + - TestId + - dependency2 + - dependency3 + metadata: + description: FakeDescription2 + securityContext: elevated + properties: + TestString: Bye + TestBool: true + TestInt: 4321 + Mapping: + Key: TestValue +")); + + // Serialize set. + ConfigurationSet configurationSet = openResult.Set; + InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream(); + configurationSet.Serialize(stream); + + string yamlOutput = this.ReadStream(stream); + + // Reopen configuration set from serialized string and verify values. + OpenConfigurationSetResult serializedSetResult = processor.OpenConfigurationSet(this.CreateStream(yamlOutput)); + Assert.Null(serializedSetResult.ResultCode); + ConfigurationSet set = serializedSetResult.Set; + Assert.NotNull(set); + + Assert.Equal("0.3", set.SchemaVersion); + Assert.Equal(2, set.Units.Count); + + this.VerifyValueSet(set.Metadata, new KeyValuePair<string, object>("description", "FakeSetDescription")); + this.VerifyValueSet(set.Variables, new("var1", "Test1"), new("var2", 42)); + + Assert.Equal(2, set.Parameters.Count); + this.VerifyParameter(set.Parameters[0], "param1", Windows.Foundation.PropertyType.String, true); + this.VerifyParameter(set.Parameters[1], "param2", Windows.Foundation.PropertyType.Int64, false, 89); + + Assert.Equal("FakeModule/FakeResource", set.Units[0].Type); + Assert.Equal("TestId", set.Units[0].Identifier); + this.VerifyValueSet(set.Units[0].Metadata, new("description", "FakeDescription"), new("allowPrerelease", true), new("securityContext", "elevated")); + this.VerifyValueSet(set.Units[0].Settings, new("TestString", "Hello"), new("TestBool", false), new("TestInt", 1234)); + + Assert.Equal("FakeModule2/FakeResource2", set.Units[1].Type); + Assert.Equal("TestId2", set.Units[1].Identifier); + this.VerifyStringArray(set.Units[1].Dependencies, "TestId", "dependency2", "dependency3"); + this.VerifyValueSet(set.Units[1].Metadata, new("description", "FakeDescription2"), new("securityContext", "elevated")); + + ValueSet mapping = new ValueSet(); + mapping.Add("Key", "TestValue"); + this.VerifyValueSet(set.Units[1].Settings, new("TestString", "Bye"), new("TestBool", true), new("TestInt", 4321), new("Mapping", mapping)); + } + + /// <summary> /// Test for using version 0.3 schema. /// </summary> [Fact] @@ -716,21 +800,8 @@ parameters: Assert.Equal(expectedType, parameters[0].Type); Assert.Equal(secure, parameters[0].IsSecure); - switch (expectedValue ?? throw new ArgumentException("expectedValue")) - { - case int i: - Assert.Equal(i, (int)(long)parameters[0].DefaultValue); - break; - case string s: - Assert.Equal(s, (string)parameters[0].DefaultValue); - break; - case bool b: - Assert.Equal(b, (bool)parameters[0].DefaultValue); - break; - default: - Assert.Fail($"Add expected type `{expectedValue.GetType().Name}` to switch statement."); - break; - } + Assert.NotNull(expectedValue); + this.VerifyObject(expectedValue, parameters[0].DefaultValue); } else { @@ -761,31 +832,49 @@ parameters: Assert.True(values.ContainsKey(expectation.Key), $"Not Found {expectation.Key}"); object value = values[expectation.Key]; - switch (expectation.Value) + this.VerifyObject(expectation.Value, value); + } + } + + private void VerifyStringArray(IList<string> strings, params string[] expected) + { + Assert.NotNull(strings); + Assert.Equal(expected.Length, strings.Count); + } + + private void VerifyParameter(ConfigurationParameter parameter, string name, Windows.Foundation.PropertyType type, bool secure, object? defaultValue = null) + { + Assert.Equal(name, parameter.Name); + Assert.Equal(type, parameter.Type); + Assert.Equal(secure, parameter.IsSecure); + this.VerifyObject(defaultValue, parameter.DefaultValue); + } + + private void VerifyObject(object? expectedValue, object? actualValue) + { + if (expectedValue != null) + { + Assert.NotNull(actualValue); + + switch (expectedValue) { case int i: - Assert.Equal(i, (int)(long)value); + Assert.Equal(i, (int)(long)actualValue); break; case string s: - Assert.Equal(s, (string)value); + Assert.Equal(s, (string)actualValue); break; case bool b: - Assert.Equal(b, (bool)value); + Assert.Equal(b, (bool)actualValue); break; case ValueSet v: - Assert.True(v.ContentEquals(value.As<ValueSet>())); + Assert.True(v.ContentEquals(actualValue.As<ValueSet>())); break; default: - Assert.Fail($"Add expected type `{expectation.Value.GetType().Name}` to switch statement."); + Assert.Fail($"Add expected type `{expectedValue.GetType().Name}` to switch statement."); break; } } } - - private void VerifyStringArray(IList<string> strings, params string[] expected) - { - Assert.NotNull(strings); - Assert.Equal(expected.Length, strings.Count); - } } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationParameter.cpp b/src/Microsoft.Management.Configuration/ConfigurationParameter.cpp @@ -7,7 +7,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation { - hstring ConfigurationParameter::Name() + hstring ConfigurationParameter::Name() const { return m_name; } @@ -17,7 +17,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation m_name = value; } - hstring ConfigurationParameter::Description() + hstring ConfigurationParameter::Description() const { return m_description; } @@ -27,7 +27,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation m_description = value; } - Windows::Foundation::Collections::ValueSet ConfigurationParameter::Metadata() + Windows::Foundation::Collections::ValueSet ConfigurationParameter::Metadata() const { return m_metadata; } @@ -38,7 +38,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation m_metadata = value; } - bool ConfigurationParameter::IsSecure() + bool ConfigurationParameter::IsSecure() const { return m_isSecure; } @@ -48,7 +48,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation m_isSecure = value; } - Windows::Foundation::PropertyType ConfigurationParameter::Type() + Windows::Foundation::PropertyType ConfigurationParameter::Type() const { return m_type; } @@ -59,7 +59,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation m_type = value; } - Windows::Foundation::IInspectable ConfigurationParameter::DefaultValue() + Windows::Foundation::IInspectable ConfigurationParameter::DefaultValue() const { return m_defaultValue; } @@ -74,7 +74,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation m_defaultValue = value; } - Windows::Foundation::Collections::IVector<Windows::Foundation::IInspectable> ConfigurationParameter::AllowedValues() + Windows::Foundation::Collections::IVector<Windows::Foundation::IInspectable> ConfigurationParameter::AllowedValues() const { return m_allowedValues; } @@ -100,7 +100,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation m_allowedValues = winrt::multi_threaded_vector<Windows::Foundation::IInspectable>(std::move(value)); } - uint32_t ConfigurationParameter::MinimumLength() + uint32_t ConfigurationParameter::MinimumLength() const { return m_minimumLength; } @@ -111,7 +111,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation m_minimumLength = value; } - uint32_t ConfigurationParameter::MaximumLength() + uint32_t ConfigurationParameter::MaximumLength() const { return m_maximumLength; } @@ -122,7 +122,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation m_maximumLength = value; } - Windows::Foundation::IInspectable ConfigurationParameter::MinimumValue() + Windows::Foundation::IInspectable ConfigurationParameter::MinimumValue() const { return m_minimumValue; } @@ -138,7 +138,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation m_minimumValue = value; } - Windows::Foundation::IInspectable ConfigurationParameter::MaximumValue() + Windows::Foundation::IInspectable ConfigurationParameter::MaximumValue() const { return m_maximumValue; } @@ -154,7 +154,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation m_maximumValue = value; } - Windows::Foundation::IInspectable ConfigurationParameter::ProvidedValue() + Windows::Foundation::IInspectable ConfigurationParameter::ProvidedValue() const { return m_providedValue; } diff --git a/src/Microsoft.Management.Configuration/ConfigurationParameter.h b/src/Microsoft.Management.Configuration/ConfigurationParameter.h @@ -13,40 +13,40 @@ namespace winrt::Microsoft::Management::Configuration::implementation { ConfigurationParameter() = default; - hstring Name(); + hstring Name() const; void Name(hstring const& value); - hstring Description(); + hstring Description() const; void Description(hstring const& value); - Windows::Foundation::Collections::ValueSet Metadata(); + Windows::Foundation::Collections::ValueSet Metadata() const; void Metadata(const Windows::Foundation::Collections::ValueSet& value); - bool IsSecure(); + bool IsSecure() const; void IsSecure(bool value); - Windows::Foundation::PropertyType Type(); + Windows::Foundation::PropertyType Type() const; void Type(Windows::Foundation::PropertyType value); - Windows::Foundation::IInspectable DefaultValue(); + Windows::Foundation::IInspectable DefaultValue() const; void DefaultValue(Windows::Foundation::IInspectable const& value); - Windows::Foundation::Collections::IVector<Windows::Foundation::IInspectable> AllowedValues(); + Windows::Foundation::Collections::IVector<Windows::Foundation::IInspectable> AllowedValues() const; void AllowedValues(Windows::Foundation::Collections::IVector<Windows::Foundation::IInspectable> const& value); - uint32_t MinimumLength(); + uint32_t MinimumLength() const; void MinimumLength(uint32_t value); - uint32_t MaximumLength(); + uint32_t MaximumLength() const; void MaximumLength(uint32_t value); - Windows::Foundation::IInspectable MinimumValue(); + Windows::Foundation::IInspectable MinimumValue() const; void MinimumValue(Windows::Foundation::IInspectable const& value); - Windows::Foundation::IInspectable MaximumValue(); + Windows::Foundation::IInspectable MaximumValue() const; void MaximumValue(Windows::Foundation::IInspectable const& value); - Windows::Foundation::IInspectable ProvidedValue(); + Windows::Foundation::IInspectable ProvidedValue() const; void ProvidedValue(Windows::Foundation::IInspectable const& value); HRESULT STDMETHODCALLTYPE SetLifetimeWatcher(IUnknown* watcher); diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.cpp @@ -106,43 +106,17 @@ namespace winrt::Microsoft::Management::Configuration::implementation { const Node& typeNode = CHECK_ERROR(GetAndEnsureField(node, ConfigurationField::Type, true, Node::Type::Scalar)); std::string typeValue = typeNode.as<std::string>(); + auto parsedType = ParseWindowsFoundationPropertyType(typeValue); - if (typeValue == "string") + if (parsedType) { - parameter->Type(Windows::Foundation::PropertyType::String); - } - else if (typeValue == "securestring") - { - parameter->Type(Windows::Foundation::PropertyType::String); - parameter->IsSecure(true); - } - else if (typeValue == "int") - { - parameter->Type(Windows::Foundation::PropertyType::Int64); - } - else if (typeValue == "bool") - { - parameter->Type(Windows::Foundation::PropertyType::Boolean); - } - else if (typeValue == "object") - { - parameter->Type(Windows::Foundation::PropertyType::Inspectable); - } - else if (typeValue == "secureobject") - { - parameter->Type(Windows::Foundation::PropertyType::Inspectable); - parameter->IsSecure(true); - } - else if (typeValue == "array") - { - parameter->Type(Windows::Foundation::PropertyType::InspectableArray); + parameter->Type(parsedType->first); + parameter->IsSecure(parsedType->second); } else { FIELD_VALUE_ERROR(GetConfigurationFieldName(ConfigurationField::Type), typeValue, typeNode.Mark()); } - - // TODO: Consider supporting an expanded set of type strings } void ConfigurationSetParser_0_3::GetStringValueForParameter( @@ -253,4 +227,60 @@ namespace winrt::Microsoft::Management::Configuration::implementation return false; } + + std::optional<std::pair<Windows::Foundation::PropertyType, bool>> ParseWindowsFoundationPropertyType(std::string_view value) + { + if (value == "string") + { + return std::make_pair(Windows::Foundation::PropertyType::String, false); + } + else if (value == "securestring") + { + return std::make_pair(Windows::Foundation::PropertyType::String, true); + } + else if (value == "int") + { + return std::make_pair(Windows::Foundation::PropertyType::Int64, false); + } + else if (value == "bool") + { + return std::make_pair(Windows::Foundation::PropertyType::Boolean, false); + } + else if (value == "object") + { + return std::make_pair(Windows::Foundation::PropertyType::Inspectable, false); + } + else if (value == "secureobject") + { + return std::make_pair(Windows::Foundation::PropertyType::Inspectable, true); + } + else if (value == "array") + { + return std::make_pair(Windows::Foundation::PropertyType::InspectableArray, false); + } + + // TODO: Consider supporting an expanded set of type strings + return std::nullopt; + } + + std::string_view ToString(Windows::Foundation::PropertyType value, bool isSecure) + { + switch (value) + { + case Windows::Foundation::PropertyType::Int16: + case Windows::Foundation::PropertyType::Int32: + case Windows::Foundation::PropertyType::Int64: + return "int"sv; + case Windows::Foundation::PropertyType::Boolean: + return "bool"sv; + case Windows::Foundation::PropertyType::String: + return isSecure ? "securestring"sv : "string"sv; + case Windows::Foundation::PropertyType::Inspectable: + return isSecure ? "secureobject"sv : "object"sv; + case Windows::Foundation::PropertyType::InspectableArray: + return "array"sv; + default: + return {}; + } + } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.h b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.h @@ -5,6 +5,8 @@ #include <ConfigurationParameter.h> #include <winget/Yaml.h> +#include <optional> +#include <utility> namespace winrt::Microsoft::Management::Configuration::implementation { @@ -57,4 +59,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation AppInstaller::YAML::Node m_document; }; + + std::optional<std::pair<Windows::Foundation::PropertyType, bool>> ParseWindowsFoundationPropertyType(std::string_view value); + std::string_view ToString(Windows::Foundation::PropertyType value, bool isSecure); } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp @@ -8,8 +8,10 @@ #include "ConfigurationSetSerializer.h" #include "ConfigurationSetSerializer_0_2.h" +#include "ConfigurationSetSerializer_0_3.h" #include "ConfigurationSetUtilities.h" +using namespace AppInstaller::Utility; using namespace AppInstaller::YAML; using namespace winrt::Windows::Foundation; @@ -20,23 +22,19 @@ namespace winrt::Microsoft::Management::Configuration::implementation static constexpr std::string_view s_nullValue = "null"; } - // The `forHistory` parameter is temporary until the other serializers are implemented. - // It is only applicable as long as the serializers that are not implemented do not have differences in the value set or string array serialization. - std::unique_ptr<ConfigurationSetSerializer> ConfigurationSetSerializer::CreateSerializer(hstring version, bool forHistory) + std::unique_ptr<ConfigurationSetSerializer> ConfigurationSetSerializer::CreateSerializer(hstring version, bool strictVersionMatching) { // Create the parser based on the version selected - AppInstaller::Utility::SemanticVersion schemaVersion(std::move(winrt::to_string(version))); + SemanticVersion schemaVersion(std::move(winrt::to_string(version))); // TODO: Consider having the version/uri/type information all together in the future if (schemaVersion.PartAt(0).Integer == 0 && schemaVersion.PartAt(1).Integer == 1) { - // Remove this one the 0.1 serializer is implemented. - if (forHistory) - { - return std::make_unique<ConfigurationSetSerializer_0_2>(); - } + // Remove this once the 0.1 serializer is implemented. + THROW_HR_IF(E_NOTIMPL, strictVersionMatching); + + return std::make_unique<ConfigurationSetSerializer_0_2>(); - THROW_HR(E_NOTIMPL); } else if (schemaVersion.PartAt(0).Integer == 0 && schemaVersion.PartAt(1).Integer == 2) { @@ -44,13 +42,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation } else if (schemaVersion.PartAt(0).Integer == 0 && schemaVersion.PartAt(1).Integer == 3) { - // Remove this one the 0.3 serializer is implemented. - if (forHistory) - { - return std::make_unique<ConfigurationSetSerializer_0_2>(); - } - - THROW_HR(E_NOTIMPL); + return std::make_unique<ConfigurationSetSerializer_0_3>(); } else { @@ -73,6 +65,15 @@ namespace winrt::Microsoft::Management::Configuration::implementation return emitter.str(); } + void ConfigurationSetSerializer::WriteYamlValueSetIfNotEmpty(AppInstaller::YAML::Emitter& emitter, ConfigurationField key, const Windows::Foundation::Collections::ValueSet& valueSet) + { + if (valueSet && valueSet.Size() != 0) + { + emitter << Key << GetConfigurationFieldName(key); + WriteYamlValueSet(emitter, valueSet); + } + } + void ConfigurationSetSerializer::WriteYamlValueSet(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSet, std::initializer_list<ConfigurationField> exclusions) { // Create a sorted list of the field names to exclude @@ -105,7 +106,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation for (const auto& value : values) { - emitter << AppInstaller::Utility::ConvertToUTF8(value); + emitter << ConvertToUTF8(value); } emitter << EndSeq; @@ -142,7 +143,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation } else if (type == PropertyType::String) { - emitter << ScalarStyle::DoubleQuoted << AppInstaller::Utility::ConvertToUTF8(property.GetString()); + emitter << ScalarStyle::DoubleQuoted << ConvertToUTF8(property.GetString()); } else if (type == PropertyType::Int64) { @@ -156,6 +157,23 @@ namespace winrt::Microsoft::Management::Configuration::implementation } } + void ConfigurationSetSerializer::WriteYamlValueIfNotEmpty(AppInstaller::YAML::Emitter& emitter, ConfigurationField key, const winrt::Windows::Foundation::IInspectable& value) + { + if (value != nullptr) + { + emitter << Key << GetConfigurationFieldName(key) << Value; + WriteYamlValue(emitter, value); + } + } + + void ConfigurationSetSerializer::WriteYamlStringValueIfNotEmpty(AppInstaller::YAML::Emitter& emitter, ConfigurationField key, hstring value) + { + if (!value.empty()) + { + emitter << Key << GetConfigurationFieldName(key) << Value << ConvertToUTF8(value); + } + } + void ConfigurationSetSerializer::WriteYamlValueSetAsArray(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSetArray) { std::vector<std::pair<int, winrt::Windows::Foundation::IInspectable>> arrayValues; @@ -185,64 +203,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation emitter << EndSeq; } - void ConfigurationSetSerializer::WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const std::vector<ConfigurationUnit>& units) - { - emitter << BeginSeq; - - for (const auto& unit : units) - { - // Resource - emitter << BeginMap; - emitter << Key << GetConfigurationFieldName(ConfigurationField::Resource) << Value << AppInstaller::Utility::ConvertToUTF8(GetResourceName(unit)); - - // Id - if (!unit.Identifier().empty()) - { - emitter << Key << GetConfigurationFieldName(ConfigurationField::Id) << Value << AppInstaller::Utility::ConvertToUTF8(unit.Identifier()); - } - - // Dependencies - if (unit.Dependencies().Size() > 0) - { - emitter << Key << GetConfigurationFieldName(ConfigurationField::DependsOn); - emitter << BeginSeq; - - for (const auto& dependency : unit.Dependencies()) - { - emitter << AppInstaller::Utility::ConvertToUTF8(dependency); - } - - emitter << EndSeq; - } - - // Directives - WriteResourceDirectives(emitter, unit); - - // Settings - const auto& settings = unit.Settings(); - emitter << Key << GetConfigurationFieldName(ConfigurationField::Settings); - WriteYamlValueSet(emitter, settings); - - emitter << EndMap; - } - - emitter << EndSeq; - } - - winrt::hstring ConfigurationSetSerializer::GetResourceName(const ConfigurationUnit& unit) - { - return unit.Type(); - } - - void ConfigurationSetSerializer::WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit) - { - const auto& metadata = unit.Metadata(); - emitter << Key << GetConfigurationFieldName(ConfigurationField::Directives); - WriteYamlValueSet(emitter, metadata); - } - - winrt::hstring ConfigurationSetSerializer::GetSchemaVersionComment(winrt::hstring version) + std::wstring_view ConfigurationSetSerializer::GetSchemaVersionCommentPrefix() { - return winrt::to_hstring(L"# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/") + version; + return L"# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/"sv; } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h @@ -7,12 +7,13 @@ #include <winget/Yaml.h> #include <initializer_list> +#include <string_view> namespace winrt::Microsoft::Management::Configuration::implementation { struct ConfigurationSetSerializer { - static std::unique_ptr<ConfigurationSetSerializer> CreateSerializer(hstring version, bool forHistory = false); + static std::unique_ptr<ConfigurationSetSerializer> CreateSerializer(hstring version, bool strictVersionMatching = false); virtual ~ConfigurationSetSerializer() noexcept = default; @@ -33,14 +34,16 @@ namespace winrt::Microsoft::Management::Configuration::implementation protected: ConfigurationSetSerializer() = default; - void WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const std::vector<ConfigurationUnit>& units); void WriteYamlValueSet(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSet, std::initializer_list<ConfigurationField> exclusions = {}); + void WriteYamlValueSetIfNotEmpty(AppInstaller::YAML::Emitter& emitter, ConfigurationField key, const Windows::Foundation::Collections::ValueSet& valueSet); + void WriteYamlValueSetAsArray(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSetArray); + void WriteYamlStringArray(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::IVector<hstring>& values); + void WriteYamlValue(AppInstaller::YAML::Emitter& emitter, const winrt::Windows::Foundation::IInspectable& value); - void WriteYamlValueSetAsArray(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSetArray); - winrt::hstring GetSchemaVersionComment(winrt::hstring version); + void WriteYamlValueIfNotEmpty(AppInstaller::YAML::Emitter& emitter, ConfigurationField key, const winrt::Windows::Foundation::IInspectable& value); + void WriteYamlStringValueIfNotEmpty(AppInstaller::YAML::Emitter& emitter, ConfigurationField key, hstring value); - virtual winrt::hstring GetResourceName(const ConfigurationUnit& unit) = 0; - virtual void WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit) = 0; + std::wstring_view GetSchemaVersionCommentPrefix(); }; } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.cpp @@ -6,11 +6,12 @@ #include <AppInstallerStrings.h> +using namespace AppInstaller::Utility; +using namespace AppInstaller::YAML; +using namespace winrt::Windows::Foundation; + namespace winrt::Microsoft::Management::Configuration::implementation { - using namespace AppInstaller::YAML; - using namespace winrt::Windows::Foundation; - hstring ConfigurationSetSerializer_0_2::Serialize(ConfigurationSet* configurationSet) { std::vector<ConfigurationUnit> assertions; @@ -18,11 +19,13 @@ namespace winrt::Microsoft::Management::Configuration::implementation for (auto unit : configurationSet->Units()) { - if (unit.Intent() == ConfigurationUnitIntent::Assert) + ConfigurationUnitIntent unitIntent = unit.Intent(); + + if (unitIntent == ConfigurationUnitIntent::Assert) { assertions.emplace_back(unit); } - else if (unit.Intent() == ConfigurationUnitIntent::Apply) + else if (unitIntent == ConfigurationUnitIntent::Apply) { resources.emplace_back(unit); } @@ -34,7 +37,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation emitter << Key << GetConfigurationFieldName(ConfigurationField::Properties); emitter << BeginMap; - emitter << Key << GetConfigurationFieldName(ConfigurationField::ConfigurationVersion) << Value << AppInstaller::Utility::ConvertToUTF8(configurationSet->SchemaVersion()); + emitter << Key << GetConfigurationFieldName(ConfigurationField::ConfigurationVersion) << Value << ConvertToUTF8(configurationSet->SchemaVersion()); if (!assertions.empty()) { @@ -51,7 +54,53 @@ namespace winrt::Microsoft::Management::Configuration::implementation emitter << EndMap; emitter << EndMap; - return GetSchemaVersionComment(configurationSet->SchemaVersion()) + winrt::to_hstring(L"\n") + winrt::to_hstring(emitter.str()); + std::wostringstream result; + result << GetSchemaVersionCommentPrefix() << static_cast<std::wstring_view>(configurationSet->SchemaVersion()) << L"\n" << ConvertToUTF16(emitter.str()); + return hstring{ std::move(result).str() }; + } + + void ConfigurationSetSerializer_0_2::WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const std::vector<ConfigurationUnit>& units) + { + emitter << BeginSeq; + + for (const auto& unit : units) + { + // Resource + emitter << BeginMap; + emitter << Key << GetConfigurationFieldName(ConfigurationField::Resource) << Value << AppInstaller::Utility::ConvertToUTF8(GetResourceName(unit)); + + // Id + if (!unit.Identifier().empty()) + { + emitter << Key << GetConfigurationFieldName(ConfigurationField::Id) << Value << AppInstaller::Utility::ConvertToUTF8(unit.Identifier()); + } + + // Dependencies + if (unit.Dependencies().Size() > 0) + { + emitter << Key << GetConfigurationFieldName(ConfigurationField::DependsOn); + emitter << BeginSeq; + + for (const auto& dependency : unit.Dependencies()) + { + emitter << AppInstaller::Utility::ConvertToUTF8(dependency); + } + + emitter << EndSeq; + } + + // Directives + WriteResourceDirectives(emitter, unit); + + // Settings + const auto& settings = unit.Settings(); + emitter << Key << GetConfigurationFieldName(ConfigurationField::Settings); + WriteYamlValueSet(emitter, settings); + + emitter << EndMap; + } + + emitter << EndSeq; } winrt::hstring ConfigurationSetSerializer_0_2::GetResourceName(const ConfigurationUnit& unit) diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.h b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.h @@ -20,7 +20,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation hstring Serialize(ConfigurationSet* configurationSet) override; protected: - winrt::hstring GetResourceName(const ConfigurationUnit& unit) override; - void WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit) override; + void WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const std::vector<ConfigurationUnit>& units); + + virtual winrt::hstring GetResourceName(const ConfigurationUnit& unit); + virtual void WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit); }; } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_3.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_3.cpp @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ConfigurationSetSerializer_0_3.h" +#include "ArgumentValidation.h" +#include "ConfigurationSetParser_0_3.h" +#include "ConfigurationSetUtilities.h" +#include <AppInstallerErrors.h> +#include <AppInstallerStrings.h> + +using namespace AppInstaller::Utility; +using namespace AppInstaller::YAML; +using namespace winrt::Windows::Foundation; + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + hstring ConfigurationSetSerializer_0_3::Serialize(ConfigurationSet* configurationSet) + { + Emitter emitter; + + emitter << BeginMap; + + emitter << Key << GetConfigurationFieldName(ConfigurationField::Schema) << Value << ConvertToUTF8(configurationSet->SchemaUri().ToString()); + + WriteYamlValueSetIfNotEmpty(emitter, ConfigurationField::Metadata, configurationSet->Metadata()); + WriteYamlParameters(emitter, configurationSet->Parameters()); + WriteYamlValueSetIfNotEmpty(emitter, ConfigurationField::Variables, configurationSet->Variables()); + WriteYamlConfigurationUnits(emitter, configurationSet->Units()); + + emitter << EndMap; + + std::wostringstream result; + result << GetSchemaVersionCommentPrefix() << static_cast<std::wstring_view>(configurationSet->SchemaVersion()) << L"\n" << ConvertToUTF16(emitter.str()); + return hstring{ std::move(result).str() }; + } + + void ConfigurationSetSerializer_0_3::WriteYamlParameters(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::IVector<Configuration::ConfigurationParameter>& values) + { + if (!values || values.Size() == 0) + { + return; + } + + emitter << Key << GetConfigurationFieldName(ConfigurationField::Parameters); + + emitter << BeginMap; + + for (const Configuration::ConfigurationParameter& parameter : values) + { + emitter << Key << ConvertToUTF8(parameter.Name()); + + emitter << BeginMap; + + auto type = parameter.Type(); + + emitter << Key << GetConfigurationFieldName(ConfigurationField::Type) << Value << ToString(type, parameter.IsSecure()); + WriteYamlValueSetIfNotEmpty(emitter, ConfigurationField::Metadata, parameter.Metadata()); + WriteYamlStringValueIfNotEmpty(emitter, ConfigurationField::Description, parameter.Description()); + WriteYamlValueIfNotEmpty(emitter, ConfigurationField::DefaultValue, parameter.DefaultValue()); + + auto allowedValues = parameter.AllowedValues(); + if (allowedValues && allowedValues.Size() != 0) + { + emitter << Key << GetConfigurationFieldName(ConfigurationField::AllowedValues); + + emitter << BeginSeq; + + for (const auto& value : allowedValues) + { + emitter << Value; + WriteYamlValue(emitter, value); + } + + emitter << EndSeq; + } + + if (IsLengthType(type)) + { + uint32_t minimumLength = parameter.MinimumLength(); + if (minimumLength != 0) + { + emitter << Key << GetConfigurationFieldName(ConfigurationField::MinimumLength) << Value << static_cast<int64_t>(minimumLength); + } + + uint32_t maximumLength = parameter.MaximumLength(); + if (maximumLength != std::numeric_limits<uint32_t>::max()) + { + emitter << Key << GetConfigurationFieldName(ConfigurationField::MaximumLength) << Value << static_cast<int64_t>(maximumLength); + } + } + + if (IsComparableType(type)) + { + WriteYamlValueIfNotEmpty(emitter, ConfigurationField::MinimumValue, parameter.MinimumValue()); + WriteYamlValueIfNotEmpty(emitter, ConfigurationField::MaximumValue, parameter.MaximumValue()); + } + + emitter << EndMap; + } + + emitter << EndMap; + } + + void ConfigurationSetSerializer_0_3::WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::IVector<Configuration::ConfigurationUnit>& values) + { + emitter << Key << GetConfigurationFieldName(ConfigurationField::Resources); + + emitter << BeginSeq; + + for (const Configuration::ConfigurationUnit& unit : values) + { + emitter << BeginMap; + + hstring identifier = unit.Identifier(); + THROW_HR_IF(WINGET_CONFIG_ERROR_MISSING_FIELD, identifier.empty()); + emitter << Key << GetConfigurationFieldName(ConfigurationField::Name) << Value << ConvertToUTF8(identifier); + + hstring type = unit.Type(); + THROW_HR_IF(WINGET_CONFIG_ERROR_MISSING_FIELD, type.empty()); + emitter << Key << GetConfigurationFieldName(ConfigurationField::Type) << Value << ConvertToUTF8(type); + + WriteYamlValueSetIfNotEmpty(emitter, ConfigurationField::Metadata, unit.Metadata()); + + auto dependencies = unit.Dependencies(); + if (dependencies && dependencies.Size() != 0) + { + emitter << Key << GetConfigurationFieldName(ConfigurationField::DependsOn); + + emitter << BeginSeq; + + for (const auto& value : dependencies) + { + emitter << ConvertToUTF8(value); + } + + emitter << EndSeq; + } + + WriteYamlValueSetIfNotEmpty(emitter, ConfigurationField::Properties, unit.Settings()); + + emitter << EndMap; + } + + emitter << EndSeq; + } +} diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_3.h b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_3.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ConfigurationSetSerializer.h" + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + // Serializer for schema version 0.3 + struct ConfigurationSetSerializer_0_3 : public ConfigurationSetSerializer + { + ConfigurationSetSerializer_0_3() {} + + virtual ~ConfigurationSetSerializer_0_3() noexcept = default; + + ConfigurationSetSerializer_0_3(const ConfigurationSetSerializer_0_3&) = delete; + ConfigurationSetSerializer_0_3& operator=(const ConfigurationSetSerializer_0_3&) = delete; + ConfigurationSetSerializer_0_3(ConfigurationSetSerializer_0_3&&) = default; + ConfigurationSetSerializer_0_3& operator=(ConfigurationSetSerializer_0_3&&) = default; + + hstring Serialize(ConfigurationSet* configurationSet) override; + + protected: + void WriteYamlParameters(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::IVector<Configuration::ConfigurationParameter>& values); + void WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::IVector<Configuration::ConfigurationUnit>& values); + }; +} diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.cpp b/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.cpp @@ -113,7 +113,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation::Database: Savepoint savepoint = Savepoint::Create(m_connection, "SetInfoTable_Add_0_1"); hstring schemaVersion = configurationSet.SchemaVersion(); - auto serializer = ConfigurationSetSerializer::CreateSerializer(schemaVersion, true); + auto serializer = ConfigurationSetSerializer::CreateSerializer(schemaVersion); StatementBuilder builder; builder.InsertInto(s_SetInfoTable_Table).Columns({ @@ -163,7 +163,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation::Database: Savepoint savepoint = Savepoint::Create(m_connection, "SetInfoTable_Update_0_1"); hstring schemaVersion = configurationSet.SchemaVersion(); - auto serializer = ConfigurationSetSerializer::CreateSerializer(schemaVersion, true); + auto serializer = ConfigurationSetSerializer::CreateSerializer(schemaVersion); StatementBuilder builder; builder.Update(s_SetInfoTable_Table).Set(). diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_1/UnitInfoTable.cpp b/src/Microsoft.Management.Configuration/Database/Schema/0_1/UnitInfoTable.cpp @@ -106,7 +106,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation::Database: std::queue<UnitsToInsert> unitsToInsert; unitsToInsert.emplace(UnitsToInsert{ std::nullopt, configurationUnit }); - auto serializer = ConfigurationSetSerializer::CreateSerializer(schemaVersion, true); + auto serializer = ConfigurationSetSerializer::CreateSerializer(schemaVersion); while (!unitsToInsert.empty()) { diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj @@ -218,6 +218,7 @@ <ClInclude Include="ConfigurationSetParser_0_3.h" /> <ClInclude Include="ConfigurationSetSerializer.h" /> <ClInclude Include="ConfigurationSetSerializer_0_2.h" /> + <ClInclude Include="ConfigurationSetSerializer_0_3.h" /> <ClInclude Include="ConfigurationSetUtilities.h" /> <ClInclude Include="ConfigurationStaticFunctions.h" /> <ClInclude Include="ConfigurationStatus.h" /> @@ -272,6 +273,7 @@ <ClCompile Include="ConfigurationSetParser_0_3.cpp" /> <ClCompile Include="ConfigurationSetSerializer.cpp" /> <ClCompile Include="ConfigurationSetSerializer_0_2.cpp" /> + <ClCompile Include="ConfigurationSetSerializer_0_3.cpp" /> <ClCompile Include="ConfigurationSetUtilities.cpp" /> <ClCompile Include="ConfigurationStaticFunctions.cpp" /> <ClCompile Include="ConfigurationStatus.cpp" /> diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters @@ -150,6 +150,9 @@ <ClCompile Include="Database\Schema\0_3\StatusItemTable.cpp"> <Filter>Database\Schema\0_3</Filter> </ClCompile> + <ClCompile Include="ConfigurationSetSerializer_0_3.cpp"> + <Filter>Parser</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h" /> @@ -309,6 +312,9 @@ <ClInclude Include="Database\Schema\0_3\StatusItemTable.h"> <Filter>Database\Schema\0_3</Filter> </ClInclude> + <ClInclude Include="ConfigurationSetSerializer_0_3.h"> + <Filter>Parser</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <Midl Include="Microsoft.Management.Configuration.idl" />