diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 1ded03803c..e8869de97f 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -423,6 +423,7 @@ srs startswith STARTUPINFOW STDMETHODCALLTYPE +storeorigin STRRET stylecop subdir diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index ffc41ed56e..84b79a0cdc 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -111,6 +111,10 @@ namespace AppInstaller::CLI return { type, "arg"_liv, 'a' }; case Execution::Args::Type::ForceSourceReset: return { type, "force"_liv }; + case Execution::Args::Type::SourceExplicit: + return { type, "explicit"_liv }; + case Execution::Args::Type::SourceTrustLevel: + return { type, "trust-level"_liv }; //Hash Command case Execution::Args::Type::HashFile: @@ -340,6 +344,10 @@ namespace AppInstaller::CLI return Argument{ type, Resource::String::SourceArgArgumentDescription, ArgumentType::Positional, true }; case Args::Type::SourceType: return Argument{ type, Resource::String::SourceTypeArgumentDescription, ArgumentType::Positional }; + case Args::Type::SourceExplicit: + return Argument{ type, Resource::String::SourceExplicitArgumentDescription, ArgumentType::Flag }; + case Args::Type::SourceTrustLevel: + return Argument{ type, Resource::String::SourceTrustLevelArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }; case Args::Type::ValidateManifest: return Argument{ type, Resource::String::ValidateManifestArgumentDescription, ArgumentType::Positional, true }; case Args::Type::IgnoreWarnings: diff --git a/src/AppInstallerCLICore/Commands/SourceCommand.cpp b/src/AppInstallerCLICore/Commands/SourceCommand.cpp index cd44ffaef5..4b0b557ffb 100644 --- a/src/AppInstallerCLICore/Commands/SourceCommand.cpp +++ b/src/AppInstallerCLICore/Commands/SourceCommand.cpp @@ -52,8 +52,10 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::SourceName).SetRequired(true), Argument::ForType(Args::Type::SourceArg), Argument::ForType(Args::Type::SourceType), + Argument::ForType(Args::Type::SourceTrustLevel), Argument::ForType(Args::Type::CustomHeader), Argument::ForType(Args::Type::AcceptSourceAgreements), + Argument::ForType(Args::Type::SourceExplicit), }; } @@ -72,6 +74,29 @@ namespace AppInstaller::CLI return s_SourceCommand_HelpLink; } + void SourceAddCommand::ValidateArgumentsInternal(Args& execArgs) const + { + if (execArgs.Contains(Execution::Args::Type::SourceTrustLevel)) + { + try + { + std::string trustLevelArg = std::string{ execArgs.GetArg(Execution::Args::Type::SourceTrustLevel) }; + + for (auto trustLevel : Utility::Split(trustLevelArg, '|', true)) + { + Repository::ConvertToSourceTrustLevelEnum(trustLevel); + } + } + catch (...) + { + auto validOptions = std::vector{ + Utility::LocIndString{ Repository::SourceTrustLevelEnumToString(Repository::SourceTrustLevel::None) }, + Utility::LocIndString{ Repository::SourceTrustLevelEnumToString(Repository::SourceTrustLevel::Trusted) } }; + throw CommandException(Resource::String::InvalidArgumentValueError(ArgumentCommon::ForType(Execution::Args::Type::SourceTrustLevel).Name, Utility::Join(","_liv, validOptions))); + } + } + } + void SourceAddCommand::ExecuteInternal(Context& context) const { // Note: Group Policy for allowed sources is enforced at the RepositoryCore level diff --git a/src/AppInstallerCLICore/Commands/SourceCommand.h b/src/AppInstallerCLICore/Commands/SourceCommand.h index 80d831c0e2..4656bfb62c 100644 --- a/src/AppInstallerCLICore/Commands/SourceCommand.h +++ b/src/AppInstallerCLICore/Commands/SourceCommand.h @@ -32,6 +32,7 @@ namespace AppInstaller::CLI Utility::LocIndView HelpLink() const override; protected: + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; void ExecuteInternal(Execution::Context& context) const override; }; diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index 8ea1e0754d..622265a893 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -60,6 +60,8 @@ namespace AppInstaller::CLI::Execution SourceType, SourceArg, ForceSourceReset, + SourceExplicit, + SourceTrustLevel, //Hash Command HashFile, diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index a05c3c691a..528651fa82 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -545,6 +545,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceListName); WINGET_DEFINE_RESOURCE_STRINGID(SourceListNoneFound); WINGET_DEFINE_RESOURCE_STRINGID(SourceListNoSources); + WINGET_DEFINE_RESOURCE_STRINGID(SourceListExplicit); + WINGET_DEFINE_RESOURCE_STRINGID(SourceListTrustLevel); WINGET_DEFINE_RESOURCE_STRINGID(SourceListType); WINGET_DEFINE_RESOURCE_STRINGID(SourceListUpdated); WINGET_DEFINE_RESOURCE_STRINGID(SourceListUpdatedNever); @@ -558,12 +560,14 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveOne); WINGET_DEFINE_RESOURCE_STRINGID(SourceRequiresAuthentication); + WINGET_DEFINE_RESOURCE_STRINGID(SourceExplicitArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetAll); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetForceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetListAndOverridePreamble); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetOne); + WINGET_DEFINE_RESOURCE_STRINGID(SourceTrustLevelArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceTypeArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceUpdateAll); WINGET_DEFINE_RESOURCE_STRINGID(SourceUpdateCommandLongDescription); diff --git a/src/AppInstallerCLICore/Workflows/SourceFlow.cpp b/src/AppInstallerCLICore/Workflows/SourceFlow.cpp index 0c565539d7..a6a1dee6d4 100644 --- a/src/AppInstallerCLICore/Workflows/SourceFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/SourceFlow.cpp @@ -107,8 +107,16 @@ namespace AppInstaller::CLI::Workflow std::string_view name = context.Args.GetArg(Args::Type::SourceName); std::string_view arg = context.Args.GetArg(Args::Type::SourceArg); std::string_view type = context.Args.GetArg(Args::Type::SourceType); + bool isExplicit = context.Args.Contains(Args::Type::SourceExplicit); - Repository::Source sourceToAdd{ name, arg, type }; + Repository::SourceTrustLevel trustLevel = Repository::SourceTrustLevel::None; + if (context.Args.Contains(Execution::Args::Type::SourceTrustLevel)) + { + std::vector trustLevelArgs = Utility::Split(std::string{ context.Args.GetArg(Execution::Args::Type::SourceTrustLevel) }, '|', true); + trustLevel = Repository::ConvertToSourceTrustLevelFlag(trustLevelArgs); + } + + Repository::Source sourceToAdd{ name, arg, type, trustLevel, isExplicit}; if (context.Args.Contains(Execution::Args::Type::CustomHeader)) { @@ -156,6 +164,8 @@ namespace AppInstaller::CLI::Workflow table.OutputLine({ Resource::LocString(Resource::String::SourceListArg), source.Arg }); table.OutputLine({ Resource::LocString(Resource::String::SourceListData), source.Data }); table.OutputLine({ Resource::LocString(Resource::String::SourceListIdentifier), source.Identifier }); + table.OutputLine({ Resource::LocString(Resource::String::SourceListTrustLevel), Repository::GetSourceTrustLevelForDisplay(source.TrustLevel)}); + table.OutputLine({ Resource::LocString(Resource::String::SourceListExplicit), std::string{ Utility::ConvertBoolToString(source.Explicit) }}); if (source.LastUpdateTime == Utility::ConvertUnixEpochToSystemClock(0)) { @@ -181,10 +191,10 @@ namespace AppInstaller::CLI::Workflow } else { - Execution::TableOutput<2> table(context.Reporter, { Resource::String::SourceListName, Resource::String::SourceListArg }); + Execution::TableOutput<3> table(context.Reporter, { Resource::String::SourceListName, Resource::String::SourceListArg, Resource::String::SourceListExplicit }); for (const auto& source : sources) { - table.OutputLine({ source.Name, source.Arg }); + table.OutputLine({ source.Name, source.Arg, std::string{ Utility::ConvertBoolToString(source.Explicit) }}); } table.Complete(); } @@ -199,6 +209,7 @@ namespace AppInstaller::CLI::Workflow } const std::vector& sources = context.Get(); + for (const auto& sd : sources) { Repository::Source source{ sd.Name }; @@ -303,6 +314,10 @@ namespace AppInstaller::CLI::Workflow s.Arg = source.Arg; s.Data = source.Data; s.Identifier = source.Identifier; + + std::vector sourceTrustLevels = Repository::SourceTrustLevelFlagToList(source.TrustLevel); + s.TrustLevel = std::vector(sourceTrustLevels.begin(), sourceTrustLevels.end()); + s.Explicit = source.Explicit; context.Reporter.Info() << s.ToJsonString() << std::endl; } } diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index 87eace8626..8a0e8ba7fa 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // diff --git a/src/AppInstallerCLIE2ETests/GroupPolicy.cs b/src/AppInstallerCLIE2ETests/GroupPolicy.cs index c35dffed2b..ac64eaa62b 100644 --- a/src/AppInstallerCLIE2ETests/GroupPolicy.cs +++ b/src/AppInstallerCLIE2ETests/GroupPolicy.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -180,6 +180,32 @@ public void EnableAdditionalSources() Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); } + /// + /// Test additional sources with trust levels and explicit are enabled by policy. + /// + [Test] + public void EnableAdditionalSources_TrustLevel_Explicit() + { + // Remove the test source, then add it with policy. + TestCommon.RunAICLICommand("source remove", "TestSource"); + var result = TestCommon.RunAICLICommand("source list", "TestSource"); + Assert.AreEqual(Constants.ErrorCode.ERROR_SOURCE_NAME_DOES_NOT_EXIST, result.ExitCode); + + GroupPolicyHelper.EnableAdditionalSources.SetEnabledList(new string[] + { + "{\"Arg\":\"https://localhost:5001/TestKit\",\"Data\":\"WingetE2E.Tests_8wekyb3d8bbwe\",\"Identifier\":\"WingetE2E.Tests_8wekyb3d8bbwe\",\"Name\":\"TestSource\",\"Type\":\"Microsoft.PreIndexed.Package\",\"TrustLevel\":[\"Trusted\"],\"Explicit\":true}", + }); + + result = TestCommon.RunAICLICommand("source list", "TestSource"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Trust Level")); + Assert.True(result.StdOut.Contains("Trusted")); + + var searchResult = TestCommon.RunAICLICommand("search", "TestExampleInstaller"); + Assert.AreEqual(Constants.ErrorCode.ERROR_NO_SOURCES_DEFINED, searchResult.ExitCode); + Assert.True(searchResult.StdOut.Contains("No sources defined; add one with 'source add' or reset to defaults with 'source reset'")); + } + /// /// Test enable allowed sources. /// diff --git a/src/AppInstallerCLIE2ETests/GroupPolicyHelper.cs b/src/AppInstallerCLIE2ETests/GroupPolicyHelper.cs index 8179934fa6..b98d3a7f9b 100644 --- a/src/AppInstallerCLIE2ETests/GroupPolicyHelper.cs +++ b/src/AppInstallerCLIE2ETests/GroupPolicyHelper.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -385,6 +385,16 @@ public class GroupPolicySource /// Gets or sets certificate pinning. /// public GroupPolicyCertificatePinning CertificatePinning { get; set; } + + /// + /// Gets or sets the source trust levels. + /// + public string[] TrustLevel { get; set; } + + /// + /// Gets or sets a value indicating whether the source is explicit. + /// + public bool Explicit { get; set; } } /// diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs index 4c068b9579..c5c7d290a4 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -701,6 +701,8 @@ public static void SetupTestSource(bool useGroupPolicyForTestSource = false) }, }, }, + TrustLevel = new string[] { "None" }, + Explicit = false, }, }); } diff --git a/src/AppInstallerCLIE2ETests/SearchCommand.cs b/src/AppInstallerCLIE2ETests/SearchCommand.cs index c030f7d166..a2464a19f2 100644 --- a/src/AppInstallerCLIE2ETests/SearchCommand.cs +++ b/src/AppInstallerCLIE2ETests/SearchCommand.cs @@ -197,6 +197,8 @@ public void SearchStoreWithBadPin() }, }, }, + TrustLevel = new string[] { "None" }, + Explicit = false, }, }); diff --git a/src/AppInstallerCLIE2ETests/SourceCommand.cs b/src/AppInstallerCLIE2ETests/SourceCommand.cs index ff034f18e8..128a1b4678 100644 --- a/src/AppInstallerCLIE2ETests/SourceCommand.cs +++ b/src/AppInstallerCLIE2ETests/SourceCommand.cs @@ -35,6 +35,58 @@ public void SourceAdd() TestCommon.RunAICLICommand("source remove", $"-n SourceTest"); } + /// + /// Test source add with trust level. + /// + [Test] + public void SourceAddWithTrustLevel() + { + var result = TestCommon.RunAICLICommand("source add", $"SourceTest {Constants.TestSourceUrl} --trust-level trusted"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Done")); + + var listResult = TestCommon.RunAICLICommand("source list", $"-n SourceTest"); + Assert.AreEqual(Constants.ErrorCode.S_OK, listResult.ExitCode); + Assert.True(listResult.StdOut.Contains("Trust Level")); + Assert.True(listResult.StdOut.Contains("Trusted")); + TestCommon.RunAICLICommand("source remove", $"-n SourceTest"); + } + + /// + /// Test source add with store origin trust level. + /// + [Test] + public void SourceAddWithStoreOriginTrustLevel() + { + var result = TestCommon.RunAICLICommand("source add", $"SourceTest {Constants.TestSourceUrl} --trust-level storeOrigin"); + Assert.AreEqual(Constants.ErrorCode.ERROR_SOURCE_DATA_INTEGRITY_FAILURE, result.ExitCode); + Assert.True(result.StdOut.Contains("The source data is corrupted or tampered")); + } + + /// + /// Test source add with explicit flag. Packages should only appear if the source is explicitly declared. + /// + [Test] + public void SourceAddWithExplicit() + { + // Remove the test source. + TestCommon.RunAICLICommand("source remove", "TestSource"); + + var result = TestCommon.RunAICLICommand("source add", $"SourceTest {Constants.TestSourceUrl} --explicit"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Done")); + + var searchResult = TestCommon.RunAICLICommand("search", "TestExampleInstaller"); + Assert.AreEqual(Constants.ErrorCode.ERROR_NO_SOURCES_DEFINED, searchResult.ExitCode); + Assert.True(searchResult.StdOut.Contains("No sources defined; add one with 'source add' or reset to defaults with 'source reset'")); + + var searchResult2 = TestCommon.RunAICLICommand("search", "TestExampleInstaller --source SourceTest"); + Assert.AreEqual(Constants.ErrorCode.S_OK, searchResult2.ExitCode); + Assert.True(searchResult2.StdOut.Contains("TestExampleInstaller")); + Assert.True(searchResult2.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); + TestCommon.RunAICLICommand("source remove", $"-n SourceTest"); + } + /// /// Test source add with duplicate name. /// @@ -94,6 +146,7 @@ public void SourceListWithName() Assert.True(result.StdOut.Contains(Constants.TestSourceName)); Assert.True(result.StdOut.Contains(Constants.TestSourceUrl)); Assert.True(result.StdOut.Contains("Microsoft.PreIndexed.Package")); + Assert.True(result.StdOut.Contains("Trust Level")); Assert.True(result.StdOut.Contains("Updated")); } @@ -184,4 +237,4 @@ public void SourceForceReset() Assert.False(result.StdOut.Contains(Constants.TestSourceUrl)); } } -} \ No newline at end of file +} diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 3f4c5df8e9..9cb7b8ed43 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -2861,4 +2861,16 @@ Please specify one of them using the --source option to proceed. Sets the value of an admin setting. + + Excludes a source from discovery unless specified + + + Explicit + + + Trust level of the source (none or trusted) + + + Trust Level + \ No newline at end of file diff --git a/src/AppInstallerCLITests/GroupPolicy.cpp b/src/AppInstallerCLITests/GroupPolicy.cpp index 4c8c2356ac..907c55969e 100644 --- a/src/AppInstallerCLITests/GroupPolicy.cpp +++ b/src/AppInstallerCLITests/GroupPolicy.cpp @@ -13,10 +13,10 @@ using namespace std::string_view_literals; namespace { - std::wstring GetSourceJson(std::wstring_view name, std::wstring_view arg, std::wstring_view type, std::wstring_view data, std::wstring_view identifier, std::wstring_view pinningConfig = {}) + std::wstring GetSourceJson(std::wstring_view name, std::wstring_view arg, std::wstring_view type, std::wstring_view data, std::wstring_view identifier, std::wstring_view trustLevel, std::wstring_view isExplicit, std::wstring_view pinningConfig = {}) { std::wstringstream json; - json << L"{ \"Name\":\"" << name << L"\", \"Arg\":\"" << arg << L"\", \"Type\":\"" << type << L"\", \"Data\":\"" << data << L"\", \"Identifier\":\"" << identifier << L"\""; + json << L"{ \"Name\":\"" << name << L"\", \"Arg\":\"" << arg << L"\", \"Type\":\"" << type << L"\", \"Data\":\"" << data << L"\", \"Identifier\":\"" << identifier << L"\", \"TrustLevel\":" << trustLevel << L", \"Explicit\":" << isExplicit; if (!pinningConfig.empty()) { json << L", \"CertificatePinning\":" << pinningConfig; @@ -125,7 +125,7 @@ TEST_CASE("GroupPolicy_Sources", "[groupPolicy]") { // We can read single source correctly auto additionalSourcesKey = RegCreateVolatileSubKey(policiesKey.get(), AdditionalSourcesPolicyKeyName); - SetRegistryValue(additionalSourcesKey.get(), L"0", GetSourceJson(L"source-name", L"source-arg", L"source-type", L"source-data", L"source-identifier"), REG_SZ); + SetRegistryValue(additionalSourcesKey.get(), L"0", GetSourceJson(L"source-name", L"source-arg", L"source-type", L"source-data", L"source-identifier", L"[\"Trusted\", \"StoreOrigin\"]", L"true"), REG_SZ); GroupPolicy groupPolicy{ policiesKey.get() }; auto policy = groupPolicy.GetValue(); @@ -136,6 +136,9 @@ TEST_CASE("GroupPolicy_Sources", "[groupPolicy]") REQUIRE(policy.value()[0].Type == "source-type"); REQUIRE(policy.value()[0].Data == "source-data"); REQUIRE(policy.value()[0].Identifier == "source-identifier"); + REQUIRE(policy.value()[0].TrustLevel[0] == "Trusted"); + REQUIRE(policy.value()[0].TrustLevel[1] == "StoreOrigin"); + REQUIRE(policy.value()[0].Explicit == true); } SECTION("Missing field") { @@ -198,9 +201,9 @@ TEST_CASE("GroupPolicy_Sources", "[groupPolicy]") // We should be able to read multiple values. // No specific order is required, but it will likely be the same. auto additionalSourcesKey = RegCreateVolatileSubKey(policiesKey.get(), AdditionalSourcesPolicyKeyName); - SetRegistryValue(additionalSourcesKey.get(), L"0", GetSourceJson(L"s0-name", L"s0-arg", L"s0-type", L"s0-data", L"s0-identifier"), REG_SZ); - SetRegistryValue(additionalSourcesKey.get(), L"1", GetSourceJson(L"s1-name", L"s1-arg", L"s1-type", L"s1-data", L"s1-identifier"), REG_SZ); - SetRegistryValue(additionalSourcesKey.get(), L"2", GetSourceJson(L"s2-name", L"s2-arg", L"s2-type", L"s2-data", L"s2-identifier"), REG_SZ); + SetRegistryValue(additionalSourcesKey.get(), L"0", GetSourceJson(L"s0-name", L"s0-arg", L"s0-type", L"s0-data", L"s0-identifier", L"[\"None\"]", L"true"), REG_SZ); + SetRegistryValue(additionalSourcesKey.get(), L"1", GetSourceJson(L"s1-name", L"s1-arg", L"s1-type", L"s1-data", L"s1-identifier", L"[\"Trusted\", \"StoreOrigin\"]", L"false"), REG_SZ); + SetRegistryValue(additionalSourcesKey.get(), L"2", GetSourceJson(L"s2-name", L"s2-arg", L"s2-type", L"s2-data", L"s2-identifier", L"[\"StoreOrigin\", \"Trusted\"]", L"true"), REG_SZ); GroupPolicy groupPolicy{ policiesKey.get() }; auto policy = groupPolicy.GetValue(); @@ -212,26 +215,34 @@ TEST_CASE("GroupPolicy_Sources", "[groupPolicy]") REQUIRE(policy.value()[0].Type == "s0-type"); REQUIRE(policy.value()[0].Data == "s0-data"); REQUIRE(policy.value()[0].Identifier == "s0-identifier"); + REQUIRE(policy.value()[0].TrustLevel[0] == "None"); + REQUIRE(policy.value()[0].Explicit == true); REQUIRE(policy.value()[1].Name == "s1-name"); REQUIRE(policy.value()[1].Arg == "s1-arg"); REQUIRE(policy.value()[1].Type == "s1-type"); REQUIRE(policy.value()[1].Data == "s1-data"); REQUIRE(policy.value()[1].Identifier == "s1-identifier"); + REQUIRE(policy.value()[1].TrustLevel[0] == "Trusted"); + REQUIRE(policy.value()[1].TrustLevel[1] == "StoreOrigin"); + REQUIRE(policy.value()[1].Explicit == false); REQUIRE(policy.value()[2].Name == "s2-name"); REQUIRE(policy.value()[2].Arg == "s2-arg"); REQUIRE(policy.value()[2].Type == "s2-type"); REQUIRE(policy.value()[2].Data == "s2-data"); REQUIRE(policy.value()[2].Identifier == "s2-identifier"); + REQUIRE(policy.value()[2].TrustLevel[0] == "StoreOrigin"); + REQUIRE(policy.value()[2].TrustLevel[1] == "Trusted"); + REQUIRE(policy.value()[2].Explicit == true); } SECTION("Invalid source in list") { // If a single source is invalid we should still get all others auto additionalSourcesKey = RegCreateVolatileSubKey(policiesKey.get(), AdditionalSourcesPolicyKeyName); - SetRegistryValue(additionalSourcesKey.get(), L"0", GetSourceJson(L"s0-name", L"s0-arg", L"s0-type", L"s0-data", L"s0-identifier"), REG_SZ); + SetRegistryValue(additionalSourcesKey.get(), L"0", GetSourceJson(L"s0-name", L"s0-arg", L"s0-type", L"s0-data", L"s0-identifier", L"[\"Trusted\", \"StoreOrigin\"]", L"false"), REG_SZ); SetRegistryValue(additionalSourcesKey.get(), L"1", L"not a source", REG_SZ); - SetRegistryValue(additionalSourcesKey.get(), L"2", GetSourceJson(L"s2-name", L"s2-arg", L"s2-type", L"s2-data", L"s2-identifier"), REG_SZ); + SetRegistryValue(additionalSourcesKey.get(), L"2", GetSourceJson(L"s2-name", L"s2-arg", L"s2-type", L"s2-data", L"s2-identifier", L"[\"StoreOrigin\", \"Trusted\"]", L"true"), REG_SZ); GroupPolicy groupPolicy{ policiesKey.get() }; auto policy = groupPolicy.GetValue(); @@ -243,12 +254,18 @@ TEST_CASE("GroupPolicy_Sources", "[groupPolicy]") REQUIRE(policy.value()[0].Type == "s0-type"); REQUIRE(policy.value()[0].Data == "s0-data"); REQUIRE(policy.value()[0].Identifier == "s0-identifier"); + REQUIRE(policy.value()[0].TrustLevel[0] == "Trusted"); + REQUIRE(policy.value()[0].TrustLevel[1] == "StoreOrigin"); + REQUIRE(policy.value()[0].Explicit == false); REQUIRE(policy.value()[1].Name == "s2-name"); REQUIRE(policy.value()[1].Arg == "s2-arg"); REQUIRE(policy.value()[1].Type == "s2-type"); REQUIRE(policy.value()[1].Data == "s2-data"); REQUIRE(policy.value()[1].Identifier == "s2-identifier"); + REQUIRE(policy.value()[1].TrustLevel[0] == "StoreOrigin"); + REQUIRE(policy.value()[1].TrustLevel[1] == "Trusted"); + REQUIRE(policy.value()[1].Explicit == true); } SECTION("Exported JSON") { @@ -259,6 +276,8 @@ TEST_CASE("GroupPolicy_Sources", "[groupPolicy]") source.Arg = "json-arg"; source.Data = "json-data"; source.Identifier = "json-id"; + source.TrustLevel = {"Trusted", "StoreOrigin"}; + source.Explicit = false; auto additionalSourcesKey = RegCreateVolatileSubKey(policiesKey.get(), AllowedSourcesPolicyKeyName); SetRegistryValue(additionalSourcesKey.get(), L"0", AppInstaller::Utility::ConvertToUTF16(source.ToJsonString())); @@ -272,6 +291,9 @@ TEST_CASE("GroupPolicy_Sources", "[groupPolicy]") REQUIRE(policy.value()[0].Type == source.Type); REQUIRE(policy.value()[0].Data == source.Data); REQUIRE(policy.value()[0].Identifier == source.Identifier); + REQUIRE(policy.value()[0].TrustLevel[0] == source.TrustLevel[0]); // Trusted + REQUIRE(policy.value()[0].TrustLevel[1] == source.TrustLevel[1]); // StoreOrigin + REQUIRE(policy.value()[0].Explicit == source.Explicit); } SECTION("Source with PinningConfiguration") { @@ -305,7 +327,7 @@ LR"({ }] })"; - SetRegistryValue(additionalSourcesKey.get(), L"0", GetSourceJson(L"source-name", L"source-arg", L"source-type", L"source-data", L"source-identifier", pinningConfig.str()), REG_SZ); + SetRegistryValue(additionalSourcesKey.get(), L"0", GetSourceJson(L"source-name", L"source-arg", L"source-type", L"source-data", L"source-identifier", L"[\"Trusted\", \"StoreOrigin\"]", L"true", pinningConfig.str()), REG_SZ); GroupPolicy groupPolicy{ policiesKey.get() }; auto policy = groupPolicy.GetValue(); @@ -317,6 +339,9 @@ LR"({ REQUIRE(sourceInfo.Type == "source-type"); REQUIRE(sourceInfo.Data == "source-data"); REQUIRE(sourceInfo.Identifier == "source-identifier"); + REQUIRE(sourceInfo.TrustLevel[0] == "Trusted"); + REQUIRE(sourceInfo.TrustLevel[1] == "StoreOrigin"); + REQUIRE(sourceInfo.Explicit == true); // Use loaded pinning config and validate against leaf certificate REQUIRE(!sourceInfo.PinningConfiguration.IsEmpty()); diff --git a/src/AppInstallerCLITests/Sources.cpp b/src/AppInstallerCLITests/Sources.cpp index b19c53102f..b427880794 100644 --- a/src/AppInstallerCLITests/Sources.cpp +++ b/src/AppInstallerCLITests/Sources.cpp @@ -30,6 +30,8 @@ constexpr std::string_view s_SourcesYaml_Source_Name = "Name"sv; constexpr std::string_view s_SourcesYaml_Source_Type = "Type"sv; constexpr std::string_view s_SourcesYaml_Source_Arg = "Arg"sv; constexpr std::string_view s_SourcesYaml_Source_Data = "Data"sv; +constexpr std::string_view s_SourcesYaml_Source_TrustLevel = "TrustLevel"sv; +constexpr std::string_view s_SourcesYaml_Source_Explicit = "Explicit"sv; constexpr std::string_view s_SourcesYaml_Source_LastUpdate = "LastUpdate"sv; constexpr std::string_view s_EmptySources = R"( @@ -172,6 +174,17 @@ constexpr std::string_view s_UserSourceNamedLikeDefault = R"( IsTombstone: false )"sv; +constexpr std::string_view s_SingleSource_TrustLevels_Explicit= R"( +Sources: + - Name: testName + Type: testType + Arg: testArg + Data: testData + IsTombstone: false + TrustLevel: 3 + Explicit: true +)"sv; + namespace { // Helper to create a simple source. @@ -286,6 +299,27 @@ TEST_CASE("RepoSources_SingleSource", "[sources]") RequireDefaultSourcesAt(sources, 1); } +TEST_CASE("RepoSources_SingleSource_TrustLevel_Explicit", "[sources]") +{ + SetSetting(Stream::UserSources, s_SingleSource_TrustLevels_Explicit); + RemoveSetting(Stream::SourcesMetadata); + + std::vector sources = GetSources(); + REQUIRE(sources.size() == c_DefaultSourceCount + 1); + + REQUIRE(sources[0].Name == "testName"); + REQUIRE(sources[0].Type == "testType"); + REQUIRE(sources[0].Arg == "testArg"); + REQUIRE(sources[0].Data == "testData"); + REQUIRE(sources[0].Origin == SourceOrigin::User); + REQUIRE(sources[0].Explicit == true); + REQUIRE(WI_IsFlagSet(sources[0].TrustLevel, SourceTrustLevel::Trusted)); + REQUIRE(WI_IsFlagSet(sources[0].TrustLevel, SourceTrustLevel::StoreOrigin)); + REQUIRE(sources[0].LastUpdateTime == ConvertUnixEpochToSystemClock(0)); + + RequireDefaultSourcesAt(sources, 1); +} + TEST_CASE("RepoSources_ThreeSources", "[sources]") { SetSetting(Stream::UserSources, s_ThreeSources); @@ -332,6 +366,8 @@ TEST_CASE("RepoSources_AddSource", "[sources]") details.Type = "thisIsTheType"; details.Arg = "thisIsTheArg"; details.Data = "thisIsTheData"; + details.TrustLevel = Repository::SourceTrustLevel::None; + details.Explicit = false; bool addCalledOnFactory = false; TestSourceFactory factory{ SourcesTestSource::Create }; @@ -352,6 +388,8 @@ TEST_CASE("RepoSources_AddSource", "[sources]") REQUIRE(sources[0].Data == details.Data); REQUIRE(sources[0].LastUpdateTime != ConvertUnixEpochToSystemClock(0)); REQUIRE(sources[0].Origin == SourceOrigin::User); + REQUIRE(sources[0].TrustLevel == details.TrustLevel); + REQUIRE(sources[0].Explicit == details.Explicit); RequireDefaultSourcesAt(sources, 1); } @@ -1247,7 +1285,7 @@ TEST_CASE("RepoSources_RestoringWellKnownSource", "[sources]") SECTION("with well known name") { - Source addStoreBack{ details.Name, details.Arg, details.Type }; + Source addStoreBack{ details.Name, details.Arg, details.Type, Repository::SourceTrustLevel::None, false }; REQUIRE(addStoreBack.Add(progress)); Source storeAfterAdd{ details.Name }; @@ -1258,7 +1296,7 @@ TEST_CASE("RepoSources_RestoringWellKnownSource", "[sources]") SECTION("with different name") { std::string newName = details.Name + "_new"; - Source addStoreBack{ newName, details.Arg, details.Type }; + Source addStoreBack{ newName, details.Arg, details.Type, Repository::SourceTrustLevel::None, false }; REQUIRE(addStoreBack.Add(progress)); Source storeAfterAdd{ newName }; @@ -1300,4 +1338,4 @@ TEST_CASE("RepoSources_BuiltInDesktopFrameworkSourceAlwaysCreatable", "[sources] { Source source(WellKnownSource::DesktopFrameworks); REQUIRE(source); -} \ No newline at end of file +} diff --git a/src/AppInstallerCLITests/Strings.cpp b/src/AppInstallerCLITests/Strings.cpp index 345fb9a499..a28b45451a 100644 --- a/src/AppInstallerCLITests/Strings.cpp +++ b/src/AppInstallerCLITests/Strings.cpp @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" #include "TestCommon.h" @@ -278,6 +278,11 @@ TEST_CASE("SplitWithSeparator", "[string]") std::vector test3 = Split("test", '.'); REQUIRE(test3.size() == 1); REQUIRE(test3[0] == "test"); + + std::vector test4 = Split(" trim | spaces ", '|', true); + REQUIRE(test4.size() == 2); + REQUIRE(test4[0] == "trim"); + REQUIRE(test4[1] == "spaces"); } TEST_CASE("ConvertGuid", "[string]") diff --git a/src/AppInstallerCLITests/TestSource.cpp b/src/AppInstallerCLITests/TestSource.cpp index 06050b6402..0cd9645b81 100644 --- a/src/AppInstallerCLITests/TestSource.cpp +++ b/src/AppInstallerCLITests/TestSource.cpp @@ -388,7 +388,7 @@ namespace TestCommon bool AddSource(const AppInstaller::Repository::SourceDetails& details, AppInstaller::IProgressCallback& progress) { - Repository::Source source{ details.Name, details.Arg, details.Type }; + Repository::Source source{ details.Name, details.Arg, details.Type, Repository::SourceTrustLevel::None, false }; return source.Add(progress); } diff --git a/src/AppInstallerRepositoryCore/ISource.h b/src/AppInstallerRepositoryCore/ISource.h index 5dc609ee1d..58add7013d 100644 --- a/src/AppInstallerRepositoryCore/ISource.h +++ b/src/AppInstallerRepositoryCore/ISource.h @@ -65,7 +65,7 @@ namespace AppInstaller::Repository return std::shared_ptr(source, reinterpret_cast(castResult)); } - // Internal interface to represents source information; basically SourceDetails but with methods to enable differential behaviors. + // Internal interface to represent source information; basically SourceDetails but with methods to enable differential behaviors. struct ISourceReference { virtual ~ISourceReference() = default; diff --git a/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h index 33a8acc5bf..cd44fe0c64 100644 --- a/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h +++ b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h @@ -45,6 +45,21 @@ namespace AppInstaller::Repository DEFINE_ENUM_FLAG_OPERATORS(SourceTrustLevel); + // Converts a string_view to the corresponding SourceTrustLevel enum. + SourceTrustLevel ConvertToSourceTrustLevelEnum(std::string_view trustLevel); + + // Converts a vector of trust level strings to the corresponding SourceTrustLevel enum flag. + SourceTrustLevel ConvertToSourceTrustLevelFlag(std::vector values); + + // Converts a SourceTrustLevel flag to a list of trust level strings. + std::vector SourceTrustLevelFlagToList(SourceTrustLevel trustLevel); + + // Converts a SourceTrustLevel enum to the corresponding string. + std::string_view SourceTrustLevelEnumToString(SourceTrustLevel trustLevel); + + // Gets the full trust level string name for display. + std::string GetSourceTrustLevelForDisplay(SourceTrustLevel trustLevel); + std::string_view ToString(SourceOrigin origin); // Fields that require user agreements. @@ -135,6 +150,9 @@ namespace AppInstaller::Repository // This value is used as an alternative to the `Arg` value if it is failing to function properly. // The alternate location must point to identical data or inconsistencies may arise. std::string AlternateArg; + + // Whether the source should be hidden by default unless explicitly declared. + bool Explicit = false; }; // Individual source agreement entry. Label will be highlighted in the display as the key of the agreement entry. @@ -200,7 +218,7 @@ namespace AppInstaller::Repository Source(WellKnownSource source); // Constructor for a source to be added. - Source(std::string_view name, std::string_view arg, std::string_view type); + Source(std::string_view name, std::string_view arg, std::string_view type, SourceTrustLevel trustLevel, bool isExplicit); // Constructor for creating a composite source from a list of available sources. Source(const std::vector& availableSources); diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp index f3899b104c..cef2a61891 100644 --- a/src/AppInstallerRepositoryCore/RepositorySource.cpp +++ b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -21,6 +21,7 @@ using namespace AppInstaller::Settings; using namespace std::chrono_literals; +using namespace AppInstaller::Utility::literals; namespace AppInstaller::Repository { @@ -320,6 +321,93 @@ namespace AppInstaller::Repository THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_SOURCE_TYPE); } + SourceTrustLevel ConvertToSourceTrustLevelEnum(std::string_view trustLevel) + { + std::string lowerTrustLevel = Utility::ToLower(trustLevel); + + if (lowerTrustLevel == "storeorigin") + { + return SourceTrustLevel::StoreOrigin; + } + else if (lowerTrustLevel == "trusted") + { + return SourceTrustLevel::Trusted; + } + else if (lowerTrustLevel == "none") + { + return SourceTrustLevel::None; + } + else + { + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + } + } + + std::string_view SourceTrustLevelEnumToString(SourceTrustLevel trustLevel) + { + switch (trustLevel) + { + case SourceTrustLevel::StoreOrigin: + return "StoreOrigin"sv; + case SourceTrustLevel::Trusted: + return "Trusted"sv; + case SourceTrustLevel::None: + return "None"sv; + } + + return "Unknown"sv; + } + + SourceTrustLevel ConvertToSourceTrustLevelFlag(std::vector trustLevels) + { + Repository::SourceTrustLevel result = Repository::SourceTrustLevel::None; + for (auto& trustLevel : trustLevels) + { + Repository::SourceTrustLevel trustLevelEnum = ConvertToSourceTrustLevelEnum(trustLevel); + if (trustLevelEnum == Repository::SourceTrustLevel::None) + { + return Repository::SourceTrustLevel::None; + } + else if (trustLevelEnum == Repository::SourceTrustLevel::Trusted) + { + WI_SetFlag(result, Repository::SourceTrustLevel::Trusted); + } + else if (trustLevelEnum == Repository::SourceTrustLevel::StoreOrigin) + { + WI_SetFlag(result, Repository::SourceTrustLevel::StoreOrigin); + } + else + { + THROW_HR_MSG(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), "Invalid source trust level."); + } + } + + return result; + } + + std::vector SourceTrustLevelFlagToList(SourceTrustLevel trustLevel) + { + std::vector result; + + if (WI_IsFlagSet(trustLevel, Repository::SourceTrustLevel::Trusted)) + { + result.emplace_back(Repository::SourceTrustLevelEnumToString(Repository::SourceTrustLevel::Trusted)); + } + if (WI_IsFlagSet(trustLevel, Repository::SourceTrustLevel::StoreOrigin)) + { + result.emplace_back(Repository::SourceTrustLevelEnumToString(Repository::SourceTrustLevel::StoreOrigin)); + } + + return result; + } + + std::string GetSourceTrustLevelForDisplay(SourceTrustLevel trustLevel) + { + std::vector trustLevelList = Repository::SourceTrustLevelFlagToList(trustLevel); + std::vector locIndList(trustLevelList.begin(), trustLevelList.end()); + return Utility::Join("|"_liv, locIndList); + } + std::string_view ToString(SourceOrigin origin) { switch (origin) @@ -369,7 +457,7 @@ namespace AppInstaller::Repository m_sourceReferences.emplace_back(CreateSourceFromDetails(details)); } - Source::Source(std::string_view name, std::string_view arg, std::string_view type) + Source::Source(std::string_view name, std::string_view arg, std::string_view type, SourceTrustLevel trustLevel, bool isExplicit) { m_isSourceToBeAdded = true; SourceDetails details; @@ -385,6 +473,8 @@ namespace AppInstaller::Repository details.Name = name; details.Arg = arg; details.Type = type; + details.TrustLevel = trustLevel; + details.Explicit = isExplicit; } m_sourceReferences.emplace_back(CreateSourceFromDetails(details)); @@ -442,8 +532,15 @@ namespace AppInstaller::Repository } else if (currentSources.size() == 1) { - AICLI_LOG(Repo, Info, << "Default source requested, only 1 source available, using the only source: " << currentSources[0].get().Name); - InitializeSourceReference(currentSources[0].get().Name); + if (!currentSources[0].get().Explicit) + { + AICLI_LOG(Repo, Info, << "Default source requested, only 1 source available, using the only source: " << currentSources[0].get().Name); + InitializeSourceReference(currentSources[0].get().Name); + } + else + { + AICLI_LOG(Repo, Info, << "Skipping explicit source reference " << currentSources[0].get().Name); + } } else { @@ -451,8 +548,15 @@ namespace AppInstaller::Repository for (auto& source : currentSources) { - AICLI_LOG(Repo, Info, << "Adding to source references " << source.get().Name); - m_sourceReferences.emplace_back(CreateSourceFromDetails(source)); + if (!source.get().Explicit) + { + AICLI_LOG(Repo, Info, << "Adding to source references " << source.get().Name); + m_sourceReferences.emplace_back(CreateSourceFromDetails(source)); + } + else + { + AICLI_LOG(Repo, Info, << "Skipping explicit source reference " << source.get().Name); + } } m_isComposite = true; diff --git a/src/AppInstallerRepositoryCore/SourceList.cpp b/src/AppInstallerRepositoryCore/SourceList.cpp index 197a19f4f1..f161c4bd51 100644 --- a/src/AppInstallerRepositoryCore/SourceList.cpp +++ b/src/AppInstallerRepositoryCore/SourceList.cpp @@ -24,6 +24,8 @@ namespace AppInstaller::Repository constexpr std::string_view s_SourcesYaml_Source_Data = "Data"sv; constexpr std::string_view s_SourcesYaml_Source_Identifier = "Identifier"sv; constexpr std::string_view s_SourcesYaml_Source_IsTombstone = "IsTombstone"sv; + constexpr std::string_view s_SourcesYaml_Source_Explicit = "Explicit"sv; + constexpr std::string_view s_SourcesYaml_Source_TrustLevel = "TrustLevel"sv; constexpr std::string_view s_MetadataYaml_Sources = "Sources"sv; constexpr std::string_view s_MetadataYaml_Source_Name = "Name"sv; @@ -178,6 +180,8 @@ namespace AppInstaller::Repository out << YAML::Key << s_SourcesYaml_Source_Data << YAML::Value << details.Data; out << YAML::Key << s_SourcesYaml_Source_Identifier << YAML::Value << details.Identifier; out << YAML::Key << s_SourcesYaml_Source_IsTombstone << YAML::Value << details.IsTombstone; + out << YAML::Key << s_SourcesYaml_Source_Explicit << YAML::Value << details.Explicit; + out << YAML::Key << s_SourcesYaml_Source_TrustLevel << YAML::Value << static_cast(details.TrustLevel); out << YAML::EndMap; } } @@ -633,7 +637,15 @@ namespace AppInstaller::Repository if (!TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Arg, details.Arg)) { return false; } if (!TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Data, details.Data)) { return false; } if (!TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_IsTombstone, details.IsTombstone)) { return false; } + TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Explicit, details.Explicit, false); TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Identifier, details.Identifier, false); + + int64_t trustLevelValue; + if (TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_TrustLevel, trustLevelValue, false)) + { + details.TrustLevel = static_cast(trustLevelValue); + } + return true; }); @@ -669,9 +681,20 @@ namespace AppInstaller::Repository details.Data = additionalSource.Data; details.Identifier = additionalSource.Identifier; details.Origin = SourceOrigin::GroupPolicy; + details.Explicit = additionalSource.Explicit; #ifndef AICLI_DISABLE_TEST_HOOKS details.CertificatePinningConfiguration = additionalSource.PinningConfiguration; #endif + try + { + details.TrustLevel = Repository::ConvertToSourceTrustLevelFlag(additionalSource.TrustLevel); + } + catch (...) + { + details.TrustLevel = Repository::SourceTrustLevel::None; + AICLI_LOG(Repo, Verbose, << "Invalid source trust level from policy. Trust level set to None."); + } + result.emplace_back(std::move(details)); } } diff --git a/src/AppInstallerSharedLib/AppInstallerStrings.cpp b/src/AppInstallerSharedLib/AppInstallerStrings.cpp index 33ea49e898..c763f8453b 100644 --- a/src/AppInstallerSharedLib/AppInstallerStrings.cpp +++ b/src/AppInstallerSharedLib/AppInstallerStrings.cpp @@ -839,7 +839,7 @@ namespace AppInstaller::Utility return LocIndString{ ssJoin.str() }; } - std::vector Split(const std::string& input, char separator) + std::vector Split(const std::string& input, char separator, bool trim) { std::vector result; size_t startIndex = 0; @@ -848,11 +848,17 @@ namespace AppInstaller::Utility while ((endIndex = input.find(separator, startIndex)) != std::string::npos) { std::string substring = input.substr(startIndex, endIndex - startIndex); + + if (trim) + { + Utility::Trim(substring); + } + result.push_back(substring); startIndex = endIndex + 1; } - result.push_back(input.substr(startIndex)); + result.push_back(trim ? Utility::Trim(input.substr(startIndex)) : input.substr(startIndex)); return result; } diff --git a/src/AppInstallerSharedLib/GroupPolicy.cpp b/src/AppInstallerSharedLib/GroupPolicy.cpp index 7325203238..862c1ccce4 100644 --- a/src/AppInstallerSharedLib/GroupPolicy.cpp +++ b/src/AppInstallerSharedLib/GroupPolicy.cpp @@ -195,11 +195,13 @@ namespace AppInstaller::Settings } }; + // All required fields should be read here. bool allRead = readSourceAttribute("Name", &SourceFromPolicy::Name) && readSourceAttribute("Arg", &SourceFromPolicy::Arg) && readSourceAttribute("Type", &SourceFromPolicy::Type) && readSourceAttribute("Data", &SourceFromPolicy::Data) && readSourceAttribute("Identifier", &SourceFromPolicy::Identifier); + if (!allRead) { return std::nullopt; @@ -217,6 +219,22 @@ namespace AppInstaller::Settings } } #endif + // TrustLevel and Explicit are optional policy fields with default values. + const std::string trustLevelName = "TrustLevel"; + if (sourceJson.isMember(trustLevelName) && sourceJson[trustLevelName].isArray()) + { + const Json::Value in = sourceJson[trustLevelName]; + std::vector result; + result.reserve(in.size()); + std::transform(in.begin(), in.end(), std::back_inserter(result), [](const auto& e) { return e.asString(); }); + source.TrustLevel = result; + } + + const std::string explicitName = "Explicit"; + if (sourceJson.isMember(explicitName) && sourceJson[explicitName].isBool()) + { + source.Explicit = sourceJson[explicitName].asBool(); + } return source; } @@ -336,6 +354,14 @@ namespace AppInstaller::Settings json["Arg"] = Arg; json["Data"] = Data; json["Identifier"] = Identifier; + json["Explicit"] = Explicit; + + // Trust level is represented as an array of trust level strings since there can be multiple flags set. + int trustLevelLength = static_cast(TrustLevel.size()); + for (int i = 0; i < trustLevelLength; ++i) + { + json["TrustLevel"][i] = TrustLevel[i]; + } Json::StreamWriterBuilder writerBuilder; writerBuilder.settings_["indentation"] = ""; @@ -397,4 +423,4 @@ namespace AppInstaller::Settings InstanceInternal(nullptr); } #endif -} \ No newline at end of file +} diff --git a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h index 829a7a7723..51ca0373c6 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h @@ -250,8 +250,8 @@ namespace AppInstaller::Utility // Join a string vector using the provided separator. LocIndString Join(LocIndView separator, const std::vector& vector); - // Splits the string using the provided separator. - std::vector Split(const std::string& input, char separator); + // Splits the string using the provided separator. Entries can also be trimmed. + std::vector Split(const std::string& input, char separator, bool trim = false); // Format an input string by replacing placeholders {index} with provided values at corresponding indices. // Note: After upgrading to C++20, this function should be deprecated in favor of std::format. diff --git a/src/AppInstallerSharedLib/Public/winget/GroupPolicy.h b/src/AppInstallerSharedLib/Public/winget/GroupPolicy.h index 114ef6703a..850526f60c 100644 --- a/src/AppInstallerSharedLib/Public/winget/GroupPolicy.h +++ b/src/AppInstallerSharedLib/Public/winget/GroupPolicy.h @@ -84,6 +84,8 @@ namespace AppInstaller::Settings std::string Type; std::string Data; std::string Identifier; + std::vector TrustLevel; + bool Explicit = false; #ifndef AICLI_DISABLE_TEST_HOOKS Certificates::PinningConfiguration PinningConfiguration;