winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 1b24d12bf6b1360cb47e07dfdbf2910ecc678e5f
parent cefeec71e5b41ed06e7be9dd0da39e8a1e5c04d9
Author: JohnMcPMS <johnmcp@microsoft.com>
Date:   Fri, 16 May 2025 11:09:26 -0700

DSCv3 admin settings resource (#5458)

## Change
Adds an admin settings resource to mirror the existing v2.

Also adds `winget settings reset --all` to reset all admin settings to
their default values (respecting group policy of course).
Diffstat:
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj | 2++
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters | 6++++++
Asrc/AppInstallerCLICore/Commands/DscAdminSettingsResource.cpp | 294+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/Commands/DscAdminSettingsResource.h | 25+++++++++++++++++++++++++
Msrc/AppInstallerCLICore/Commands/DscCommand.cpp | 2++
Msrc/AppInstallerCLICore/Commands/DscComposableObject.h | 2+-
Msrc/AppInstallerCLICore/Commands/SettingsCommand.cpp | 20++++++++++++++++++--
Msrc/AppInstallerCLICore/Resources.h | 5+++++
Msrc/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp | 3++-
Msrc/AppInstallerCLICore/Workflows/SettingsFlow.cpp | 6++++++
Msrc/AppInstallerCLICore/Workflows/SettingsFlow.h | 6++++++
Asrc/AppInstallerCLIE2ETests/DSCv3AdminSettingsResourceCommand.cs | 372+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 15+++++++++++++++
Msrc/AppInstallerCommonCore/AdminSettings.cpp | 12++++++++++++
Msrc/AppInstallerCommonCore/Public/winget/AdminSettings.h | 1+
15 files changed, 767 insertions(+), 4 deletions(-)

diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -302,6 +302,7 @@ <ClInclude Include="Commands\ConfigureValidateCommand.h" /> <ClInclude Include="Commands\DebugCommand.h" /> <ClInclude Include="Commands\DownloadCommand.h" /> + <ClInclude Include="Commands\DscAdminSettingsResource.h" /> <ClInclude Include="Commands\DscCommand.h" /> <ClInclude Include="Commands\DscCommandBase.h" /> <ClInclude Include="Commands\DscComposableObject.h" /> @@ -390,6 +391,7 @@ <ClCompile Include="Commands\ConfigureValidateCommand.cpp" /> <ClCompile Include="Commands\DebugCommand.cpp" /> <ClCompile Include="Commands\DownloadCommand.cpp" /> + <ClCompile Include="Commands\DscAdminSettingsResource.cpp" /> <ClCompile Include="Commands\DscCommand.cpp" /> <ClCompile Include="Commands\DscCommandBase.cpp" /> <ClCompile Include="Commands\DscComposableObject.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -290,6 +290,9 @@ <ClInclude Include="Commands\DscUserSettingsFileResource.h"> <Filter>Commands\Configuration</Filter> </ClInclude> + <ClInclude Include="Commands\DscAdminSettingsResource.h"> + <Filter>Commands\Configuration</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -547,6 +550,9 @@ <ClCompile Include="Commands\DscUserSettingsFileResource.cpp"> <Filter>Commands\Configuration</Filter> </ClCompile> + <ClCompile Include="Commands\DscAdminSettingsResource.cpp"> + <Filter>Commands\Configuration</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Commands/DscAdminSettingsResource.cpp b/src/AppInstallerCLICore/Commands/DscAdminSettingsResource.cpp @@ -0,0 +1,294 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "DscAdminSettingsResource.h" +#include "DscComposableObject.h" +#include "Resources.h" +#include <winget/AdminSettings.h> + +using namespace AppInstaller::Utility::literals; +using namespace AppInstaller::Repository; + +namespace AppInstaller::CLI +{ + namespace AdminSettingsDetails + { + // The base for admin settings. + struct IAdminSetting + { + virtual ~IAdminSetting() = default; + + // Gets the name of the setting. + virtual Utility::LocIndView SettingName() const = 0; + + // Tests the value. + // Returns true if in the desired state; false if not. + virtual bool Test() const = 0; + + // Sets the value. + // Returns true if the value could be set; false if not. + virtual bool Set() const = 0; + }; + + // A boolean based admin setting + struct AdminSetting_Bool : public IAdminSetting + { + AdminSetting_Bool(Settings::BoolAdminSetting setting, bool value) : m_setting(setting), m_value(value) {} + + Utility::LocIndView SettingName() const override + { + return Settings::AdminSettingToString(m_setting); + } + + bool Test() const override + { + return Settings::IsAdminSettingEnabled(m_setting) == m_value; + } + + bool Set() const override + { + return m_value ? Settings::EnableAdminSetting(m_setting) : Settings::DisableAdminSetting(m_setting); + } + + private: + Settings::BoolAdminSetting m_setting; + bool m_value; + }; + + // A string based admin setting + struct AdminSetting_String : public IAdminSetting + { + AdminSetting_String(Settings::StringAdminSetting setting, std::optional<std::string> value) : m_setting(setting), m_value(std::move(value)) {} + + Utility::LocIndView SettingName() const override + { + return Settings::AdminSettingToString(m_setting); + } + + bool Test() const override + { + return Settings::GetAdminSetting(m_setting) == m_value; + } + + bool Set() const override + { + return m_value ? Settings::SetAdminSetting(m_setting, m_value.value()) : Settings::ResetAdminSetting(m_setting); + } + + private: + Settings::StringAdminSetting m_setting; + std::optional<std::string> m_value; + }; + } + + namespace + { + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(SettingsProperty, Json::Value, Settings, "settings", Resource::String::DscResourcePropertyDescriptionAdminSettingsSettings); + + using AdminSettingsResourceObject = DscComposableObject<StandardInDesiredStateProperty, SettingsProperty>; + + struct AdminSettingsFunctionData + { + AdminSettingsFunctionData() = default; + + AdminSettingsFunctionData(const std::optional<Json::Value>& json) : + Input(json) + { + } + + const AdminSettingsResourceObject Input; + AdminSettingsResourceObject Output; + std::vector<std::unique_ptr<AdminSettingsDetails::IAdminSetting>> InputSettings; + + // Converts the input settings into the appropriate settings object. + void ParseSettings() + { + if (Input.Settings()) + { + const Json::Value& inputSettings = Input.Settings().value(); + for (const auto& property : inputSettings.getMemberNames()) + { + auto boolSetting = Settings::StringToBoolAdminSetting(property); + if (boolSetting != Settings::BoolAdminSetting::Unknown) + { + bool value = inputSettings[property].asBool(); + AICLI_LOG(Config, Info, << "Bool admin setting: " << property << " => " << (value ? "true" : "false")); + InputSettings.emplace_back(std::make_unique<AdminSettingsDetails::AdminSetting_Bool>(boolSetting, value)); + continue; + } + + auto stringSetting = Settings::StringToStringAdminSetting(property); + if (stringSetting != Settings::StringAdminSetting::Unknown) + { + const auto& propertyNode = inputSettings[property]; + std::optional<std::string> value; + + if (propertyNode.isNull()) + { + AICLI_LOG(Config, Info, << "String admin setting: " << property << " => null"); + } + else + { + value = propertyNode.asString(); + AICLI_LOG(Config, Info, << "String admin setting: " << property << " => `" << value.value() << "`"); + } + + InputSettings.emplace_back(std::make_unique<AdminSettingsDetails::AdminSetting_String>(stringSetting, std::move(value))); + continue; + } + + AICLI_LOG(Config, Warning, << "Unknown admin setting: " << property); + } + } + } + + // Fills the Output object with the current state + void Get() + { + Json::Value adminSettings{ Json::objectValue }; + + for (const auto& setting : Settings::GetAllBoolAdminSettings()) + { + auto str = std::string{ Settings::AdminSettingToString(setting) }; + adminSettings[str] = Settings::IsAdminSettingEnabled(setting); + } + + for (const auto& setting : Settings::GetAllStringAdminSettings()) + { + auto name = std::string{ Settings::AdminSettingToString(setting) }; + auto value = Settings::GetAdminSetting(setting); + if (value) + { + adminSettings[name] = value.value(); + } + } + + Output.Settings(std::move(adminSettings)); + } + + // Determines if the current Output values match the Input values state. + bool Test() + { + for (const auto& setting : InputSettings) + { + if (!setting->Test()) + { + return false; + } + } + + return true; + } + + // Sets all of the input settings. + void Set() + { + for (const auto& setting : InputSettings) + { + if (!setting->Test()) + { + if (!setting->Set()) + { + auto message = Resource::String::DisabledByGroupPolicy(setting->SettingName()); + THROW_HR_MSG(APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY, "%hs", message.get().c_str()); + } + } + } + } + + Json::Value DiffJson(bool inDesiredState) + { + Json::Value result{ Json::ValueType::arrayValue }; + + if (!inDesiredState) + { + result.append(std::string{ SettingsProperty::Name() }); + } + + return result; + } + }; + } + + DscAdminSettingsResource::DscAdminSettingsResource(std::string_view parent) : + DscCommandBase(parent, "admin-settings", DscResourceKind::Resource, + DscFunctions::Get | DscFunctions::Set | DscFunctions::Test | DscFunctions::Export | DscFunctions::Schema, + DscFunctionModifiers::ImplementsPretest | DscFunctionModifiers::ReturnsStateAndDiff) + { + } + + Resource::LocString DscAdminSettingsResource::ShortDescription() const + { + return Resource::String::DscAdminSettingsResourceShortDescription; + } + + Resource::LocString DscAdminSettingsResource::LongDescription() const + { + return Resource::String::DscAdminSettingsResourceLongDescription; + } + + std::string DscAdminSettingsResource::ResourceType() const + { + return "AdminSettings"; + } + + void DscAdminSettingsResource::ResourceFunctionGet(Execution::Context& context) const + { + AdminSettingsFunctionData data; + data.Get(); + WriteJsonOutputLine(context, data.Output.ToJson()); + } + + void DscAdminSettingsResource::ResourceFunctionSet(Execution::Context& context) const + { + if (auto json = GetJsonFromInput(context)) + { + AdminSettingsFunctionData data{ json }; + + data.ParseSettings(); + + bool inDesiredState = data.Test(); + if (!inDesiredState) + { + Workflow::EnsureRunningAsAdmin(context); + + if (context.IsTerminated()) + { + return; + } + + data.Set(); + } + + data.Get(); + WriteJsonOutputLine(context, data.Output.ToJson()); + WriteJsonOutputLine(context, data.DiffJson(inDesiredState)); + } + } + + void DscAdminSettingsResource::ResourceFunctionTest(Execution::Context& context) const + { + if (auto json = GetJsonFromInput(context)) + { + AdminSettingsFunctionData data{ json }; + + data.ParseSettings(); + + data.Get(); + data.Output.InDesiredState(data.Test()); + + WriteJsonOutputLine(context, data.Output.ToJson()); + WriteJsonOutputLine(context, data.DiffJson(data.Output.InDesiredState().value())); + } + } + + void DscAdminSettingsResource::ResourceFunctionExport(Execution::Context& context) const + { + ResourceFunctionGet(context); + } + + void DscAdminSettingsResource::ResourceFunctionSchema(Execution::Context& context) const + { + WriteJsonOutputLine(context, AdminSettingsResourceObject::Schema(ResourceType())); + } +} diff --git a/src/AppInstallerCLICore/Commands/DscAdminSettingsResource.h b/src/AppInstallerCLICore/Commands/DscAdminSettingsResource.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "DscCommandBase.h" + +namespace AppInstaller::CLI +{ + // A resource for managing admin settings. + struct DscAdminSettingsResource : public DscCommandBase + { + DscAdminSettingsResource(std::string_view parent); + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + protected: + std::string ResourceType() const override; + + void ResourceFunctionGet(Execution::Context& context) const override; + void ResourceFunctionSet(Execution::Context& context) const override; + void ResourceFunctionTest(Execution::Context& context) const override; + void ResourceFunctionExport(Execution::Context& context) const override; + void ResourceFunctionSchema(Execution::Context& context) const override; + }; +} diff --git a/src/AppInstallerCLICore/Commands/DscCommand.cpp b/src/AppInstallerCLICore/Commands/DscCommand.cpp @@ -5,6 +5,7 @@ #include "DscPackageResource.h" #include "DscUserSettingsFileResource.h" #include "DscSourceResource.h" +#include "DscAdminSettingsResource.h" #ifndef AICLI_DISABLE_TEST_HOOKS #include "DscTestFileResource.h" @@ -33,6 +34,7 @@ namespace AppInstaller::CLI std::make_unique<DscPackageResource>(FullName()), std::make_unique<DscSourceResource>(FullName()), std::make_unique<DscUserSettingsFileResource>(FullName()), + std::make_unique<DscAdminSettingsResource>(FullName()), #ifndef AICLI_DISABLE_TEST_HOOKS std::make_unique<DscTestFileResource>(FullName()), std::make_unique<DscTestJsonResource>(FullName()), diff --git a/src/AppInstallerCLICore/Commands/DscComposableObject.h b/src/AppInstallerCLICore/Commands/DscComposableObject.h @@ -162,7 +162,7 @@ namespace AppInstaller::CLI static void FromJson(Derived* self, const Json::Value* value, bool ignoreFieldRequirements) { - if (value) + if (value && !value->isNull()) { self->m_value = GetJsonTypeValue<PropertyType>::Get(*value); } diff --git a/src/AppInstallerCLICore/Commands/SettingsCommand.cpp b/src/AppInstallerCLICore/Commands/SettingsCommand.cpp @@ -162,7 +162,8 @@ namespace AppInstaller::CLI std::vector<Argument> SettingsResetCommand::GetArguments() const { return { - Argument { Execution::Args::Type::SettingName, Resource::String::SettingNameArgumentDescription, ArgumentType::Positional, true }, + Argument { Execution::Args::Type::SettingName, Resource::String::SettingNameArgumentDescription, ArgumentType::Positional }, + Argument { Execution::Args::Type::All, Resource::String::ResetAllAdminSettingsArgumentDescription, ArgumentType::Flag }, }; } @@ -183,6 +184,21 @@ namespace AppInstaller::CLI void SettingsResetCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const { + if (execArgs.Contains(Execution::Args::Type::All)) + { + if (execArgs.Contains(Execution::Args::Type::SettingName)) + { + throw CommandException(Resource::String::MultipleExclusiveArgumentsProvided("all|setting"_liv)); + } + + return; + } + + if (!execArgs.Contains(Execution::Args::Type::SettingName)) + { + throw CommandException(Resource::String::RequiredArgError(ArgumentCommon::ForType(Execution::Args::Type::SettingName).Name)); + } + // Get admin setting string for all available options except Unknown. // We accept both bool and string settings std::vector<Utility::LocIndString> adminSettingList; @@ -208,6 +224,6 @@ namespace AppInstaller::CLI { context << Workflow::EnsureRunningAsAdmin << - Workflow::ResetAdminSetting; + (context.Args.Contains(Execution::Args::Type::All) ? Workflow::ResetAllAdminSettings : Workflow::ResetAdminSetting); } } diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -223,6 +223,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(Downloading); WINGET_DEFINE_RESOURCE_STRINGID(DscCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(DscCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(DscAdminSettingsResourceShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(DscAdminSettingsResourceLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(DscPackageResourceShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(DscPackageResourceLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionGet); @@ -239,6 +241,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionExist); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionInDesiredState); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionAcceptAgreements); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionAdminSettingsSettings); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageId); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageSource); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageVersion); @@ -571,6 +574,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ReservedFilenameError); WINGET_DEFINE_RESOURCE_STRINGID(ResetAdminSettingFailed); WINGET_DEFINE_RESOURCE_STRINGID(ResetAdminSettingSucceeded); + WINGET_DEFINE_RESOURCE_STRINGID(ResetAllAdminSettingsArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ResetAllAdminSettingsSucceeded); WINGET_DEFINE_RESOURCE_STRINGID(ResumeCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ResumeCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ResumeIdArgumentDescription); diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -52,6 +52,7 @@ namespace AppInstaller::CLI::Workflow constexpr std::wstring_view s_UnitType_WinGetPackage_DSCv3 = WINGET_DSCV3_MODULE_NAME_WIDE L"/Package"; constexpr std::wstring_view s_UnitType_WinGetSource_DSCv3 = WINGET_DSCV3_MODULE_NAME_WIDE L"/Source"; constexpr std::wstring_view s_UnitType_WinGetUserSettingsFile_DSCv3 = WINGET_DSCV3_MODULE_NAME_WIDE L"/UserSettingsFile"; + constexpr std::wstring_view s_UnitType_WinGetAdminSettings_DSCv3 = WINGET_DSCV3_MODULE_NAME_WIDE L"/AdminSettings"; constexpr std::wstring_view s_UnitType_PowerShellModuleGet = L"PowerShellGet/PSModule"; constexpr std::wstring_view s_Module_WinGetClient = L"Microsoft.WinGet.DSC"; @@ -86,7 +87,7 @@ namespace AppInstaller::CLI::Workflow std::vector<PredefinedResource> PredefinedResourcesForExport() { return { - { {}, { { s_UnitType_WinGetUserSettingsFile_DSCv3 } } }, + { {}, { { s_UnitType_WinGetUserSettingsFile_DSCv3 }, { s_UnitType_WinGetAdminSettings_DSCv3, true } } }, { L"Microsoft.Windows.Settings", { { L"Microsoft.Windows.Settings/WindowsSettings", true } } }, }; } diff --git a/src/AppInstallerCLICore/Workflows/SettingsFlow.cpp b/src/AppInstallerCLICore/Workflows/SettingsFlow.cpp @@ -113,6 +113,12 @@ namespace AppInstaller::CLI::Workflow } } + void ResetAllAdminSettings(Execution::Context& context) + { + Settings::ResetAllAdminSettings(); + context.Reporter.Info() << Resource::String::ResetAllAdminSettingsSucceeded << std::endl; + } + void OpenUserSetting(Execution::Context& context) { // Show warnings only when the setting command is executed. diff --git a/src/AppInstallerCLICore/Workflows/SettingsFlow.h b/src/AppInstallerCLICore/Workflows/SettingsFlow.h @@ -29,6 +29,12 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void ResetAdminSetting(Execution::Context& context); + // Resets all admin settings to the default. + // Required Args: None + // Inputs: None + // Outputs: None + void ResetAllAdminSettings(Execution::Context& context); + // Opens the user settings. // Required Args: None // Inputs: None diff --git a/src/AppInstallerCLIE2ETests/DSCv3AdminSettingsResourceCommand.cs b/src/AppInstallerCLIE2ETests/DSCv3AdminSettingsResourceCommand.cs @@ -0,0 +1,372 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DSCv3AdminSettingsResourceCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System.Collections.Generic; + using System.Text.Json; + using System.Text.Json.Nodes; + using System.Text.Json.Serialization; + using AppInstallerCLIE2ETests.Helpers; + using NUnit.Framework; + + /// <summary> + /// `Configure` command tests. + /// </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1010:Opening square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1011:Closing square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")] + public class DSCv3AdminSettingsResourceCommand : DSCv3ResourceTestBase + { + private const string AdminSettingsResource = "admin-settings"; + private const string SettingsPropertyName = "settings"; + + // Bool settings + private const string BypassCertificatePinningForMicrosoftStore = "BypassCertificatePinningForMicrosoftStore"; + private const string InstallerHashOverride = "InstallerHashOverride"; + private const string LocalArchiveMalwareScanOverride = "LocalArchiveMalwareScanOverride"; + private const string LocalManifestFiles = "LocalManifestFiles"; + private const string ProxyCommandLineOptions = "ProxyCommandLineOptions"; + + // String settings + private const string DefaultProxy = "DefaultProxy"; + + // Not a setting + private const string NotAnAdminSettingName = "NotAnAdminSetting"; + + /// <summary> + /// Setup done once before all the tests here. + /// </summary> + [OneTimeSetUp] + public void OneTimeSetup() + { + WinGetSettingsHelper.ConfigureFeature("dsc3", true); + EnsureTestResourcePresence(); + } + + /// <summary> + /// Teardown done once after all the tests here. + /// </summary> + [OneTimeTearDown] + public void OneTimeTeardown() + { + WinGetSettingsHelper.ConfigureFeature("dsc3", false); + ResetAllSettings(); + GroupPolicyHelper.DeleteExistingPolicies(); + } + + /// <summary> + /// Set up. + /// </summary> + [SetUp] + public void Setup() + { + ResetAllSettings(); + GroupPolicyHelper.DeleteExistingPolicies(); + } + + /// <summary> + /// Calls `get` on the `admin-settings` resource with the value not present. + /// </summary> + /// <param name="function">The resource function to invoke.</param> + [TestCase(GetFunction)] + [TestCase(ExportFunction)] + public void AdminSettings_Get(string function) + { + var result = RunDSCv3Command(AdminSettingsResource, function, null); + AssertSuccessfulResourceRun(ref result); + + AdminSettingsResourceData output = GetSingleOutputLineAs<AdminSettingsResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.IsNotNull(output.Settings); + Assert.IsTrue(output.Settings.ContainsKey(LocalManifestFiles)); + Assert.IsFalse(output.Settings.ContainsKey(DefaultProxy)); + Assert.IsFalse(output.Settings.ContainsKey(NotAnAdminSettingName)); + } + + /// <summary> + /// Calls `test` on the `admin-settings` resource with a bool setting. + /// </summary> + /// <param name="settingName">The setting to test.</param> + [TestCase(BypassCertificatePinningForMicrosoftStore)] + [TestCase(InstallerHashOverride)] + [TestCase(LocalArchiveMalwareScanOverride)] + [TestCase(LocalManifestFiles)] + [TestCase(ProxyCommandLineOptions)] + public void AdminSettings_Test_BoolSetting(string settingName) + { + AdminSettingsResourceData resourceData = new AdminSettingsResourceData() { Settings = new JsonObject() }; + + resourceData.Settings[settingName] = true; + var result = RunDSCv3Command(AdminSettingsResource, TestFunction, resourceData); + AssertTestOfBoolSetting(ref result, settingName, false, true); + + resourceData.Settings[settingName] = false; + result = RunDSCv3Command(AdminSettingsResource, TestFunction, resourceData); + AssertTestOfBoolSetting(ref result, settingName, false, false); + + Assert.AreEqual(Constants.ErrorCode.S_OK, TestCommon.RunAICLICommand("settings", $"--enable {settingName}").ExitCode); + + resourceData.Settings[settingName] = false; + result = RunDSCv3Command(AdminSettingsResource, TestFunction, resourceData); + AssertTestOfBoolSetting(ref result, settingName, true, false); + + resourceData.Settings[settingName] = true; + result = RunDSCv3Command(AdminSettingsResource, TestFunction, resourceData); + AssertTestOfBoolSetting(ref result, settingName, true, true); + } + + /// <summary> + /// Calls `test` on the `admin-settings` resource with a string setting. + /// </summary> + /// <param name="settingName">The setting to test.</param> + [TestCase(DefaultProxy)] + public void AdminSettings_Test_StringSetting(string settingName) + { + const string testValue = "A string to test"; + const string differentTestValue = "A different value"; + + AdminSettingsResourceData resourceData = new AdminSettingsResourceData() { Settings = new JsonObject() }; + + resourceData.Settings[settingName] = null; + var result = RunDSCv3Command(AdminSettingsResource, TestFunction, resourceData); + AssertTestOfStringSetting(ref result, settingName, null, null); + + resourceData.Settings[settingName] = testValue; + result = RunDSCv3Command(AdminSettingsResource, TestFunction, resourceData); + AssertTestOfStringSetting(ref result, settingName, null, testValue); + + Assert.AreEqual(Constants.ErrorCode.S_OK, TestCommon.RunAICLICommand("settings set", $"{settingName} \"{testValue}\"").ExitCode); + + resourceData.Settings[settingName] = null; + result = RunDSCv3Command(AdminSettingsResource, TestFunction, resourceData); + AssertTestOfStringSetting(ref result, settingName, testValue, null); + + resourceData.Settings[settingName] = testValue; + result = RunDSCv3Command(AdminSettingsResource, TestFunction, resourceData); + AssertTestOfStringSetting(ref result, settingName, testValue, testValue); + + resourceData.Settings[settingName] = differentTestValue; + result = RunDSCv3Command(AdminSettingsResource, TestFunction, resourceData); + AssertTestOfStringSetting(ref result, settingName, testValue, differentTestValue); + } + + /// <summary> + /// Calls `test` on the `admin-settings` resource with a complex input. + /// </summary> + [Test] + public void AdminSettings_Test_Complex() + { + Assert.AreEqual(Constants.ErrorCode.S_OK, TestCommon.RunAICLICommand("settings", $"--enable {LocalArchiveMalwareScanOverride}").ExitCode); + Assert.AreEqual(Constants.ErrorCode.S_OK, TestCommon.RunAICLICommand("settings", $"--enable {LocalManifestFiles}").ExitCode); + + AdminSettingsResourceData resourceData = new AdminSettingsResourceData() { Settings = new JsonObject() }; + resourceData.Settings[LocalArchiveMalwareScanOverride] = true; + resourceData.Settings[BypassCertificatePinningForMicrosoftStore] = false; + resourceData.Settings[DefaultProxy] = null; + + var result = RunDSCv3Command(AdminSettingsResource, TestFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + + (AdminSettingsResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<AdminSettingsResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.IsTrue(output.InDesiredState); + Assert.IsNotNull(output.Settings); + + AssertDiffState(diff, []); + } + + /// <summary> + /// Calls `set` on the `admin-settings` resource with a bool setting. + /// </summary> + /// <param name="settingName">The setting to test.</param> + [TestCase(BypassCertificatePinningForMicrosoftStore)] + [TestCase(InstallerHashOverride)] + [TestCase(LocalArchiveMalwareScanOverride)] + [TestCase(LocalManifestFiles)] + [TestCase(ProxyCommandLineOptions)] + public void AdminSettings_Set_BoolSetting(string settingName) + { + AdminSettingsResourceData resourceData = new AdminSettingsResourceData() { Settings = new JsonObject() }; + + resourceData.Settings[settingName] = true; + var result = RunDSCv3Command(AdminSettingsResource, SetFunction, resourceData); + AssertSetOfBoolSetting(ref result, settingName, false, true); + + result = RunDSCv3Command(AdminSettingsResource, SetFunction, resourceData); + AssertSetOfBoolSetting(ref result, settingName, true, true); + + resourceData.Settings[settingName] = false; + result = RunDSCv3Command(AdminSettingsResource, SetFunction, resourceData); + AssertSetOfBoolSetting(ref result, settingName, true, false); + + result = RunDSCv3Command(AdminSettingsResource, SetFunction, resourceData); + AssertSetOfBoolSetting(ref result, settingName, false, false); + } + + /// <summary> + /// Calls `set` on the `admin-settings` resource with a string setting. + /// </summary> + /// <param name="settingName">The setting to test.</param> + [TestCase(DefaultProxy)] + public void AdminSettings_Set_StringSetting(string settingName) + { + const string testValue = "A string to test"; + const string differentTestValue = "A different value"; + + AdminSettingsResourceData resourceData = new AdminSettingsResourceData() { Settings = new JsonObject() }; + + resourceData.Settings[settingName] = null; + var result = RunDSCv3Command(AdminSettingsResource, SetFunction, resourceData); + AssertSetOfStringSetting(ref result, settingName, null, null); + + resourceData.Settings[settingName] = testValue; + result = RunDSCv3Command(AdminSettingsResource, SetFunction, resourceData); + AssertSetOfStringSetting(ref result, settingName, null, testValue); + + resourceData.Settings[settingName] = testValue; + result = RunDSCv3Command(AdminSettingsResource, SetFunction, resourceData); + AssertSetOfStringSetting(ref result, settingName, testValue, testValue); + + resourceData.Settings[settingName] = differentTestValue; + result = RunDSCv3Command(AdminSettingsResource, SetFunction, resourceData); + AssertSetOfStringSetting(ref result, settingName, testValue, differentTestValue); + + resourceData.Settings[settingName] = null; + result = RunDSCv3Command(AdminSettingsResource, SetFunction, resourceData); + AssertSetOfStringSetting(ref result, settingName, differentTestValue, null); + } + + /// <summary> + /// Calls `set` on the `admin-settings` resource with a complex input. + /// </summary> + [Test] + public void AdminSettings_Set_Complex() + { + const string testValue = "A string to test"; + + AdminSettingsResourceData resourceData = new AdminSettingsResourceData() { Settings = new JsonObject() }; + resourceData.Settings[LocalArchiveMalwareScanOverride] = true; + resourceData.Settings[BypassCertificatePinningForMicrosoftStore] = false; + resourceData.Settings[DefaultProxy] = testValue; + + var result = RunDSCv3Command(AdminSettingsResource, SetFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + + (AdminSettingsResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<AdminSettingsResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.IsNotNull(output.Settings); + Assert.AreEqual(JsonValueKind.True, output.Settings[LocalArchiveMalwareScanOverride].AsValue().GetValueKind()); + Assert.AreEqual(JsonValueKind.False, output.Settings[BypassCertificatePinningForMicrosoftStore].AsValue().GetValueKind()); + Assert.AreEqual(testValue, output.Settings[DefaultProxy].AsValue().GetValue<string>()); + + AssertDiffState(diff, [ SettingsPropertyName ]); + } + + /// <summary> + /// Calls `set` on the `admin-settings` resource attempting to change a setting with group policy enabled. + /// </summary> + [Test] + public void AdminSettings_Set_GroupPolicyBlocked() + { + GroupPolicyHelper.EnableHashOverride.Disable(); + + AdminSettingsResourceData resourceData = new AdminSettingsResourceData() { Settings = new JsonObject() }; + resourceData.Settings[InstallerHashOverride] = true; + + var result = RunDSCv3Command(AdminSettingsResource, SetFunction, resourceData); + Assert.AreEqual(Constants.ErrorCode.ERROR_BLOCKED_BY_POLICY, result.ExitCode); + } + + private static void ResetAllSettings() + { + Assert.AreEqual(Constants.ErrorCode.S_OK, TestCommon.RunAICLICommand("settings reset", "--all").ExitCode); + } + + private static void AssertTestOfBoolSetting(ref TestCommon.RunCommandResult result, string settingName, bool expectedState, bool testState) + { + AssertSuccessfulResourceRun(ref result); + + bool inDesiredState = expectedState == testState; + + (AdminSettingsResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<AdminSettingsResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.AreEqual(inDesiredState, output.InDesiredState); + Assert.IsNotNull(output.Settings); + Assert.IsTrue(output.Settings.ContainsKey(settingName)); + Assert.AreEqual(expectedState ? JsonValueKind.True : JsonValueKind.False, output.Settings[settingName].AsValue().GetValueKind()); + + AssertDiffState(diff, inDesiredState ? [] : [ SettingsPropertyName ]); + } + + private static void AssertSetOfBoolSetting(ref TestCommon.RunCommandResult result, string settingName, bool previousState, bool desiredState) + { + AssertSuccessfulResourceRun(ref result); + + bool inDesiredState = previousState == desiredState; + + (AdminSettingsResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<AdminSettingsResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.IsNotNull(output.Settings); + Assert.IsTrue(output.Settings.ContainsKey(settingName)); + Assert.AreEqual(desiredState ? JsonValueKind.True : JsonValueKind.False, output.Settings[settingName].AsValue().GetValueKind()); + + AssertDiffState(diff, inDesiredState ? [] : [ SettingsPropertyName ]); + } + + private static void AssertTestOfStringSetting(ref TestCommon.RunCommandResult result, string settingName, string expectedState, string testState) + { + AssertSuccessfulResourceRun(ref result); + + bool inDesiredState = expectedState == testState; + + (AdminSettingsResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<AdminSettingsResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.AreEqual(inDesiredState, output.InDesiredState); + Assert.IsNotNull(output.Settings); + if (expectedState != null) + { + Assert.IsTrue(output.Settings.ContainsKey(settingName)); + Assert.AreEqual(expectedState, output.Settings[settingName].AsValue().GetValue<string>()); + } + else + { + Assert.IsFalse(output.Settings.ContainsKey(settingName)); + } + + AssertDiffState(diff, inDesiredState ? [] : [SettingsPropertyName]); + } + + private static void AssertSetOfStringSetting(ref TestCommon.RunCommandResult result, string settingName, string previousState, string desiredState) + { + AssertSuccessfulResourceRun(ref result); + + bool inDesiredState = previousState == desiredState; + + (AdminSettingsResourceData output, List<string> diff) = GetSingleOutputLineAndDiffAs<AdminSettingsResourceData>(result.StdOut); + Assert.IsNotNull(output); + Assert.IsNotNull(output.Settings); + if (desiredState != null) + { + Assert.IsTrue(output.Settings.ContainsKey(settingName)); + Assert.AreEqual(desiredState, output.Settings[settingName].AsValue().GetValue<string>()); + } + else + { + Assert.IsFalse(output.Settings.ContainsKey(settingName)); + } + + AssertDiffState(diff, inDesiredState ? [] : [SettingsPropertyName]); + } + + private class AdminSettingsResourceData + { + [JsonPropertyName(InDesiredStatePropertyName)] + public bool? InDesiredState { get; set; } + + public JsonObject Settings { get; set; } + } + } +} diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3355,4 +3355,19 @@ Please specify one of them using the --source option to proceed.</value> <value>Failed to install Desired State Configuration package. Install the package manually or provide the path to dsc.exe through --processor-path argument.</value> <comment>{Locked="dsc.exe","--processor-path"}</comment> </data> + <data name="DscResourcePropertyDescriptionAdminSettingsSettings" xml:space="preserve"> + <value>An object containing the administrator settings and their values.</value> + </data> + <data name="DscAdminSettingsResourceShortDescription" xml:space="preserve"> + <value>Manage administrator settings</value> + </data> + <data name="DscAdminSettingsResourceLongDescription" xml:space="preserve"> + <value>Allows management of administrator settings via the DSC v3 command line interface protocol. See the help link for details.</value> + </data> + <data name="ResetAllAdminSettingsArgumentDescription" xml:space="preserve"> + <value>Resets all admin settings</value> + </data> + <data name="ResetAllAdminSettingsSucceeded" xml:space="preserve"> + <value>All admin settings reset.</value> + </data> </root> diff --git a/src/AppInstallerCommonCore/AdminSettings.cpp b/src/AppInstallerCommonCore/AdminSettings.cpp @@ -60,6 +60,8 @@ namespace AppInstaller::Settings bool GetAdminSettingValue(BoolAdminSetting setting) const; std::optional<std::string> GetAdminSettingValue(StringAdminSetting setting) const; + void Reset(); + private: void LoadAdminSettings(); [[nodiscard]] bool SaveAdminSettings(); @@ -171,6 +173,11 @@ namespace AppInstaller::Settings } } + void AdminSettingsInternal::Reset() + { + m_settingStream.Remove(); + } + void AdminSettingsInternal::LoadAdminSettings() { auto stream = m_settingStream.Get(); @@ -406,6 +413,11 @@ namespace AppInstaller::Settings return true; } + void ResetAllAdminSettings() + { + AdminSettingsInternal{}.Reset(); + } + std::optional<std::string> GetAdminSetting(StringAdminSetting setting) { // Check for a policy that overrides this setting. diff --git a/src/AppInstallerCommonCore/Public/winget/AdminSettings.h b/src/AppInstallerCommonCore/Public/winget/AdminSettings.h @@ -38,6 +38,7 @@ namespace AppInstaller::Settings bool DisableAdminSetting(BoolAdminSetting setting); bool SetAdminSetting(StringAdminSetting setting, std::string_view value); bool ResetAdminSetting(StringAdminSetting setting); + void ResetAllAdminSettings(); bool IsAdminSettingEnabled(BoolAdminSetting setting); std::optional<std::string> GetAdminSetting(StringAdminSetting setting);