winget-cli

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

commit 9879f0765c80a2e2819891d6e488a285751b3ffb
parent b1ec74df53ae5593c2dd13cf13fcb0a4af3d1a8f
Author: Luis Chacón <lechacon@users.noreply.github.com>
Date:   Mon, 29 Mar 2021 14:27:38 -0700

Implement Group Policy (#810)


Diffstat:
M.github/actions/spelling/allow.txt | 1+
Msrc/AppInstallerCLICore/Argument.cpp | 9++++++++-
Msrc/AppInstallerCLICore/Argument.h | 47+++++++++++++++++++++++++++++------------------
Msrc/AppInstallerCLICore/Command.cpp | 99++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msrc/AppInstallerCLICore/Command.h | 18+++++++++++++-----
Msrc/AppInstallerCLICore/Commands/FeaturesCommand.cpp | 12++++++++++--
Msrc/AppInstallerCLICore/Commands/InstallCommand.cpp | 2+-
Msrc/AppInstallerCLICore/Commands/RootCommand.cpp | 51++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/AppInstallerCLICore/Commands/RootCommand.h | 2++
Msrc/AppInstallerCLICore/Commands/SettingsCommand.cpp | 30+++++++++++++++++++++++++++++-
Msrc/AppInstallerCLICore/Commands/SettingsCommand.h | 2+-
Msrc/AppInstallerCLICore/Commands/ShowCommand.cpp | 3++-
Msrc/AppInstallerCLICore/Commands/SourceCommand.cpp | 2+-
Msrc/AppInstallerCLICore/Commands/UpgradeCommand.cpp | 2+-
Msrc/AppInstallerCLICore/ExecutionArgs.h | 3++-
Msrc/AppInstallerCLICore/PackageCollection.cpp | 10+++++-----
Msrc/AppInstallerCLICore/PackageCollection.h | 9+++------
Msrc/AppInstallerCLICore/Resources.cpp | 5+----
Msrc/AppInstallerCLICore/Resources.h | 41++++++++++++++++-------------------------
Msrc/AppInstallerCLICore/Workflows/InstallFlow.cpp | 8++++++--
Msrc/AppInstallerCLICore/Workflows/SourceFlow.cpp | 2+-
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/ARPChanges.cpp | 1+
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 4++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 12++++++++++++
Msrc/AppInstallerCLITests/ExperimentalFeature.cpp | 59++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Asrc/AppInstallerCLITests/GroupPolicy.cpp | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/TestCommon.cpp | 3++-
Msrc/AppInstallerCLITests/TestCommon.h | 2+-
Asrc/AppInstallerCLITests/TestSettings.cpp | 37+++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLITests/TestSettings.h | 35+++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/UserSettings.cpp | 67+++++++++++++++++++++++++++++++++++++++++++++----------------------
Asrc/AppInstallerCLITests/WorkflowGroupPolicy.cpp | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj | 3+++
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters | 9+++++++++
Msrc/AppInstallerCommonCore/Errors.cpp | 2++
Msrc/AppInstallerCommonCore/ExperimentalFeature.cpp | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Asrc/AppInstallerCommonCore/GroupPolicy.cpp | 226+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Public/AppInstallerErrors.h | 1+
Msrc/AppInstallerCommonCore/Public/AppInstallerLanguageUtilities.h | 2+-
Msrc/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h | 6++++++
Asrc/AppInstallerCommonCore/Public/winget/GroupPolicy.h | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Public/winget/Registry.h | 17+++++++++++++++--
Asrc/AppInstallerCommonCore/Public/winget/Resources.h | 43+++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Public/winget/UserSettings.h | 37++++++++++++++++++++++++++++++++-----
Msrc/AppInstallerCommonCore/Registry.cpp | 6+++---
Msrc/AppInstallerCommonCore/UserSettings.cpp | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msrc/AppInstallerRepositoryCore/RepositorySource.cpp | 89++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
48 files changed, 1435 insertions(+), 213 deletions(-)

diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -72,6 +72,7 @@ dbconn DBId declspec decltype +declval defaultlocale delstore Demitrius diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -18,7 +18,7 @@ namespace AppInstaller::CLI case Args::Type::Query: return Argument{ "query", 'q', Args::Type::Query, Resource::String::QueryArgumentDescription, ArgumentType::Positional}; case Args::Type::Manifest: - return Argument{ "manifest", 'm', Args::Type::Manifest, Resource::String::ManifestArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }; + return Argument{ "manifest", 'm', Args::Type::Manifest, Resource::String::ManifestArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help, Settings::TogglePolicy::Policy::LocalManifestFiles }; case Args::Type::Id: return Argument{ "id", NoAlias, Args::Type::Id,Resource::String::IdArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }; case Args::Type::Name: @@ -51,6 +51,8 @@ namespace AppInstaller::CLI return Argument{ "override", NoAlias, Args::Type::Override, Resource::String::OverrideArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }; case Args::Type::InstallLocation: return Argument{ "location", 'l', Args::Type::InstallLocation, Resource::String::LocationArgumentDescription, ArgumentType::Standard }; + case Args::Type::HashOverride: + return Argument{ "force", Argument::NoAlias, Args::Type::HashOverride, Resource::String::InstallForceArgumentDescription, ArgumentType::Flag, Settings::TogglePolicy::Policy::HashOverride }; case Args::Type::HashFile: return Argument{ "file", 'f', Args::Type::HashFile, Resource::String::FileArgumentDescription, ArgumentType::Positional, true }; case Args::Type::Msix: @@ -98,6 +100,11 @@ namespace AppInstaller::CLI return Argument::Visibility::Hidden; } + if (!GroupPolicies().IsEnabled(m_groupPolicy)) + { + return Argument::Visibility::Hidden; + } + return m_visibility; } } diff --git a/src/AppInstallerCLICore/Argument.h b/src/AppInstallerCLICore/Argument.h @@ -5,6 +5,7 @@ #include "Resources.h" #include <winget/UserSettings.h> #include <winget/ExperimentalFeature.h> +#include <winget/GroupPolicy.h> #include <string> #include <string_view> @@ -65,24 +66,6 @@ namespace AppInstaller::CLI Argument(std::string_view name, char alias, Execution::Args::Type execArgType, Resource::StringId desc, ArgumentType type, Argument::Visibility visibility, bool required) : m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_type(type), m_visibility(visibility), m_required(required) {} - Argument(std::string_view name, char alias, Execution::Args::Type execArgType, Resource::StringId desc, Settings::ExperimentalFeature::Feature feature) : - m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_feature(feature) {} - - Argument(std::string_view name, char alias, Execution::Args::Type execArgType, Resource::StringId desc, bool required, Settings::ExperimentalFeature::Feature feature) : - m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_required(required), m_feature(feature) {} - - Argument(std::string_view name, char alias, Execution::Args::Type execArgType, Resource::StringId desc, ArgumentType type, Settings::ExperimentalFeature::Feature feature) : - m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_type(type), m_feature(feature) {} - - Argument(std::string_view name, char alias, Execution::Args::Type execArgType, Resource::StringId desc, ArgumentType type, Argument::Visibility visibility, Settings::ExperimentalFeature::Feature feature) : - m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_type(type), m_visibility(visibility), m_feature(feature) {} - - Argument(std::string_view name, char alias, Execution::Args::Type execArgType, Resource::StringId desc, ArgumentType type, bool required, Settings::ExperimentalFeature::Feature feature) : - m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_type(type), m_required(required), m_feature(feature) {} - - Argument(std::string_view name, char alias, Execution::Args::Type execArgType, Resource::StringId desc, ArgumentType type, Argument::Visibility visibility, bool required, Settings::ExperimentalFeature::Feature feature) : - m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_type(type), m_visibility(visibility), m_required(required), m_feature(feature) {} - ~Argument() = default; Argument(const Argument&) = default; @@ -107,10 +90,37 @@ namespace AppInstaller::CLI size_t Limit() const { return m_countLimit; } Argument::Visibility GetVisibility() const; Settings::ExperimentalFeature::Feature Feature() const { return m_feature; } + Settings::TogglePolicy::Policy GroupPolicy() const { return m_groupPolicy; } Argument& SetRequired(bool required) { m_required = required; return *this; } private: + // Constructors that set a Feature or Policy are private to force callers to go through the ForType() function. + // This helps keep it all in one place to reduce chances of missing it somewhere. + Argument(std::string_view name, char alias, Execution::Args::Type execArgType, Resource::StringId desc, Settings::ExperimentalFeature::Feature feature) : + m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_feature(feature) {} + + Argument(std::string_view name, char alias, Execution::Args::Type execArgType, Resource::StringId desc, bool required, Settings::ExperimentalFeature::Feature feature) : + m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_required(required), m_feature(feature) {} + + Argument(std::string_view name, char alias, Execution::Args::Type execArgType, Resource::StringId desc, ArgumentType type, Settings::ExperimentalFeature::Feature feature) : + m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_type(type), m_feature(feature) {} + + Argument(std::string_view name, char alias, Execution::Args::Type execArgType, Resource::StringId desc, ArgumentType type, Argument::Visibility visibility, Settings::ExperimentalFeature::Feature feature) : + m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_type(type), m_visibility(visibility), m_feature(feature) {} + + Argument(std::string_view name, char alias, Execution::Args::Type execArgType, Resource::StringId desc, ArgumentType type, bool required, Settings::ExperimentalFeature::Feature feature) : + m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_type(type), m_required(required), m_feature(feature) {} + + Argument(std::string_view name, char alias, Execution::Args::Type execArgType, Resource::StringId desc, ArgumentType type, Argument::Visibility visibility, bool required, Settings::ExperimentalFeature::Feature feature) : + m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_type(type), m_visibility(visibility), m_required(required), m_feature(feature) {} + + Argument(std::string_view name, char alias, Execution::Args::Type execArgType, Resource::StringId desc, ArgumentType type, Settings::TogglePolicy::Policy groupPolicy) : + m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_type(type), m_groupPolicy(groupPolicy) {} + + Argument(std::string_view name, char alias, Execution::Args::Type execArgType, Resource::StringId desc, ArgumentType type, Argument::Visibility visibility, Settings::TogglePolicy::Policy groupPolicy) : + m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_type(type), m_visibility(visibility), m_groupPolicy(groupPolicy) {} + std::string_view m_name; char m_alias; Execution::Args::Type m_execArgType; @@ -120,5 +130,6 @@ namespace AppInstaller::CLI Argument::Visibility m_visibility = Argument::Visibility::Example; size_t m_countLimit = 1; Settings::ExperimentalFeature::Feature m_feature = Settings::ExperimentalFeature::Feature::None; + Settings::TogglePolicy::Policy m_groupPolicy = Settings::TogglePolicy::Policy::None; }; } diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp @@ -5,16 +5,21 @@ #include "Resources.h" #include <winget/UserSettings.h> +using namespace std::string_view_literals; +using namespace AppInstaller::Utility::literals; +using namespace AppInstaller::Settings; + namespace AppInstaller::CLI { - using namespace std::string_view_literals; - using namespace Utility::literals; - using namespace Settings; - constexpr std::string_view s_Command_ArgName_SilentAndInteractive = "silent|interactive"sv; - Command::Command(std::string_view name, std::string_view parent, Command::Visibility visibility, ExperimentalFeature::Feature feature) : - m_name(name), m_visibility(visibility), m_feature(feature) + Command::Command( + std::string_view name, + std::string_view parent, + Command::Visibility visibility, + Settings::ExperimentalFeature::Feature feature, + Settings::TogglePolicy::Policy groupPolicy) : + m_name(name), m_visibility(visibility), m_feature(feature), m_groupPolicy(groupPolicy) { if (!parent.empty()) { @@ -271,6 +276,13 @@ namespace AppInstaller::CLI throw CommandException(Resource::String::FeatureDisabledMessage, feature.JsonName()); } + if (!Settings::GroupPolicies().IsEnabled(command->GroupPolicy())) + { + auto policy = TogglePolicy::GetPolicy(command->GroupPolicy()); + AICLI_LOG(CLI, Error, << "Trying to use command: " << *itr << " disabled by group policy " << policy.RegValueName()); + throw CommandException(Resource::String::DisabledByGroupPolicy, policy.PolicyName()); + } + AICLI_LOG(CLI, Info, << "Found subcommand: " << *itr); inv.consume(itr); return std::move(command); @@ -587,6 +599,38 @@ namespace AppInstaller::CLI return; } + for (const auto& arg : GetArguments()) + { + if (!ExperimentalFeature::IsEnabled(arg.Feature()) && execArgs.Contains(arg.ExecArgType())) + { + auto feature = ExperimentalFeature::GetFeature(arg.Feature()); + AICLI_LOG(CLI, Error, << "Trying to use argument: " << arg.Name() << " without enabling feature " << feature.JsonName()); + throw CommandException(Resource::String::FeatureDisabledMessage, feature.JsonName()); + } + + if (!Settings::GroupPolicies().IsEnabled(arg.GroupPolicy()) && execArgs.Contains(arg.ExecArgType())) + { + auto policy = TogglePolicy::GetPolicy(arg.GroupPolicy()); + AICLI_LOG(CLI, Error, << "Trying to use argument: " << arg.Name() << " disabled by group policy " << policy.RegValueName()); + throw CommandException(Resource::String::DisabledByGroupPolicy, policy.PolicyName()); + } + + if (arg.Required() && !execArgs.Contains(arg.ExecArgType())) + { + throw CommandException(Resource::String::RequiredArgError, arg.Name()); + } + + if (arg.Limit() < execArgs.GetCount(arg.ExecArgType())) + { + throw CommandException(Resource::String::TooManyArgError, arg.Name()); + } + } + + if (execArgs.Contains(Execution::Args::Type::Silent) && execArgs.Contains(Execution::Args::Type::Interactive)) + { + throw CommandException(Resource::String::TooManyBehaviorsError, s_Command_ArgName_SilentAndInteractive); + } + ValidateArgumentsInternal(execArgs); } @@ -690,6 +734,16 @@ namespace AppInstaller::CLI void Command::Execute(Execution::Context& context) const { + // Block any execution if winget is disabled by policy. + // Override the function to bypass this. + if (!Settings::GroupPolicies().IsEnabled(Settings::TogglePolicy::Policy::WinGet)) + { + auto policy = TogglePolicy::GetPolicy(Settings::TogglePolicy::Policy::WinGet); + AICLI_LOG(CLI, Error, << "WinGet is disabled by group policy " << policy.RegValueName()); + context.Reporter.Error() << Resource::String::DisabledByGroupPolicy << " : "_liv << policy.PolicyName() << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY); + } + AICLI_LOG(CLI, Info, << "Executing command: " << Name()); if (context.Args.Contains(Execution::Args::Type::Help)) { @@ -701,32 +755,10 @@ namespace AppInstaller::CLI } } - void Command::ValidateArgumentsInternal(Execution::Args& execArgs) const + void Command::ValidateArgumentsInternal(Execution::Args&) const { - for (const auto& arg : GetArguments()) - { - if (!ExperimentalFeature::IsEnabled(arg.Feature()) && execArgs.Contains(arg.ExecArgType())) - { - auto feature = ExperimentalFeature::GetFeature(arg.Feature()); - AICLI_LOG(CLI, Error, << "Trying to use argument: " << arg.Name() << " without enabling feature " << feature.JsonName()); - throw CommandException(Resource::String::FeatureDisabledMessage, feature.JsonName()); - } - - if (arg.Required() && !execArgs.Contains(arg.ExecArgType())) - { - throw CommandException(Resource::String::RequiredArgError, arg.Name()); - } - - if (arg.Limit() < execArgs.GetCount(arg.ExecArgType())) - { - throw CommandException(Resource::String::TooManyArgError, arg.Name()); - } - } - - if (execArgs.Contains(Execution::Args::Type::Silent) && execArgs.Contains(Execution::Args::Type::Interactive)) - { - throw CommandException(Resource::String::TooManyBehaviorsError, s_Command_ArgName_SilentAndInteractive); - } + // Do nothing by default. + // Commands may not need any extra validation. } void Command::ExecuteInternal(Execution::Context& context) const @@ -742,6 +774,11 @@ namespace AppInstaller::CLI return Command::Visibility::Hidden; } + if (!Settings::GroupPolicies().IsEnabled(m_groupPolicy)) + { + return Command::Visibility::Hidden; + } + return m_visibility; } diff --git a/src/AppInstallerCLICore/Command.h b/src/AppInstallerCLICore/Command.h @@ -7,6 +7,7 @@ #include "Resources.h" #include <winget/UserSettings.h> #include <winget/ExperimentalFeature.h> +#include <winget/GroupPolicy.h> #include <initializer_list> #include <memory> @@ -16,17 +17,18 @@ #include <type_traits> #include <vector> - namespace AppInstaller::CLI { struct CommandException { - // The message should be a localized string, but the parameters are currently not localized. - // We 'convert' the param to a localization independent view here. + // The message should be a localized string. + // The parameters can be either localized or not. + // We 'convert' the param to a localization independent view here if needed. + CommandException(Resource::LocString message, Resource::LocString param) : m_message(std::move(message)), m_param(param) {} CommandException(Resource::LocString message, std::string_view param) : m_message(std::move(message)), m_param(param) {} const Resource::LocString& Message() const { return m_message; } - const Utility::LocIndString Param() const { return m_param; } + const Utility::LocIndString& Param() const { return m_param; } private: Resource::LocString m_message; @@ -50,7 +52,11 @@ namespace AppInstaller::CLI Command(name, parent, visibility, Settings::ExperimentalFeature::Feature::None) {} Command(std::string_view name, std::string_view parent, Settings::ExperimentalFeature::Feature feature) : Command(name, parent, Command::Visibility::Show, feature) {} - Command(std::string_view name, std::string_view parent, Command::Visibility visibility, Settings::ExperimentalFeature::Feature feature); + Command(std::string_view name, std::string_view parent, Settings::TogglePolicy::Policy groupPolicy) : + Command(name, parent, Command::Visibility::Show, Settings::ExperimentalFeature::Feature::None, groupPolicy) {} + Command(std::string_view name, std::string_view parent, Command::Visibility visibility, Settings::ExperimentalFeature::Feature feature) : + Command(name, parent, visibility, feature, Settings::TogglePolicy::Policy::None) {} + Command(std::string_view name, std::string_view parent, Command::Visibility visibility, Settings::ExperimentalFeature::Feature feature, Settings::TogglePolicy::Policy groupPolicy); virtual ~Command() = default; Command(const Command&) = default; @@ -66,6 +72,7 @@ namespace AppInstaller::CLI const std::string& FullName() const { return m_fullName; } Command::Visibility GetVisibility() const; Settings::ExperimentalFeature::Feature Feature() const { return m_feature; } + Settings::TogglePolicy::Policy GroupPolicy() const { return m_groupPolicy; } virtual std::vector<std::unique_ptr<Command>> GetCommands() const { return {}; } virtual std::vector<Argument> GetArguments() const { return {}; } @@ -97,6 +104,7 @@ namespace AppInstaller::CLI std::string m_fullName; Command::Visibility m_visibility; Settings::ExperimentalFeature::Feature m_feature; + Settings::TogglePolicy::Policy m_groupPolicy; }; template <typename Container> diff --git a/src/AppInstallerCLICore/Commands/FeaturesCommand.cpp b/src/AppInstallerCLICore/Commands/FeaturesCommand.cpp @@ -27,10 +27,18 @@ namespace AppInstaller::CLI void FeaturesCommand::ExecuteInternal(Execution::Context& context) const { - context.Reporter.Info() << Resource::String::FeaturesMessage << std::endl << std::endl; + if (GroupPolicies().IsEnabled(TogglePolicy::Policy::ExperimentalFeatures) && + GroupPolicies().IsEnabled(TogglePolicy::Policy::Settings)) + { + context.Reporter.Info() << Resource::String::FeaturesMessage << std::endl << std::endl; + } + else + { + context.Reporter.Info() << Resource::String::FeaturesMessageDisabledByPolicy << std::endl << std::endl; + } auto features = ExperimentalFeature::GetAllFeatures(); - + if (!features.empty()) { Execution::TableOutput<4> table(context.Reporter, { diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -31,7 +31,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Log), Argument::ForType(Args::Type::Override), Argument::ForType(Args::Type::InstallLocation), - Argument{ "force", Argument::NoAlias, Args::Type::Force, Resource::String::InstallForceArgumentDescription, ArgumentType::Flag }, + Argument::ForType(Args::Type::HashOverride), }; } diff --git a/src/AppInstallerCLICore/Commands/RootCommand.cpp b/src/AppInstallerCLICore/Commands/RootCommand.cpp @@ -22,9 +22,43 @@ #include "Resources.h" #include "TableOutput.h" +using namespace AppInstaller::Utility::literals; + namespace AppInstaller::CLI { - using namespace Utility::literals; + namespace + { + void OutputGroupPolicies(Execution::Context& context) + { + const auto& groupPolicies = Settings::GroupPolicies(); + + std::map<Settings::TogglePolicy::Policy, Settings::PolicyState> activePolicies; + for (const auto& togglePolicy : Settings::TogglePolicy::GetAllPolicies()) + { + auto state = groupPolicies.GetState(togglePolicy.GetPolicy()); + if (state != Settings::PolicyState::NotConfigured) + { + activePolicies[togglePolicy.GetPolicy()] = state; + } + } + + if (!activePolicies.empty()) + { + context.Reporter.Info() << std::endl; + + Execution::TableOutput<2> policiesTable{ context.Reporter, { Resource::String::PoliciesPolicy, Resource::String::PoliciesState } }; + for (const auto& activePolicy : activePolicies) + { + auto policy = Settings::TogglePolicy::GetPolicy(activePolicy.first); + policiesTable.OutputLine({ + Resource::LocString{ policy.PolicyName() }.get(), + Resource::LocString{ activePolicy.second == Settings::PolicyState::Enabled ? Resource::String::PoliciesEnabled : Resource::String::PoliciesDisabled }.get() }); + } + + policiesTable.Complete(); + } + } + } std::vector<std::unique_ptr<Command>> RootCommand::GetCommands() const { @@ -66,6 +100,19 @@ namespace AppInstaller::CLI return "https://aka.ms/winget-command-help"; } + void RootCommand::Execute(Execution::Context& context) const + { + AICLI_LOG(CLI, Info, << "Executing command: " << Name()); + if (context.Args.Contains(Execution::Args::Type::Help)) + { + OutputHelp(context.Reporter); + } + else + { + ExecuteInternal(context); + } + } + void RootCommand::ExecuteInternal(Execution::Context& context) const { if (context.Args.Contains(Execution::Args::Type::Info)) @@ -94,6 +141,8 @@ namespace AppInstaller::CLI links.OutputLine({ Resource::LocString(Resource::String::MainHomepage).get(), "https://aka.ms/winget" }); links.Complete(); + + OutputGroupPolicies(context); } else if (context.Args.Contains(Execution::Args::Type::ListVersions)) { diff --git a/src/AppInstallerCLICore/Commands/RootCommand.h b/src/AppInstallerCLICore/Commands/RootCommand.h @@ -16,6 +16,8 @@ namespace AppInstaller::CLI std::string HelpLink() const override; + void Execute(Execution::Context& context) const override; + protected: virtual void ExecuteInternal(Execution::Context& context) const; }; diff --git a/src/AppInstallerCLICore/Commands/SettingsCommand.cpp b/src/AppInstallerCLICore/Commands/SettingsCommand.cpp @@ -36,7 +36,35 @@ namespace AppInstaller::CLI context.Reporter.Warn() << Resource::String::SettingLoadFailure << std::endl; for (const auto& warning : User().GetWarnings()) { - context.Reporter.Warn() << warning << std::endl; + auto warn = context.Reporter.Warn(); + warn << warning.Message; + if (!warning.Path.empty()) + { + if (warning.IsFieldWarning) + { + warn << ' ' << Resource::String::SettingsWarningField << ' ' << warning.Path; + } + else + { + warn << ' ' << warning.Path; + } + } + + if (!warning.Data.empty()) + { + if (warning.IsFieldWarning) + { + warn << ' ' << Resource::String::SettingsWarningValue << ' ' << warning.Data; + } + else + { + warn << + std::endl << + warning.Data; + } + } + + warn << std::endl; } } diff --git a/src/AppInstallerCLICore/Commands/SettingsCommand.h b/src/AppInstallerCLICore/Commands/SettingsCommand.h @@ -7,7 +7,7 @@ namespace AppInstaller::CLI { struct SettingsCommand final : public Command { - SettingsCommand(std::string_view parent) : Command("settings", parent) {} + SettingsCommand(std::string_view parent) : Command("settings", parent, Settings::TogglePolicy::Policy::Settings) {} virtual Resource::LocString ShortDescription() const override; virtual Resource::LocString LongDescription() const override; diff --git a/src/AppInstallerCLICore/Commands/ShowCommand.cpp b/src/AppInstallerCLICore/Commands/ShowCommand.cpp @@ -13,7 +13,8 @@ namespace AppInstaller::CLI { return { Argument::ForType(Execution::Args::Type::Query), - Argument::ForType(Execution::Args::Type::Manifest), + // The manifest argument from Argument::ForType can be blocked by Group Policy but we don't want that here + Argument{ "manifest", 'm', Execution::Args::Type::Manifest, Resource::String::ManifestArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, Argument::ForType(Execution::Args::Type::Id), Argument::ForType(Execution::Args::Type::Name), Argument::ForType(Execution::Args::Type::Moniker), diff --git a/src/AppInstallerCLICore/Commands/SourceCommand.cpp b/src/AppInstallerCLICore/Commands/SourceCommand.cpp @@ -197,7 +197,7 @@ namespace AppInstaller::CLI { return { Argument::ForType(Args::Type::SourceName), - Argument{ "force", Argument::NoAlias, Args::Type::Force, Resource::String::SourceResetForceArgumentDescription, ArgumentType::Flag }, + Argument{ "force", Argument::NoAlias, Args::Type::ForceSourceReset, Resource::String::SourceResetForceArgumentDescription, ArgumentType::Flag }, }; } diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -41,7 +41,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Log), Argument::ForType(Args::Type::Override), Argument::ForType(Args::Type::InstallLocation), - Argument{ "force", Argument::NoAlias, Args::Type::Force, Resource::String::InstallForceArgumentDescription, ArgumentType::Flag }, + Argument::ForType(Args::Type::HashOverride), Argument{ "all", Argument::NoAlias, Args::Type::All, Resource::String::UpdateAllArgumentDescription, ArgumentType::Flag }, }; } diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -37,11 +37,13 @@ namespace AppInstaller::CLI::Execution Log, Override, //Override args are (and the only args) directly passed to installer InstallLocation, + HashOverride, // Ignore hash mismatches //Source Command SourceName, SourceType, SourceArg, + ForceSourceReset, //Hash Command HashFile, @@ -66,7 +68,6 @@ namespace AppInstaller::CLI::Execution // Other All, // Used in Update command to update all installed packages to latest - Force, // Generic flag to enable a command to skip some check ListVersions, // Used in Show command to list all available versions of an app NoVT, // Disable VirtualTerminal outputs RetroStyle, // Makes progress display as retro diff --git a/src/AppInstallerCLICore/PackageCollection.cpp b/src/AppInstallerCLICore/PackageCollection.cpp @@ -12,10 +12,10 @@ #include <algorithm> #include <ostream> +using namespace AppInstaller::Repository; + namespace AppInstaller::CLI { - using namespace AppInstaller::Repository; - namespace { // Strings used in the Packages JSON file. @@ -165,9 +165,9 @@ namespace AppInstaller::CLI { AICLI_LOG(CLI, Error, << "Import file is missing \"" << s_PackagesJson_Schema << "\" property"); return ParseResult{ ParseResult::Type::MissingSchema }; - } - - const auto& schemaUri = root[s_PackagesJson_Schema].asString(); + } + + const auto& schemaUri = root[s_PackagesJson_Schema].asString(); Json::Value schemaJson; if (schemaUri == s_PackagesJson_SchemaUri_v1_0) { diff --git a/src/AppInstallerCLICore/PackageCollection.h b/src/AppInstallerCLICore/PackageCollection.h @@ -3,7 +3,6 @@ #pragma once #include "AppInstallerDateTime.h" -#include "AppInstallerLanguageUtilities.h" #include "AppInstallerRepositorySource.h" #include <json.h> @@ -12,8 +11,6 @@ namespace AppInstaller::CLI { - using namespace AppInstaller::Repository; - // Container for data to identify multiple packages to be installed from multiple sources. struct PackageCollection { @@ -37,10 +34,10 @@ namespace AppInstaller::CLI struct Source { Source() = default; - Source(const SourceDetails& sourceDetails) : Details(sourceDetails) {} - Source(SourceDetails&& sourceDetails) : Details(std::move(sourceDetails)) {} + Source(const Repository::SourceDetails& sourceDetails) : Details(sourceDetails) {} + Source(Repository::SourceDetails&& sourceDetails) : Details(std::move(sourceDetails)) {} - SourceDetails Details; + Repository::SourceDetails Details; std::vector<Package> Packages; }; diff --git a/src/AppInstallerCLICore/Resources.cpp b/src/AppInstallerCLICore/Resources.cpp @@ -7,10 +7,7 @@ namespace AppInstaller::CLI::Resource { - LocString::LocString(StringId id) - { - m_value = Loader::Instance().ResolveString(id); - } + LocString::LocString(StringId id) : Utility::LocIndString(Loader::Instance().ResolveString(id)) {} const Loader& Loader::Instance() { diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -1,30 +1,22 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include <winget/LocIndependent.h> +#include <winget/Resources.h> + #include <winrt/Windows.ApplicationModel.Resources.h> #include <iostream> -#include <string> - namespace AppInstaller::CLI::Resource { - using namespace std::string_view_literals; - -#define WINGET_WIDE_STRINGIFY_HELP(_id_) L ## _id_ -#define WINGET_WIDE_STRINGIFY(_id_) WINGET_WIDE_STRINGIFY_HELP(_id_) -#define WINGET_DEFINE_RESOURCE_STRINGID(_id_) static constexpr StringId _id_ { WINGET_WIDE_STRINGIFY(#_id_) ## sv } - - // A resource identifier - struct StringId : public std::wstring_view - { - explicit constexpr StringId(std::wstring_view id) : std::wstring_view(id) {} - }; + using AppInstaller::StringResource::StringId; // Resource string identifiers. - // This list can easily be generated by the following PowerShell: + // This list can mostly be generated by the following PowerShell: // > [xml]$res = Get-Content <winget.resw file path> // > $res.root.data.name | % { "WINGET_DEFINE_RESOURCE_STRINGID($_);" } + // struct String { WINGET_DEFINE_RESOURCE_STRINGID(AdjoinedNotFlagError); @@ -43,6 +35,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(CompleteCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(CompleteCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(CountArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(DisabledByGroupPolicy); WINGET_DEFINE_RESOURCE_STRINGID(Done); WINGET_DEFINE_RESOURCE_STRINGID(ExactArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalArgumentDescription); @@ -61,6 +54,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(FeaturesFeature); WINGET_DEFINE_RESOURCE_STRINGID(FeaturesLink); WINGET_DEFINE_RESOURCE_STRINGID(FeaturesMessage); + WINGET_DEFINE_RESOURCE_STRINGID(FeaturesMessageDisabledByPolicy); WINGET_DEFINE_RESOURCE_STRINGID(FeaturesProperty); WINGET_DEFINE_RESOURCE_STRINGID(FeaturesStatus); WINGET_DEFINE_RESOURCE_STRINGID(FileArgumentDescription); @@ -94,6 +88,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InstallerFailedSecurityCheck); WINGET_DEFINE_RESOURCE_STRINGID(InstallerFailedVirusScan); WINGET_DEFINE_RESOURCE_STRINGID(InstallerHashMismatchAdminBlock); + WINGET_DEFINE_RESOURCE_STRINGID(InstallerHashMismatchError); WINGET_DEFINE_RESOURCE_STRINGID(InstallerHashMismatchOverridden); WINGET_DEFINE_RESOURCE_STRINGID(InstallerHashMismatchOverrideRequired); WINGET_DEFINE_RESOURCE_STRINGID(InstallerHashVerified); @@ -149,6 +144,10 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(OverrideArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(Package); WINGET_DEFINE_RESOURCE_STRINGID(PendingWorkError); + WINGET_DEFINE_RESOURCE_STRINGID(PoliciesDisabled); + WINGET_DEFINE_RESOURCE_STRINGID(PoliciesEnabled); + WINGET_DEFINE_RESOURCE_STRINGID(PoliciesPolicy); + WINGET_DEFINE_RESOURCE_STRINGID(PoliciesState); WINGET_DEFINE_RESOURCE_STRINGID(PositionArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(PreviewVersion); WINGET_DEFINE_RESOURCE_STRINGID(PrivacyStatement); @@ -169,6 +168,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SettingsCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SettingsCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarnings); + WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningField); + WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningValue); WINGET_DEFINE_RESOURCE_STRINGID(ShowChannel); WINGET_DEFINE_RESOURCE_STRINGID(ShowCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ShowCommandShortDescription); @@ -251,7 +252,7 @@ namespace AppInstaller::CLI::Resource }; // A localized string - struct LocString + struct LocString : public Utility::LocIndString { LocString() = default; @@ -262,11 +263,6 @@ namespace AppInstaller::CLI::Resource LocString(LocString&&) = default; LocString& operator=(LocString&&) = default; - - const std::string& get() const { return m_value; } - - private: - std::string m_value; }; // Utility class to load resources @@ -286,11 +282,6 @@ namespace AppInstaller::CLI::Resource }; } -inline std::ostream& operator<<(std::ostream& out, const AppInstaller::CLI::Resource::LocString& ls) -{ - return (out << ls.get()); -} - inline std::ostream& operator<<(std::ostream& out, AppInstaller::CLI::Resource::StringId si) { return (out << AppInstaller::CLI::Resource::LocString{ si }); diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -199,7 +199,7 @@ namespace AppInstaller::CLI::Workflow hashPair.first.end(), hashPair.second.begin())) { - bool overrideHashMismatch = context.Args.Contains(Execution::Args::Type::Force); + bool overrideHashMismatch = context.Args.Contains(Execution::Args::Type::HashOverride); const auto& manifest = context.Get<Execution::Data::Manifest>(); Logging::Telemetry().LogInstallerHashMismatch(manifest.Id, manifest.Version, manifest.Channel, hashPair.first, hashPair.second, overrideHashMismatch); @@ -214,10 +214,14 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Warn() << Resource::String::InstallerHashMismatchOverridden << std::endl; return; } - else + else if (Settings::GroupPolicies().IsEnabled(Settings::TogglePolicy::Policy::HashOverride)) { context.Reporter.Error() << Resource::String::InstallerHashMismatchOverrideRequired << std::endl; } + else + { + context.Reporter.Error() << Resource::String::InstallerHashMismatchError << std::endl; + } AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALLER_HASH_MISMATCH); } diff --git a/src/AppInstallerCLICore/Workflows/SourceFlow.cpp b/src/AppInstallerCLICore/Workflows/SourceFlow.cpp @@ -182,7 +182,7 @@ namespace AppInstaller::CLI::Workflow void QueryUserForSourceReset(Execution::Context& context) { - if (!context.Args.Contains(Execution::Args::Type::Force)) + if (!context.Args.Contains(Execution::Args::Type::ForceSourceReset)) { context << GetSourceListWithFilter; const std::vector<Repository::SourceDetails>& sources = context.Get<Data::SourceList>(); diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -817,4 +817,81 @@ They can be configured through the settings file 'winget settings'.</value> <data name="ImportFileHasInvalidSchema" xml:space="preserve"> <value>The JSON file does not specify a recognized schema.</value> </data> + <data name="DisabledByGroupPolicy" xml:space="preserve"> + <value>This operation is disabled by Group Policy</value> + </data> + <data name="PolicyAdditionalSources" xml:space="preserve"> + <value>Enable Additional Windows App Installer Sources</value> + </data> + <data name="PolicyAllowedSources" xml:space="preserve"> + <value>Enable Windows App Installer Allowed Sources</value> + </data> + <data name="PolicyEnableDefaultSource" xml:space="preserve"> + <value>Enable Windows App Installer Default Source</value> + </data> + <data name="PolicyEnableExperimentalFeatures" xml:space="preserve"> + <value>Enable Windows App Installer Experimental Features</value> + </data> + <data name="PolicyEnableMSStoreSource" xml:space="preserve"> + <value>Enable Windows App Installer Microsoft Store Source</value> + </data> + <data name="PolicyEnableWinGetSettings" xml:space="preserve"> + <value>Enable Windows Package Manager Settings</value> + </data> + <data name="PolicyEnableWinGet" xml:space="preserve"> + <value>Enable Windows Package Manager</value> + </data> + <data name="PolicySourceAutoUpdateInterval" xml:space="preserve"> + <value>Set Windows Package Manager Source Auto Update Interval In Minutes</value> + </data> + <data name="PoliciesDisabled" xml:space="preserve"> + <value>Disabled</value> + <comment>As in enabled/disabled</comment> + </data> + <data name="PoliciesEnabled" xml:space="preserve"> + <value>Enabled</value> + <comment>As in enabled/disabled</comment> + </data> + <data name="PoliciesPolicy" xml:space="preserve"> + <value>Group Policy</value> + <comment>Header for a table listing active Group Policies</comment> + </data> + <data name="PoliciesState" xml:space="preserve"> + <value>State</value> + <comment>Header for a table listing the state (enabled/disabled) of Group Policies</comment> + </data> + <data name="PolicyEnableLocalManifests" xml:space="preserve"> + <value>Enable Windows App Installer Local Manifest Files</value> + </data> + <data name="SettingsWarningField" xml:space="preserve"> + <value>Field:</value> + </data> + <data name="SettingsWarningInvalidFieldFormat" xml:space="preserve"> + <value>Invalid field format.</value> + </data> + <data name="SettingsWarningInvalidFieldValue" xml:space="preserve"> + <value>Invalid field value.</value> + </data> + <data name="SettingsWarningInvalidValueFromPolicy" xml:space="preserve"> + <value>Invalid setting from Group Policy.</value> + </data> + <data name="SettingsWarningLoadedBackupSettings" xml:space="preserve"> + <value>Loaded settings from backup file.</value> + </data> + <data name="SettingsWarningParseError" xml:space="preserve"> + <value>Error parsing file:</value> + </data> + <data name="SettingsWarningValue" xml:space="preserve"> + <value>Value:</value> + </data> + <data name="FeaturesMessageDisabledByPolicy" xml:space="preserve"> + <value>The following experimental features are in progress. +Configuration is disabled due to Group Policy.</value> + </data> + <data name="InstallerHashMismatchError" xml:space="preserve"> + <value>Installer hash does not match.</value> + </data> + <data name="PolicyEnableHashOverride" xml:space="preserve"> + <value>Enable Windows App Installer Hash Override</value> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/ARPChanges.cpp b/src/AppInstallerCLITests/ARPChanges.cpp @@ -15,6 +15,7 @@ using namespace AppInstaller::CLI; using namespace AppInstaller::CLI::Execution; using namespace AppInstaller::CLI::Workflow; using namespace AppInstaller::Logging; +using namespace AppInstaller::Repository; struct TestTelemetry : public TelemetryTraceLogger { diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -177,6 +177,7 @@ <ClInclude Include="pch.h" /> <ClInclude Include="TestCommon.h" /> <ClInclude Include="TestHooks.h" /> + <ClInclude Include="TestSettings.h" /> <ClInclude Include="TestSource.h" /> </ItemGroup> <ItemGroup> @@ -186,6 +187,7 @@ <ClCompile Include="CompositeSource.cpp" /> <ClCompile Include="Downloader.cpp" /> <ClCompile Include="ExperimentalFeature.cpp" /> + <ClCompile Include="GroupPolicy.cpp" /> <ClCompile Include="HashCommand.cpp" /> <ClCompile Include="MsixInfo.cpp" /> <ClCompile Include="NameNormalization.cpp" /> @@ -196,6 +198,7 @@ <ClCompile Include="Registry.cpp" /> <ClCompile Include="SQLiteIndexSource.cpp" /> <ClCompile Include="Strings.cpp" /> + <ClCompile Include="TestSettings.cpp" /> <ClCompile Include="TestSource.cpp" /> <ClCompile Include="UserSettings.cpp" /> <ClCompile Include="Versions.cpp" /> @@ -216,6 +219,7 @@ <ClCompile Include="SQLiteWrapper.cpp" /> <ClCompile Include="Synchronization.cpp" /> <ClCompile Include="TestCommon.cpp" /> + <ClCompile Include="WorkflowGroupPolicy.cpp" /> <ClCompile Include="YamlManifest.cpp" /> </ItemGroup> <ItemGroup> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -33,6 +33,9 @@ <ClInclude Include="TestSource.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="TestSettings.h"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -125,6 +128,15 @@ <ClCompile Include="ARPChanges.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="GroupPolicy.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="WorkflowGroupPolicy.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="TestSettings.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLITests/ExperimentalFeature.cpp b/src/AppInstallerCLITests/ExperimentalFeature.cpp @@ -2,17 +2,74 @@ // Licensed under the MIT License. #include "pch.h" #include "TestCommon.h" +#include "TestSettings.h" #include <winget/ExperimentalFeature.h> +#include <winget/Settings.h> #include <AppInstallerErrors.h> using namespace AppInstaller::Settings; +using namespace TestCommon; TEST_CASE("ExperimentalFeature None", "[experimentalFeature]") { - // Make sure Feature::None is always enable. + // Make sure Feature::None is always enabled. REQUIRE(ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::None)); // Make sure to throw requesting Feature::None REQUIRE_THROWS_HR(ExperimentalFeature::GetFeature(ExperimentalFeature::Feature::None), E_UNEXPECTED); + + // Make sure Feature::None is not disabled by Group Policy + auto policiesKey = RegCreateVolatileTestRoot(); + SetRegistryValue(policiesKey.get(), ExperimentalFeaturesPolicyValueName, false); + GroupPolicyTestOverride policies{ policiesKey.get() }; + REQUIRE(ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::None)); +} + +TEST_CASE("ExperimentalFeature ExperimentalCmd", "[experimentalFeature]") +{ + DeleteUserSettingsFiles(); + + SECTION("Feature off default") + { + UserSettingsTest userSettingTest; + + REQUIRE_FALSE(ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::ExperimentalCmd, userSettingTest)); + } + SECTION("Feature on") + { + std::string_view json = R"({ "experimentalFeatures": { "experimentalCmd": true } })"; + SetSetting(Streams::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::ExperimentalCmd, userSettingTest)); + } + SECTION("Feature off") + { + std::string_view json = R"({ "experimentalFeatures": { "experimentalCmd": false } })"; + SetSetting(Streams::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE_FALSE(ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::ExperimentalCmd, userSettingTest)); + } + SECTION("Invalid value") + { + std::string_view json = R"({ "experimentalFeatures": { "experimentalCmd": "string" } })"; + SetSetting(Streams::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE_FALSE(ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::ExperimentalCmd, userSettingTest)); + } + SECTION("Disabled by group policy") + { + auto policiesKey = RegCreateVolatileTestRoot(); + SetRegistryValue(policiesKey.get(), ExperimentalFeaturesPolicyValueName, false); + GroupPolicyTestOverride policies{ policiesKey.get() }; + + std::string_view json = R"({ "experimentalFeatures": { "experimentalCmd": true } })"; + SetSetting(Streams::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE_FALSE(ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::ExperimentalCmd, userSettingTest)); + } } \ No newline at end of file diff --git a/src/AppInstallerCLITests/GroupPolicy.cpp b/src/AppInstallerCLITests/GroupPolicy.cpp @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "TestSettings.h" +#include "winget/GroupPolicy.h" + +using namespace TestCommon; +using namespace AppInstaller::Settings; +using namespace std::string_view_literals; + +TEST_CASE("GroupPolicy_NoPolicies", "[groupPolicy]") +{ + auto policiesKey = RegCreateVolatileTestRoot(); + GroupPolicy groupPolicy{ policiesKey.get() }; + + // Policies setting a value should be empty + REQUIRE(!groupPolicy.GetValue<ValuePolicy::SourceAutoUpdateIntervalInMinutes>().has_value()); + REQUIRE(!groupPolicy.GetValue<ValuePolicy::AdditionalSources>().has_value()); + REQUIRE(!groupPolicy.GetValue<ValuePolicy::AllowedSources>().has_value()); + + // Everything should be not configured + for (const auto& policy : TogglePolicy::GetAllPolicies()) + { + REQUIRE(groupPolicy.GetState(policy.GetPolicy()) == PolicyState::NotConfigured); + } +} + +TEST_CASE("GroupPolicy_UpdateInterval", "[groupPolicy]") +{ + auto policiesKey = RegCreateVolatileTestRoot(); + + SECTION("Good value") + { + SetRegistryValue(policiesKey.get(), SourceUpdateIntervalPolicyValueName, 5); + GroupPolicy groupPolicy{ policiesKey.get() }; + + auto policy = groupPolicy.GetValue<ValuePolicy::SourceAutoUpdateIntervalInMinutes>(); + REQUIRE(policy.has_value()); + REQUIRE(*policy == 5); + } + + SECTION("Wrong type") + { + SetRegistryValue(policiesKey.get(), SourceUpdateIntervalPolicyValueName, L"Wrong"); + GroupPolicy groupPolicy{ policiesKey.get() }; + + auto policy = groupPolicy.GetValue<ValuePolicy::SourceAutoUpdateIntervalInMinutes>(); + REQUIRE(!policy.has_value()); + } +} + +// TODO: additional/allowed sources + +TEST_CASE("GroupPolicy_Toggle", "[groupPolicy]") +{ + auto policiesKey = RegCreateVolatileTestRoot(); + + SECTION("'None' is not configured") + { + GroupPolicy groupPolicy{ policiesKey.get() }; + REQUIRE(groupPolicy.GetState(TogglePolicy::Policy::None) == PolicyState::NotConfigured); + REQUIRE(groupPolicy.IsEnabled(TogglePolicy::Policy::None)); + } + + SECTION("Enabled") + { + SetRegistryValue(policiesKey.get(), WinGetPolicyValueName, 1); + GroupPolicy groupPolicy{ policiesKey.get() }; + REQUIRE(groupPolicy.GetState(TogglePolicy::Policy::WinGet) == PolicyState::Enabled); + REQUIRE(groupPolicy.IsEnabled(TogglePolicy::Policy::WinGet)); + } + + SECTION("Disabled") + { + SetRegistryValue(policiesKey.get(), LocalManifestsPolicyValueName, 0); + GroupPolicy groupPolicy{ policiesKey.get() }; + REQUIRE(groupPolicy.GetState(TogglePolicy::Policy::LocalManifestFiles) == PolicyState::Disabled); + REQUIRE_FALSE(groupPolicy.IsEnabled(TogglePolicy::Policy::LocalManifestFiles)); + } + + SECTION("Wrong type") + { + SetRegistryValue(policiesKey.get(), ExperimentalFeaturesPolicyValueName, L"Wrong"); + GroupPolicy groupPolicy{ policiesKey.get() }; + REQUIRE(groupPolicy.GetState(TogglePolicy::Policy::DefaultSource) == PolicyState::NotConfigured); + REQUIRE(groupPolicy.IsEnabled(TogglePolicy::Policy::DefaultSource)); + } +} + +TEST_CASE("GroupPolicy_AllDisabled", "[groupPolicy]") +{ + auto policiesKey = RegCreateVolatileTestRoot(); + SetRegistryValue(policiesKey.get(), WinGetPolicyValueName, 1); + SetRegistryValue(policiesKey.get(), WinGetSettingsPolicyValueName, 1); + SetRegistryValue(policiesKey.get(), ExperimentalFeaturesPolicyValueName, 1); + SetRegistryValue(policiesKey.get(), LocalManifestsPolicyValueName, 1); + SetRegistryValue(policiesKey.get(), EnableHashOverridePolicyValueName, 1); + SetRegistryValue(policiesKey.get(), DefaultSourcePolicyValueName, 1); + SetRegistryValue(policiesKey.get(), MSStoreSourcePolicyValueName, 1);; + SetRegistryValue(policiesKey.get(), AdditionalSourcesPolicyValueName, 1); + SetRegistryValue(policiesKey.get(), AllowedSourcesPolicyValueName, 1); + + GroupPolicy groupPolicy{ policiesKey.get() }; + for (const auto& policy : TogglePolicy::GetAllPolicies()) + { + REQUIRE(groupPolicy.GetState(policy.GetPolicy()) == PolicyState::Enabled); + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestCommon.cpp b/src/AppInstallerCLITests/TestCommon.cpp @@ -2,6 +2,8 @@ // Licensed under the MIT License. #include "pch.h" #include "TestCommon.h" +#include "winget/GroupPolicy.h" +#include "winget/UserSettings.h" namespace TestCommon { @@ -194,7 +196,6 @@ namespace TestCommon void SetRegistryValue(HKEY key, const std::wstring& name, DWORD value) { - THROW_IF_WIN32_ERROR(RegSetValueExW(key, name.c_str(), 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(DWORD))); } } diff --git a/src/AppInstallerCLITests/TestCommon.h b/src/AppInstallerCLITests/TestCommon.h @@ -103,6 +103,7 @@ namespace TestCommon std::function<void(uint64_t, uint64_t, AppInstaller::ProgressType)> m_OnProgress; }; + // Creates a volatile key for testing. wil::unique_hkey RegCreateVolatileTestRoot(); @@ -113,5 +114,4 @@ namespace TestCommon void SetRegistryValue(HKEY key, const std::wstring& name, const std::wstring& value, DWORD type = REG_SZ); void SetRegistryValue(HKEY key, const std::wstring& name, const std::vector<BYTE>& value, DWORD type = REG_BINARY); void SetRegistryValue(HKEY key, const std::wstring& name, DWORD value); - } diff --git a/src/AppInstallerCLITests/TestSettings.cpp b/src/AppInstallerCLITests/TestSettings.cpp @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "TestSettings.h" +#include <winget/Settings.h> + +using namespace AppInstaller::Settings; + +namespace TestCommon +{ + void DeleteUserSettingsFiles() + { + auto settingsPath = UserSettings::SettingsFilePath(); + if (std::filesystem::exists(settingsPath)) + { + std::filesystem::remove(settingsPath); + } + + auto settingsBackupPath = GetPathTo(Streams::BackupUserSettings); + if (std::filesystem::exists(settingsBackupPath)) + { + std::filesystem::remove(settingsBackupPath); + } + } + + GroupPolicyTestOverride::GroupPolicyTestOverride(const AppInstaller::Registry::Key& key) : GroupPolicy(key) + { + AppInstaller::Settings::GroupPolicy::OverrideInstance(this); + } + + GroupPolicyTestOverride::~GroupPolicyTestOverride() + { + AppInstaller::Settings::GroupPolicy::ResetInstance(); + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestSettings.h b/src/AppInstallerCLITests/TestSettings.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <winget/UserSettings.h> +#include <wil/resource.h> +#include <string> + +namespace TestCommon +{ + // Repeat the policy values here so we can catch unintended changes in the source. + const std::wstring WinGetPolicyValueName = L"EnableWindowsPackageManager"; + const std::wstring WinGetSettingsPolicyValueName = L"EnableWindowsPackageManagerSettings"; + const std::wstring ExperimentalFeaturesPolicyValueName = L"EnableExperimentalFeatures"; + const std::wstring LocalManifestsPolicyValueName = L"EnableLocalManifestFiles"; + const std::wstring EnableHashOverridePolicyValueName = L"EnableHashOverride"; + const std::wstring DefaultSourcePolicyValueName = L"EnableDefaultSource"; + const std::wstring MSStoreSourcePolicyValueName = L"EnableMSStoreSource"; + const std::wstring AdditionalSourcesPolicyValueName = L"EnableAdditionalSources"; + const std::wstring AllowedSourcesPolicyValueName = L"EnableAllowedSources"; + + const std::wstring SourceUpdateIntervalPolicyValueName = L"SourceAutoUpdateIntervalInMinutes"; + + void DeleteUserSettingsFiles(); + + struct UserSettingsTest : AppInstaller::Settings::UserSettings + { + }; + + struct GroupPolicyTestOverride : AppInstaller::Settings::GroupPolicy + { + GroupPolicyTestOverride(const AppInstaller::Registry::Key& key); + ~GroupPolicyTestOverride(); + }; +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/UserSettings.cpp b/src/AppInstallerCLITests/UserSettings.cpp @@ -2,9 +2,9 @@ // Licensed under the MIT License. #include "pch.h" #include "TestCommon.h" +#include "TestSettings.h" #include <AppInstallerRuntime.h> #include <winget/Settings.h> -#include <winget/UserSettings.h> #include <AppInstallerErrors.h> @@ -14,6 +14,8 @@ using namespace AppInstaller::Settings; using namespace AppInstaller::Runtime; +using namespace TestCommon; +using namespace std::string_literals; using namespace std::string_view_literals; using namespace std::chrono_literals; @@ -23,25 +25,6 @@ namespace static constexpr std::string_view s_badJson = "{"; static constexpr std::string_view s_settings = "settings.json"sv; static constexpr std::string_view s_settingsBackup = "settings.json.backup"sv; - - void DeleteUserSettingsFiles() - { - auto settingsPath = UserSettings::SettingsFilePath(); - if (std::filesystem::exists(settingsPath)) - { - std::filesystem::remove(settingsPath); - } - - auto settingsBackupPath = GetPathTo(Streams::BackupUserSettings); - if (std::filesystem::exists(settingsBackupPath)) - { - std::filesystem::remove(settingsBackupPath); - } - } - - struct UserSettingsTest : UserSettings - { - }; } TEST_CASE("UserSettingsFilePaths", "[settings]") @@ -168,7 +151,7 @@ TEST_CASE("SettingProgressBar", "[settings]") SECTION("Default value") { UserSettingsTest userSettingTest; - + REQUIRE(userSettingTest.Get<Setting::ProgressBarVisualStyle>() == VisualStyle::Accent); REQUIRE(userSettingTest.GetWarnings().size() == 0); } @@ -270,9 +253,34 @@ TEST_CASE("SettingAutoUpdateIntervalInMinutes", "[settings]") REQUIRE(userSettingTest.Get<Setting::AutoUpdateTimeInMinutes>() == cinq); REQUIRE(userSettingTest.GetWarnings().size() == 1); } + SECTION("Overridden by Group Policy") + { + auto policiesKey = RegCreateVolatileTestRoot(); + SetRegistryValue(policiesKey.get(), SourceUpdateIntervalPolicyValueName, (DWORD)threehundred.count()); + GroupPolicyTestOverride policies{ policiesKey.get() }; + + std::string_view json = R"({ "source": { "autoUpdateIntervalInMinutes": 5 } })"; + SetSetting(Streams::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get<Setting::AutoUpdateTimeInMinutes>() == threehundred); + REQUIRE(userSettingTest.GetWarnings().size() == 0); + } + SECTION("Invalid Group Policy") + { + auto policiesKey = RegCreateVolatileTestRoot(); + SetRegistryValue(policiesKey.get(), SourceUpdateIntervalPolicyValueName, L"Not a number"s); + GroupPolicyTestOverride policies{ policiesKey.get() }; + + std::string_view json = R"({ "source": { "autoUpdateIntervalInMinutes": 5 } })"; + SetSetting(Streams::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get<Setting::AutoUpdateTimeInMinutes>() == cinq); + REQUIRE(userSettingTest.GetWarnings().size() == 0); + } } -// Test one experimental feature in usersettingstest context because there's no good way to test ExperimentalFeature TEST_CASE("SettingsExperimentalCmd", "[settings]") { DeleteUserSettingsFiles(); @@ -311,4 +319,19 @@ TEST_CASE("SettingsExperimentalCmd", "[settings]") REQUIRE(!userSettingTest.Get<Setting::EFExperimentalCmd>()); REQUIRE(userSettingTest.GetWarnings().size() == 1); } + SECTION("Disabled by group policy") + { + auto policiesKey = RegCreateVolatileTestRoot(); + SetRegistryValue(policiesKey.get(), SourceUpdateIntervalPolicyValueName, L"Not a number"s); + GroupPolicyTestOverride policies{ policiesKey.get() }; + + std::string_view json = R"({ "experimentalFeatures": { "experimentalCmd": true } })"; + SetSetting(Streams::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + // Experimental features group policy is applied at the ExperimentalFeature level, + // so it doesn't affect the settings. + REQUIRE(userSettingTest.Get<Setting::EFExperimentalCmd>()); + REQUIRE(userSettingTest.GetWarnings().size() == 0); + } } diff --git a/src/AppInstallerCLITests/WorkflowGroupPolicy.cpp b/src/AppInstallerCLITests/WorkflowGroupPolicy.cpp @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "TestSettings.h" +#include "AppInstallerErrors.h" +#include "Commands/InstallCommand.h" +#include "Commands/RootCommand.h" +#include "Commands/ShowCommand.h" +#include "Commands/UpgradeCommand.h" +#include "Commands/ValidateCommand.h" + +using namespace TestCommon; +using namespace AppInstaller::CLI; +using namespace AppInstaller::Settings; +using namespace std::string_view_literals; + + +TEST_CASE("GroupPolicy_WinGet", "[groupPolicy]") +{ + auto policiesKey = RegCreateVolatileTestRoot(); + SetRegistryValue(policiesKey.get(), WinGetPolicyValueName, false); + GroupPolicyTestOverride policies{ policiesKey.get() }; + + SECTION("Install is blocked") + { + std::ostringstream output; + Execution::Context context{ output, std::cin }; + context.Args.AddArg(Execution::Args::Type::Query, "Fake.Package"sv); + InstallCommand installCommand({}); + + installCommand.Execute(context); + + REQUIRE(context.IsTerminated()); + REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY); + } + SECTION("Info is not blocked") + { + std::ostringstream output; + Execution::Context context{ output, std::cin }; + context.Args.AddArg(Execution::Args::Type::Info); + RootCommand rootCommand({}); + + rootCommand.Execute(context); + + REQUIRE_FALSE(context.IsTerminated()); + REQUIRE(output.str().find("Enable Windows Package Manager") != std::string::npos); + } +} + +TEST_CASE("GroupPolicy_SettingsCommand", "[groupPolicy]") +{ + auto policiesKey = RegCreateVolatileTestRoot(); + SetRegistryValue(policiesKey.get(), WinGetSettingsPolicyValueName, false); + GroupPolicyTestOverride policies{ policiesKey.get() }; + + Invocation inv{ std::vector<std::string>{ "settings" } }; + RootCommand rootCommand; + REQUIRE_THROWS(rootCommand.FindSubCommand(inv)); +} + +TEST_CASE("GroupPolicy_LocalManifests", "[groupPolicy]") +{ + auto policiesKey = RegCreateVolatileTestRoot(); + SetRegistryValue(policiesKey.get(), LocalManifestsPolicyValueName, false); + GroupPolicyTestOverride policies{ policiesKey.get() }; + + SECTION("Blocked on install") + { + Execution::Args args; + args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Exe.yaml").GetPath().u8string()); + InstallCommand installCommand({}); + REQUIRE_THROWS(installCommand.ValidateArguments(args)); + } + SECTION("Blocked on upgrade") + { + Execution::Args args; + args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Exe.yaml").GetPath().u8string()); + UpgradeCommand upgradeCommand({}); + REQUIRE_THROWS(upgradeCommand.ValidateArguments(args)); + } + SECTION("Allowed on show") + { + Execution::Args args; + args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Exe.yaml").GetPath().u8string()); + ShowCommand showCommand({}); + REQUIRE_NOTHROW(showCommand.ValidateArguments(args)); + } + SECTION("Allowed on validate") + { + Execution::Args args; + args.AddArg(Execution::Args::Type::ValidateManifest, TestDataFile("InstallFlowTest_Exe.yaml").GetPath().u8string()); + ValidateCommand validateCommand({}); + REQUIRE_NOTHROW(validateCommand.ValidateArguments(args)); + } +} diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -244,6 +244,7 @@ </Link> </ItemDefinitionGroup> <ItemGroup> + <ClInclude Include="Public\winget\GroupPolicy.h" /> <ClInclude Include="HttpStream\HttpClientWrapper.h" /> <ClInclude Include="HttpStream\HttpLocalCache.h" /> <ClInclude Include="HttpStream\HttpRandomAccessStream.h" /> @@ -280,6 +281,7 @@ <ClInclude Include="Public\winget\Regex.h" /> <ClInclude Include="Public\winget\Registry.h" /> <ClInclude Include="Public\winget\ManifestSchemaValidation.h" /> + <ClInclude Include="Public\winget\Resources.h" /> <ClInclude Include="Public\winget\Settings.h" /> <ClInclude Include="Public\winget\UserSettings.h" /> <ClInclude Include="Public\winget\Yaml.h" /> @@ -289,6 +291,7 @@ <ClInclude Include="YamlWrapper.h" /> </ItemGroup> <ItemGroup> + <ClCompile Include="GroupPolicy.cpp" /> <ClCompile Include="AppInstallerLogging.cpp" /> <ClCompile Include="AppInstallerStrings.cpp" /> <ClCompile Include="DateTime.cpp" /> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -159,6 +159,12 @@ <ClInclude Include="Public\winget\JsonSchemaValidation.h"> <Filter>Public\winget</Filter> </ClInclude> + <ClInclude Include="Public\winget\GroupPolicy.h"> + <Filter>Public\winget</Filter> + </ClInclude> + <ClInclude Include="Public\winget\Resources.h"> + <Filter>Public\winget</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -269,6 +275,9 @@ <ClCompile Include="JsonSchemaValidation.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="GroupPolicy.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp @@ -129,6 +129,8 @@ namespace AppInstaller return "The configured rest source is not supported"; case APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA: return "Invalid data returned by rest source"; + case APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY: + return "Operation is blocked by Group Policy"; default: return "Unknown Error Code"; } diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -2,41 +2,69 @@ // Licensed under the MIT License. #pragma once #include "pch.h" +#include "AppInstallerLogging.h" #include "winget/ExperimentalFeature.h" +#include "winget/GroupPolicy.h" #include "winget/UserSettings.h" namespace AppInstaller::Settings { + namespace + { + bool IsEnabledInternal(ExperimentalFeature::Feature feature, const UserSettings& userSettings) + { + if (feature == ExperimentalFeature::Feature::None) + { + return true; + } + + if (!GroupPolicies().IsEnabled(TogglePolicy::Policy::ExperimentalFeatures)) + { + AICLI_LOG(Core, Info, << + "Experimental feature " << ExperimentalFeature::GetFeature(feature).Name() << + " is disabled due to group policy" << TogglePolicy::GetPolicy(TogglePolicy::Policy::ExperimentalFeatures).RegValueName()); + return false; + } + + switch (feature) + { + case ExperimentalFeature::Feature::ExperimentalCmd: + // ExperimentalArg depends on ExperimentalCmd, so instead of failing we could + // assume that if ExperimentalArg is enabled then ExperimentalCmd is as well. + return userSettings.Get<Setting::EFExperimentalCmd>() || userSettings.Get<Setting::EFExperimentalArg>(); + case ExperimentalFeature::Feature::ExperimentalArg: + return userSettings.Get<Setting::EFExperimentalArg>(); + case ExperimentalFeature::Feature::ExperimentalMSStore: + return userSettings.Get<Setting::EFExperimentalMSStore>(); + case ExperimentalFeature::Feature::ExperimentalList: + return userSettings.Get<Setting::EFList>(); + case ExperimentalFeature::Feature::ExperimentalUpgrade: + return userSettings.Get<Setting::EFExperimentalUpgrade>(); + case ExperimentalFeature::Feature::ExperimentalUninstall: + return userSettings.Get<Setting::EFUninstall>(); + case ExperimentalFeature::Feature::ExperimentalImport: + return userSettings.Get<Setting::EFImport>(); + case ExperimentalFeature::Feature::ExperimentalExport: + return userSettings.Get<Setting::EFExport>(); + case ExperimentalFeature::Feature::ExperimentalRestSource: + return userSettings.Get<Setting::EFRestSource>(); + default: + THROW_HR(E_UNEXPECTED); + } + } + } + bool ExperimentalFeature::IsEnabled(Feature feature) { - switch (feature) - { - case Feature::None: - return true; - case Feature::ExperimentalCmd: - // ExperimentalArg depends on ExperimentalCmd, so instead of failing we could - // assume that if ExperimentalArg is enabled then ExperimentalCmd is as well. - return User().Get<Setting::EFExperimentalCmd>() || User().Get<Setting::EFExperimentalArg>(); - case Feature::ExperimentalArg: - return User().Get<Setting::EFExperimentalArg>(); - case Feature::ExperimentalMSStore: - return User().Get<Setting::EFExperimentalMSStore>(); - case Feature::ExperimentalList: - return User().Get<Setting::EFList>(); - case Feature::ExperimentalUpgrade: - return User().Get<Setting::EFExperimentalUpgrade>(); - case Feature::ExperimentalUninstall: - return User().Get<Setting::EFUninstall>(); - case Feature::ExperimentalImport: - return User().Get<Setting::EFImport>(); - case Feature::ExperimentalExport: - return User().Get<Setting::EFExport>(); - case Feature::ExperimentalRestSource: - return User().Get<Setting::EFRestSource>(); - default: - THROW_HR(E_UNEXPECTED); - } + return IsEnabledInternal(feature, User()); + } + +#ifndef AICLI_DISABLE_TEST_HOOKS + bool ExperimentalFeature::IsEnabled(Feature feature, const UserSettings& userSettings) + { + return IsEnabledInternal(feature, userSettings); } +#endif ExperimentalFeature ExperimentalFeature::GetFeature(ExperimentalFeature::Feature feature) { diff --git a/src/AppInstallerCommonCore/GroupPolicy.cpp b/src/AppInstallerCommonCore/GroupPolicy.cpp @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "winget/GroupPolicy.h" +#include "AppInstallerLogging.h" + +using namespace AppInstaller::StringResource; + +namespace AppInstaller::Settings +{ + namespace + { + GroupPolicy& InstanceInternal(std::optional<GroupPolicy*> overridePolicy = {}) + { + // TODO: Read from the actual registry key + static GroupPolicy s_groupPolicy{ Registry::Key{} }; + static GroupPolicy* s_override = nullptr; + + if (overridePolicy.has_value()) + { + s_override = overridePolicy.value(); + } + + return (s_override ? *s_override : s_groupPolicy); + } + + template<Registry::Value::Type T> + std::optional<decltype(std::declval<Registry::Value>().GetValue<T>())> GetRegistryValue(const Registry::Key& key, const std::string_view valueName) + { + if (!key) + { + // Key does not exist; there's nothing to return + return std::nullopt; + } + + auto regValue = key[valueName]; + if (!regValue.has_value()) + { + // Value does not exist + return std::nullopt; + } + + auto value = regValue->TryGetValue<T>(); + if (!value.has_value()) + { + AICLI_LOG(Core, Warning, << "Value for policy '" << valueName << "' does not have expected type"); + return std::nullopt; + } + + return std::move(value.value()); + } + + std::optional<bool> RegistryValueIsTrue(const Registry::Key& key, std::string_view valueName) + { + auto intValue = GetRegistryValue<Registry::Value::Type::DWord>(key, valueName); + if (!intValue.has_value()) + { + return std::nullopt; + } + + AICLI_LOG(Core, Info, << "Found policy '" << valueName << "', Value: " << *intValue); + return (bool)*intValue; + } + + PolicyState GetStateInternal(const Registry::Key& key, TogglePolicy::Policy policy) + { + // Default to not configured if there is no policy for this + if (policy == TogglePolicy::Policy::None) + { + return PolicyState::NotConfigured; + } + + auto togglePolicy = TogglePolicy::GetPolicy(policy); + + // Policies are not configured if there is no registry value. + auto setting = RegistryValueIsTrue(key, togglePolicy.RegValueName()); + if (!setting.has_value()) + { + return PolicyState::NotConfigured; + } + + // Return flag as-is or invert depending on the policy + return *setting ? PolicyState::Enabled : PolicyState::Disabled; + } + + template <ValuePolicy P> + void Validate( + const Registry::Key& policiesKey, + GroupPolicy::ValuePoliciesMap& policies) + { + auto value = details::ValuePolicyMapping<P>::ReadAndValidate(policiesKey); + if (value.has_value()) + { + policies.Add<P>(std::move(*value)); + } + } + + template <size_t... P> + void ValidateAllValuePolicies( + const Registry::Key& policiesKey, + GroupPolicy::ValuePoliciesMap& policies, + std::index_sequence<P...>) + { + // Use folding to call each policy validate function. + (FoldHelper{}, ..., Validate<static_cast<ValuePolicy>(P)>(policiesKey, policies)); + } + } + + namespace details + { + std::optional<uint32_t> ValuePolicyMapping<ValuePolicy::SourceAutoUpdateIntervalInMinutes>::ReadAndValidate(const Registry::Key& policiesKey) + { + using Mapping = ValuePolicyMapping<ValuePolicy::SourceAutoUpdateIntervalInMinutes>; + return GetRegistryValue<Mapping::ValueType>(policiesKey , Mapping::ValueName); + } + + std::optional<std::vector<std::string>> ValuePolicyMapping<ValuePolicy::AdditionalSources>::ReadAndValidate(const Registry::Key&) + { + // TODO + return std::nullopt; + } + + std::optional<std::vector<std::string>> ValuePolicyMapping<ValuePolicy::AllowedSources>::ReadAndValidate(const Registry::Key&) + { + // TODO + return std::nullopt; + } + } + + TogglePolicy TogglePolicy::GetPolicy(TogglePolicy::Policy policy) + { + switch (policy) + { + case TogglePolicy::Policy::WinGet: + return TogglePolicy(policy, "EnableWindowsPackageManager"sv, String::PolicyEnableWinGet); + case TogglePolicy::Policy::Settings: return + TogglePolicy(policy, "EnableWindowsPackageManagerSettings"sv, String::PolicyEnableWingetSettings); + case TogglePolicy::Policy::ExperimentalFeatures: + return TogglePolicy(policy, "EnableExperimentalFeatures"sv, String::PolicyEnableExperimentalFeatures); + case TogglePolicy::Policy::LocalManifestFiles: + return TogglePolicy(policy, "EnableLocalManifestFiles"sv, String::PolicyEnableLocalManifests); + case TogglePolicy::Policy::HashOverride: + return TogglePolicy(policy, "EnableHashOverride"sv, String::PolicyEnableHashOverride); + case TogglePolicy::Policy::DefaultSource: + return TogglePolicy(policy, "EnableDefaultSource"sv, String::PolicyEnableDefaultSource); + case TogglePolicy::Policy::MSStoreSource: + return TogglePolicy(policy, "EnableMSStoreSource"sv, String::PolicyEnableMSStoreSource); + case TogglePolicy::Policy::AdditionalSources: + return TogglePolicy(policy, "EnableAdditionalSources"sv, String::PolicyAdditionalSources); + case TogglePolicy::Policy::AllowedSources: + return TogglePolicy(policy, "EnableAllowedSources"sv, String::PolicyAllowedSources); + default: + THROW_HR(E_UNEXPECTED); + } + } + + std::vector<TogglePolicy> TogglePolicy::GetAllPolicies() + { + using Toggle_t = std::underlying_type_t<TogglePolicy::Policy>; + + std::vector<TogglePolicy> result; + + // Skip "None" + for (Toggle_t i = 1 + static_cast<Toggle_t>(TogglePolicy::Policy::None); i < static_cast<Toggle_t>(TogglePolicy::Policy::Max); ++i) + { + result.emplace_back(GetPolicy(static_cast<Policy>(i))); + } + + return result; + } + + GroupPolicy::GroupPolicy(const Registry::Key& key) + { + ValidateAllValuePolicies(key, m_values, std::make_index_sequence<static_cast<size_t>(ValuePolicy::Max)>()); + + using Toggle_t = std::underlying_type_t<TogglePolicy::Policy>; + for (Toggle_t i = static_cast<Toggle_t>(TogglePolicy::Policy::None); i < static_cast<Toggle_t>(TogglePolicy::Policy::Max); ++i) + { + auto policy = static_cast<TogglePolicy::Policy>(i); + m_toggles[policy] = GetStateInternal(key, policy); + } + } + + PolicyState GroupPolicy::GetState(TogglePolicy::Policy policy) const + { + auto itr = m_toggles.find(policy); + if (itr == m_toggles.end()) + { + return PolicyState::NotConfigured; + } + + return itr->second; + } + + bool GroupPolicy::IsEnabled(TogglePolicy::Policy policy) const + { + if (policy == TogglePolicy::Policy::None) + { + return true; + } + + PolicyState state = GetState(policy); + if (state == PolicyState::NotConfigured) + { + return TogglePolicy::GetPolicy(policy).DefaultIsEnabled(); + } + + return state == PolicyState::Enabled; + } + + GroupPolicy const& GroupPolicy::Instance() + { + return InstanceInternal(); + } + + void GroupPolicy::OverrideInstance(GroupPolicy* overridePolicy) + { + InstanceInternal(overridePolicy); + } + + void GroupPolicy::ResetInstance() + { + InstanceInternal(nullptr); + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -70,6 +70,7 @@ #define APPINSTALLER_CLI_ERROR_SOURCE_NOT_REMOTE ((HRESULT)0x8A150037) #define APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE ((HRESULT)0x8A150038) #define APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA ((HRESULT)0x8A150039) +#define APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY ((HRESULT)0x8a15003A) namespace AppInstaller { diff --git a/src/AppInstallerCommonCore/Public/AppInstallerLanguageUtilities.h b/src/AppInstallerCommonCore/Public/AppInstallerLanguageUtilities.h @@ -113,7 +113,7 @@ namespace AppInstaller } // Return a value indicating whether the given enum is stored in the map. - bool Contains(Enum e) { return (m_data.find(e) != m_data.end()); } + bool Contains(Enum e) const { return (m_data.find(e) != m_data.end()); } // Gets the value. template <Enum E> diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -8,6 +8,7 @@ namespace AppInstaller::Settings { using namespace std::string_view_literals; + struct UserSettings; struct ExperimentalFeature { @@ -48,6 +49,11 @@ namespace AppInstaller::Settings ExperimentalFeature& operator=(ExperimentalFeature&&) = default; static bool IsEnabled(Feature feature); + +#ifndef AICLI_DISABLE_TEST_HOOKS + static bool IsEnabled(Feature feature, const UserSettings& userSettings); +#endif + static ExperimentalFeature GetFeature(ExperimentalFeature::Feature feature); static std::vector<ExperimentalFeature> GetAllFeatures(); diff --git a/src/AppInstallerCommonCore/Public/winget/GroupPolicy.h b/src/AppInstallerCommonCore/Public/winget/GroupPolicy.h @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +#include "AppInstallerLanguageUtilities.h" +#include "winget/Registry.h" +#include "winget/Resources.h" +#include <string_view> + +using namespace std::string_view_literals; + +namespace AppInstaller::Settings +{ + + // A policy that sets a value for some setting. + // The value of the policy is a value in the registry key, or is + // made up of sub-keys for settings that are lists. + enum class ValuePolicy + { + SourceAutoUpdateIntervalInMinutes, + AdditionalSources, // TODO + AllowedSources, // TODO + Max, + }; + + // A policy that acts as a toggle to enable or disable a feature. + // They are backed by a DWORD value with values 0 and 1. + struct TogglePolicy + { + enum class Policy + { + None = 0, + WinGet, + Settings, + ExperimentalFeatures, + LocalManifestFiles, + HashOverride, + DefaultSource, + MSStoreSource, + AdditionalSources, // TODO + AllowedSources, // TODO + Max, + }; + + TogglePolicy(Policy policy, std::string_view regValueName, StringResource::StringId policyName, bool defaultIsEnabled = true) : + m_policy(policy), m_regValueName(regValueName), m_policyName(policyName), m_defaultIsEnabled(defaultIsEnabled) {} + + static TogglePolicy GetPolicy(Policy policy); + static std::vector<TogglePolicy> GetAllPolicies(); + + Policy GetPolicy() const { return m_policy; } + std::string_view RegValueName() const { return m_regValueName; } + StringResource::StringId PolicyName() const { return m_policyName; } + bool DefaultIsEnabled() const { return m_defaultIsEnabled; } + + private: + Policy m_policy; + std::string_view m_regValueName; + StringResource::StringId m_policyName; + bool m_defaultIsEnabled; + }; + + // Possible configuration states for a policy. + enum class PolicyState + { + NotConfigured, + Disabled, + Enabled, + }; + + namespace details + { + + template <ValuePolicy P> + struct ValuePolicyMapping + { + // value_t - type of the policy + // ReadAndValidate() - Function that reads the value and does semantic validation. + + // For simple values: + // ValueName - Name of the registry value + // ValueType - Type of the registry value + // reg_value_t - Type returned by the registry when reading the value + }; + +#define POLICY_MAPPING_SPECIALIZATION(_policy_, _type_) \ + template <> \ + struct ValuePolicyMapping<_policy_> \ + { \ + using value_t = _type_; \ + static std::optional<value_t> ReadAndValidate(const Registry::Key& policiesKey); \ + } + +#define POLICY_MAPPING_VALUE_SPECIALIZATION(_policy_, _type_, _valueName_, _valueType_) \ + template<> \ + struct ValuePolicyMapping<_policy_> \ + { \ + static constexpr std::string_view ValueName = _valueName_; \ + static constexpr Registry::Value::Type ValueType = _valueType_; \ + using value_t = _type_; \ + using reg_value_t = decltype(std::declval<Registry::Value>().GetValue<ValueType>()); \ + static std::optional<value_t> ReadAndValidate(const Registry::Key& policiesKey); \ + } + + POLICY_MAPPING_VALUE_SPECIALIZATION(ValuePolicy::SourceAutoUpdateIntervalInMinutes, uint32_t, "SourceAutoUpdateIntervalInMinutes"sv, Registry::Value::Type::DWord); + + // TODO: Wire up policies for sources + POLICY_MAPPING_SPECIALIZATION(ValuePolicy::AdditionalSources, std::vector<std::string>); + POLICY_MAPPING_SPECIALIZATION(ValuePolicy::AllowedSources, std::vector<std::string>); + } + + // Representation of the policies read from the registry. + struct GroupPolicy + { + using ValuePoliciesMap = EnumBasedVariantMap<ValuePolicy, details::ValuePolicyMapping>; + + static GroupPolicy const& Instance(); + + GroupPolicy(const Registry::Key& key); + ~GroupPolicy() = default; + + GroupPolicy() = delete; + + GroupPolicy(const GroupPolicy&) = delete; + GroupPolicy& operator=(const GroupPolicy&) = delete; + + GroupPolicy(GroupPolicy&&) = delete; + GroupPolicy& operator=(GroupPolicy&&) = delete; + + // Gets the policy value if it is present + template<ValuePolicy P> + std::optional<typename details::ValuePolicyMapping<P>::value_t> GetValue() const + { + if (m_values.Contains(P)) + { + return m_values.Get<P>(); + } + else + { + return std::nullopt; + } + } + + PolicyState GetState(TogglePolicy::Policy policy) const; + + // Checks whether a policy is enabled, using an appropriate default when not configured. + // Should not be used when not configured means something different than enabled/disabled. + bool IsEnabled(TogglePolicy::Policy policy) const; + + private: + std::map<TogglePolicy::Policy, PolicyState> m_toggles; + ValuePoliciesMap m_values; + +#ifndef AICLI_DISABLE_TEST_HOOKS + protected: + static void OverrideInstance(GroupPolicy* gp); + static void ResetInstance(); +#endif + }; + + inline const GroupPolicy& GroupPolicies() + { + return GroupPolicy::Instance(); + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/Registry.h b/src/AppInstallerCommonCore/Public/winget/Registry.h @@ -87,14 +87,27 @@ namespace AppInstaller::Registry template <Type T> typename details::ValueTypeSpecifics<static_cast<DWORD>(T)>::value_t GetValue() const { - EnsureType(T); + THROW_HR_IF(E_INVALIDARG, !HasCompatibleType(T)); return details::ValueTypeSpecifics<static_cast<DWORD>(T)>::Convert(m_data); } + template <Type T> + typename std::optional<typename details::ValueTypeSpecifics<static_cast<DWORD>(T)>::value_t> TryGetValue() const + { + if (HasCompatibleType(T)) + { + return details::ValueTypeSpecifics<static_cast<DWORD>(T)>::Convert(m_data); + } + else + { + return std::nullopt; + } + } + private: Value(DWORD type, std::vector<BYTE>&& data); - void EnsureType(Type type) const; + bool HasCompatibleType(Type type) const; Type m_type; std::vector<BYTE> m_data; diff --git a/src/AppInstallerCommonCore/Public/winget/Resources.h b/src/AppInstallerCommonCore/Public/winget/Resources.h @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <winrt/Windows.ApplicationModel.Resources.h> + +#include <string> + +using namespace std::string_view_literals; + +namespace AppInstaller::StringResource +{ + +#define WINGET_WIDE_STRINGIFY_HELP(_id_) L ## _id_ +#define WINGET_WIDE_STRINGIFY(_id_) WINGET_WIDE_STRINGIFY_HELP(_id_) +#define WINGET_DEFINE_RESOURCE_STRINGID(_id_) static constexpr AppInstaller::StringResource::StringId _id_ { WINGET_WIDE_STRINGIFY(#_id_) ## sv } + + // A resource identifier + struct StringId : public std::wstring_view + { + explicit constexpr StringId(std::wstring_view id) : std::wstring_view(id) {} + }; + + // Resource string identifiers. + struct String + { + WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableWinGet); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableWingetSettings); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableExperimentalFeatures); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableLocalManifests); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableHashOverride); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableDefaultSource); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableMSStoreSource); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyAdditionalSources); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyAllowedSources); + WINGET_DEFINE_RESOURCE_STRINGID(PolicySourceAutoUpdateInterval); + + WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningInvalidFieldFormat); + WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningInvalidFieldValue); + WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningInvalidValueFromPolicy); + WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningLoadedBackupSettings); + WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningParseError); + }; +} diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -2,11 +2,14 @@ // Licensed under the MIT License. #pragma once #include "AppInstallerStrings.h" +#include "winget/GroupPolicy.h" +#include "winget/Resources.h" #include <filesystem> #include <map> #include <optional> #include <string> +#include <type_traits> #include <variant> #include <vector> @@ -72,7 +75,7 @@ namespace AppInstaller::Settings // Validate - Function that does semantic validation. }; -#define SETTINGMAPPING_SPECIALIZATION(_setting_, _json_, _value_, _default_, _path_) \ +#define SETTINGMAPPING_SPECIALIZATION_EXTEND(_setting_, _json_, _value_, _default_, _path_, _extension_) \ template <> \ struct SettingMapping<_setting_> \ { \ @@ -81,10 +84,21 @@ namespace AppInstaller::Settings static constexpr value_t DefaultValue = _default_; \ static constexpr std::string_view Path = _path_; \ static std::optional<value_t> Validate(const json_t& value); \ + _extension_ \ } +#define SETTINGMAPPING_SPECIALIZATION(_setting_, _json_, _value_, _default_, _path_) \ + SETTINGMAPPING_SPECIALIZATION_EXTEND(_setting_, _json_, _value_, _default_, _path_, ) + +#define SETTINGMAPPING_SPECIALIZATION_POLICY(_setting_, _json_, _value_, _default_, _path_, _valuePolicy_) \ + SETTINGMAPPING_SPECIALIZATION_EXTEND(_setting_, _json_, _value_, _default_, _path_, \ + static constexpr ValuePolicy Policy = _valuePolicy_; \ + using policy_t = decltype(std::declval<GroupPolicy>().GetValue<Policy>())::value_type; \ + static_assert(std::is_same<json_t, policy_t>::value); \ + ) + SETTINGMAPPING_SPECIALIZATION(Setting::ProgressBarVisualStyle, std::string, VisualStyle, VisualStyle::Accent, ".visual.progressBar"sv); - SETTINGMAPPING_SPECIALIZATION(Setting::AutoUpdateTimeInMinutes, uint32_t, std::chrono::minutes, 5min, ".source.autoUpdateIntervalInMinutes"sv); + SETTINGMAPPING_SPECIALIZATION_POLICY(Setting::AutoUpdateTimeInMinutes, uint32_t, std::chrono::minutes, 5min, ".source.autoUpdateIntervalInMinutes"sv, ValuePolicy::SourceAutoUpdateIntervalInMinutes); SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalCmd, bool, bool, false, ".experimentalFeatures.experimentalCmd"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalArg, bool, bool, false, ".experimentalFeatures.experimentalArg"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalMSStore, bool, bool, false, ".experimentalFeatures.experimentalMSStore"sv); @@ -107,10 +121,23 @@ namespace AppInstaller::Settings constexpr inline size_t SettingIndex(Setting s) { return static_cast<size_t>(s) + 1; } } - // Representation of the parsed settings file. struct UserSettings { + // Jsoncpp doesn't provide line number and column for an individual Json::Value node. + struct Warning + { + Warning(StringResource::StringId message) : Message(message) {} + Warning(StringResource::StringId message, std::string_view settingPath) : Message(message), Path(settingPath) {} + Warning(StringResource::StringId message, std::string_view settingPath, std::string_view settingValue, bool isField = true) : + Message(message), Path(settingPath), Data(settingValue), IsFieldWarning(isField) {} + + StringResource::StringId Message; + std::string Path; + std::string Data; + bool IsFieldWarning = true; + }; + static UserSettings const& Instance() { static UserSettings userSettings; @@ -126,7 +153,7 @@ namespace AppInstaller::Settings UserSettings& operator=(UserSettings&&) = delete; UserSettingsType GetType() const { return m_type; } - std::vector<std::string> const& GetWarnings() const { return m_warnings; } + std::vector<Warning> const& GetWarnings() const { return m_warnings; } void PrepareToShellExecuteFile() const; @@ -145,7 +172,7 @@ namespace AppInstaller::Settings private: UserSettingsType m_type = UserSettingsType::Default; - std::vector<std::string> m_warnings; + std::vector<Warning> m_warnings; std::map<Setting, details::SettingVariant> m_settings; protected: diff --git a/src/AppInstallerCommonCore/Registry.cpp b/src/AppInstallerCommonCore/Registry.cpp @@ -82,15 +82,15 @@ namespace AppInstaller::Registry { } - void Value::EnsureType(Type type) const + bool Value::HasCompatibleType(Type type) const { // Allow interop between String and ExpandString if ((m_type == Type::String || m_type == Type::ExpandString) && (type == Type::String || type == Type::ExpandString)) { - return; + return true; } - THROW_HR_IF(E_INVALIDARG, m_type != type); + return m_type == type; } Key::Key(HKEY key) diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -24,26 +24,10 @@ namespace AppInstaller::Settings // }, })"sv; - namespace SettingsMessage - { - const char* const Field = " Field: "; - const char* const Value = " Value: "; - const char* const ValidMessage = "Valid setting"; - const char* const InvalidFieldValue = "Invalid field value."; - const char* const InvalidFieldFormat = "Invalid field format."; - constexpr std::string_view LoadedBackupSettings = "Loaded settings from backup file."sv; - } - namespace { - // Jsoncpp doesn't provide line number and column for an individual Json::Value node. - inline std::string GetSettingsMessage(const std::string& message, const std::string& path) - { - return message + SettingsMessage::Field + path; - } - template<class T> - inline std::string GetSettingsMessage(const std::string& message, const std::string& path, T value) + inline std::string GetValueString(T value) { std::string convertedValue; @@ -56,10 +40,10 @@ namespace AppInstaller::Settings convertedValue = value; } - return GetSettingsMessage(message, path) + SettingsMessage::Value + convertedValue; + return convertedValue; } - std::optional<Json::Value> ParseFile(const StreamDefinition& setting, std::vector<std::string>& warnings) + std::optional<Json::Value> ParseFile(const StreamDefinition& setting, std::vector<UserSettings::Warning>& warnings) { auto stream = GetSettingStream(setting); if (stream) @@ -77,21 +61,65 @@ namespace AppInstaller::Settings } AICLI_LOG(Core, Error, << "Error parsing " << setting.Path << ": " << error); - warnings.emplace_back(setting.Path); - warnings.emplace_back(error); + warnings.emplace_back(StringResource::String::SettingsWarningParseError, setting.Path, error, false); } return {}; } template <Setting S> + std::optional<typename details::SettingMapping<S>::policy_t> GetValueFromPolicy(int) + { + // return std::nullopt; + return GroupPolicies().GetValue<details::SettingMapping<S>::Policy>(); + } + + template <Setting S> + std::optional<typename details::SettingMapping<S>::json_t> GetValueFromPolicy(long) + { + using T = decltype(std::declval<details::SettingMapping<S>::json_t>()); + return std::nullopt; + } + + template <Setting S> + std::optional<typename details::SettingMapping<S>::json_t> GetValueFromPolicy() + { + return GetValueFromPolicy<S>(0); + } + + template <Setting S> void Validate( Json::Value& root, std::map<Setting, details::SettingVariant>& settings, - std::vector<std::string>& warnings) + std::vector<UserSettings::Warning>& warnings) { // jsoncpp doesn't support std::string_view yet. - auto path = std::string(details::SettingMapping<S>::Path); + auto path = std::string(details::SettingMapping<S>::Path); + + // Settings set by Group Policy override anything else. See if there is one. + auto policyValue = GetValueFromPolicy<S>(); + if (policyValue.has_value()) + { + // If the value is valid, use it. + // Otherwise, fall back to default. + // In any case, we do not need to read the setting from the JSON. + auto validatedValue = details::SettingMapping<S>::Validate(policyValue.value()); + if (validatedValue.has_value()) + { + // Add it to the map + settings[S].emplace<details::SettingIndex(S)>( + std::forward<typename details::SettingMapping<S>::value_t>(validatedValue.value())); + AICLI_LOG(Core, Info, << "Valid setting from Group Policy. Field: " << path << " Value: " << GetValueString(policyValue.value())); + } + else + { + auto valueAsString = GetValueString(policyValue.value()); + AICLI_LOG(Core, Info, << "Invalid setting from Group Policy. Field: " << path << " Value: " << valueAsString); + warnings.emplace_back(StringResource::String::SettingsWarningInvalidValueFromPolicy, path, valueAsString); + } + + return; + } const Json::Path jsonPath(path); Json::Value result = jsonPath.resolve(root); @@ -108,20 +136,19 @@ namespace AppInstaller::Settings // Finally add it to the map settings[S].emplace<details::SettingIndex(S)>( std::forward<typename details::SettingMapping<S>::value_t>(validatedValue.value())); - AICLI_LOG(Core, Info, << GetSettingsMessage(SettingsMessage::ValidMessage, path, jsonValue.value())); + AICLI_LOG(Core, Info, << "Valid setting. Field: " << path << " Value: " << GetValueString(jsonValue.value())); } else { - auto invalidFieldMsg = GetSettingsMessage(SettingsMessage::InvalidFieldValue, path, jsonValue.value()); - AICLI_LOG(Core, Error, << invalidFieldMsg << " Using default"); - warnings.emplace_back(invalidFieldMsg); + auto valueAsString = GetValueString(jsonValue.value()); + AICLI_LOG(Core, Error, << "Invalid field value. Field: " << path << " Value: " << valueAsString); + warnings.emplace_back(StringResource::String::SettingsWarningInvalidFieldValue, path, valueAsString); } } else { - auto invalidFormatMsg = GetSettingsMessage(SettingsMessage::InvalidFieldFormat, path); - AICLI_LOG(Core, Error, << invalidFormatMsg << " Using default"); - warnings.emplace_back(invalidFormatMsg); + AICLI_LOG(Core, Error, << "Invalid field format. Field: " << path << " Using default"); + warnings.emplace_back(StringResource::String::SettingsWarningInvalidFieldFormat, path); } } else @@ -134,7 +161,7 @@ namespace AppInstaller::Settings void ValidateAll( Json::Value& root, std::map<Setting, details::SettingVariant>& settings, - std::vector<std::string>& warnings, + std::vector<UserSettings::Warning>& warnings, std::index_sequence<S...>) { #ifdef WINGET_DISABLE_FOR_FUZZING @@ -206,10 +233,17 @@ namespace AppInstaller::Settings Json::Value settingsRoot = Json::Value::nullSingleton(); // Settings can be loaded from settings.json or settings.json.backup files. + // 0 - Use default (empty) settings if disabled by group policy. // 1 - Use settings.json if exists and passes parsing. // 2 - Use settings.backup.json if settings.json fails to parse. // 3 - Use default (empty) if both settings files fail to load. + if (!GroupPolicies().IsEnabled(TogglePolicy::Policy::Settings)) + { + AICLI_LOG(Core, Info, << "Ignoring settings file due to group policy. Using default values."); + return; + } + auto settingsJson = ParseFile(Streams::PrimaryUserSettings, m_warnings); if (settingsJson.has_value()) { @@ -225,7 +259,7 @@ namespace AppInstaller::Settings if (settingsBackupJson.has_value()) { AICLI_LOG(Core, Info, << "Settings loaded from " << Streams::BackupUserSettings.Path); - m_warnings.emplace_back(SettingsMessage::LoadedBackupSettings); + m_warnings.emplace_back(StringResource::String::SettingsWarningLoadedBackupSettings); m_type = UserSettingsType::Backup; settingsRoot = settingsBackupJson.value(); } diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -9,6 +9,8 @@ #include "Microsoft/PreIndexedPackageSourceFactory.h" #include "Rest/RestSourceFactory.h" +#include <winget/GroupPolicy.h> + namespace AppInstaller::Repository { using namespace Settings; @@ -177,6 +179,34 @@ namespace AppInstaller::Repository }); } + // Checks whether a default source is enabled with the current settings + bool IsDefaultSourceEnabled(std::string_view sourceToLog, ExperimentalFeature::Feature feature, TogglePolicy::Policy policy) + { + if (!ExperimentalFeature::IsEnabled(feature)) + { + // No need to log here + return false; + } + + if (!GroupPolicies().IsEnabled(policy)) + { + AICLI_LOG(Repo, Info, << "The default source " << sourceToLog << " is disabled due to Group Policy"); + return false; + } + + return true; + } + + bool IsWingetCommunityDefaultSourceEnabled() + { + return IsDefaultSourceEnabled(s_Source_WingetCommunityDefault_Name, ExperimentalFeature::Feature::None, TogglePolicy::Policy::DefaultSource); + } + + bool IsWingetMSStoreDefaultSourceEnabled() + { + return IsDefaultSourceEnabled(s_Source_WingetMSStoreDefault_Name, ExperimentalFeature::Feature::ExperimentalMSStore, TogglePolicy::Policy::MSStoreSource); + } + // Gets the sources from a particular origin. std::vector<SourceDetailsInternal> GetSourcesByOrigin(SourceOrigin origin) { @@ -186,16 +216,19 @@ namespace AppInstaller::Repository { case SourceOrigin::Default: { - SourceDetailsInternal details; - details.Name = s_Source_WingetCommunityDefault_Name; - details.Type = Microsoft::PreIndexedPackageSourceFactory::Type(); - details.Arg = s_Source_WingetCommunityDefault_Arg; - details.Data = s_Source_WingetCommunityDefault_Data; - details.Identifier = s_Source_WingetCommunityDefault_Identifier; - details.TrustLevel = SourceTrustLevel::Trusted; - result.emplace_back(std::move(details)); + if (IsWingetCommunityDefaultSourceEnabled()) + { + SourceDetailsInternal details; + details.Name = s_Source_WingetCommunityDefault_Name; + details.Type = Microsoft::PreIndexedPackageSourceFactory::Type(); + details.Arg = s_Source_WingetCommunityDefault_Arg; + details.Data = s_Source_WingetCommunityDefault_Data; + details.Identifier = s_Source_WingetCommunityDefault_Identifier; + details.TrustLevel = SourceTrustLevel::Trusted; + result.emplace_back(std::move(details)); + } - if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::ExperimentalMSStore)) + if (IsWingetMSStoreDefaultSourceEnabled()) { SourceDetailsInternal storeDetails; storeDetails.Name = s_Source_WingetMSStoreDefault_Name; @@ -206,8 +239,9 @@ namespace AppInstaller::Repository storeDetails.TrustLevel = SourceTrustLevel::Trusted; result.emplace_back(std::move(storeDetails)); } + + break; } - break; case SourceOrigin::User: { std::vector<SourceDetailsInternal> userSources = GetSourcesFromSetting( @@ -563,6 +597,39 @@ namespace AppInstaller::Repository { SetMetadata(m_sourceList); } + + // Checks whether the group policy allows this source. + // Reasons for not allowing: + // - The source is a default source that is disabled + // - Allowed sources are disabled, blocking everything + // - There is an explicit list of allowed sources and this source is not in it + bool IsSourceAllowedByPolicy(std::string_view, std::string_view, std::string_view arg) + { + if (Utility::CaseInsensitiveEquals(arg, s_Source_WingetCommunityDefault_Arg)) + { + return IsWingetCommunityDefaultSourceEnabled(); + } + + if (Utility::CaseInsensitiveEquals(arg, s_Source_WingetMSStoreDefault_Arg)) + { + return IsWingetMSStoreDefaultSourceEnabled(); + } + + auto allowedSourcesPolicy = GroupPolicies().GetState(TogglePolicy::Policy::AllowedSources); + if (allowedSourcesPolicy == PolicyState::Disabled) + { + // We check here but this should already be blocked higher in the stack + AICLI_LOG(Repo, Warning, << "Additional sources are blocked by group policy"); + return false; + } + + if (allowedSourcesPolicy == PolicyState::Enabled) + { + // TODO: Allowed sources + } + + return true; + } } std::string_view ToString(SourceOrigin origin) @@ -619,6 +686,8 @@ namespace AppInstaller::Repository auto source = sourceList.GetCurrentSource(name); THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_NAME_ALREADY_EXISTS, source != nullptr); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY, !IsSourceAllowedByPolicy(name, type, arg)); + SourceDetailsInternal details; details.Name = name; details.Type = type;