blob: afb58e1de5d2e66a9d1b240b96bbdd6bc1dba175 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/memory/raw_ptr.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/values_test_util.h"
#include "chrome/browser/extensions/extension_management.h"
#include "chrome/browser/extensions/extension_management_internal.h"
#include "chrome/browser/extensions/extension_service_test_base.h"
#include "chrome/browser/extensions/manifest_v2_experiment_manager.h"
#include "chrome/browser/extensions/mv2_experiment_stage.h"
#include "chrome/browser/profiles/profile.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "extensions/browser/pref_names.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/mojom/manifest.mojom.h"
namespace extensions {
namespace {
// Hashed ID for 'aaaa...a'.
constexpr char kTestHashedIdA[] = "68F84A59A3CA2D0E5CB1646FBB164DA409B5D8F2";
// Hashed ID for 'bbbb...b'.
constexpr char kTestHashedIdB[] = "142E27CA6D179970507F4076E2AC96FEC5834F82";
// The setting of the MV2 policy to use.
enum class MV2PolicyLevel {
// Unset. Default browser behavior.
kUnset,
// All MV2 extensions are allowed.
kAllowed,
// All MV2 extensions are disallowed.
kDisallowed,
// Only MV2 extensions installed by admin policy are allowed.
kAllowedForAdminInstalledOnly,
};
// A test variant that allows parameterization of both the experiment stage and
// the policy level.
using TestVariant = std::tuple<MV2ExperimentStage, MV2PolicyLevel>;
// Describes the current test variants; used in describing parameterized tests.
std::string DescribeTestVariant(const TestVariant& test_variant) {
const MV2ExperimentStage experiment_stage = std::get<0>(test_variant);
const MV2PolicyLevel policy_level = std::get<1>(test_variant);
std::string description;
switch (policy_level) {
case MV2PolicyLevel::kUnset:
description += "PolicyUnset";
break;
case MV2PolicyLevel::kAllowed:
description += "MV2AllowedByPolicy";
break;
case MV2PolicyLevel::kDisallowed:
description += "MV2DisallowedByPolicy";
break;
case MV2PolicyLevel::kAllowedForAdminInstalledOnly:
description += "MV2ForAdminInstalledOnly";
break;
}
description += "And";
switch (experiment_stage) {
case MV2ExperimentStage::kWarning:
description += "WarningExperiment";
break;
case MV2ExperimentStage::kDisableWithReEnable:
description += "DisableExperiment";
break;
case MV2ExperimentStage::kUnsupported:
description += "UnsupportedExperiment";
break;
}
return description;
}
} // namespace
class MV2DeprecationImpactCheckerUnitTest
: public ExtensionServiceTestBase,
public testing::WithParamInterface<TestVariant> {
public:
MV2DeprecationImpactCheckerUnitTest();
~MV2DeprecationImpactCheckerUnitTest() override = default;
void SetUp() override {
ExtensionServiceTestBase::SetUp();
// Note: This is (subtly) different from
// `InitializeEmptyExtensionService()`, which doesn't initialize a
// testing PrefService.
InitializeExtensionService(ExtensionServiceInitParams{});
// Sets the current level of the MV2 admin policy.
sync_preferences::TestingPrefServiceSyncable* pref_service =
testing_pref_service();
std::optional<internal::GlobalSettings::ManifestV2Setting> pref_value;
switch (mv2_policy_level_) {
case MV2PolicyLevel::kUnset:
break; // Don't set the policy.
case MV2PolicyLevel::kAllowed:
pref_value = internal::GlobalSettings::ManifestV2Setting::kEnabled;
break;
case MV2PolicyLevel::kDisallowed:
pref_value = internal::GlobalSettings::ManifestV2Setting::kDisabled;
break;
case MV2PolicyLevel::kAllowedForAdminInstalledOnly:
pref_value = internal::GlobalSettings::ManifestV2Setting::
kEnabledForForceInstalled;
break;
}
if (pref_value) {
pref_service->SetManagedPref(pref_names::kManifestV2Availability,
base::Value(static_cast<int>(*pref_value)));
}
impact_checker_ = std::make_unique<MV2DeprecationImpactChecker>(
ExtensionManagementFactory::GetForBrowserContext(profile()));
}
void TearDown() override {
impact_checker_ = nullptr;
ExtensionServiceTestBase::TearDown();
}
// Since this is testing the MV2 deprecation experiments, we don't want to
// bypass their disabling for testing.
bool ShouldAllowMV2Extensions() override { return false; }
// Adds a new force-installed extension with the given `name`,
// `manifest_location`, and `manifest_version`.
scoped_refptr<const Extension> AddForceInstalledExtension(
const std::string& name,
mojom::ManifestLocation location,
int manifest_version) {
return AddPolicyInstalledExtension(name, location, manifest_version,
"force_installed");
}
// Adds a new "recommended" extension with the given `name`,
// `manifest_location`, and `manifest_version`. "Recommended" extensions
// are installed and always kept installed, but may be disabled by the user.
scoped_refptr<const Extension> AddRecommendedPolicyExtension(
const std::string& name,
mojom::ManifestLocation location,
int manifest_version) {
return AddPolicyInstalledExtension(name, location, manifest_version,
"normal_installed");
}
// Adds a new extension that's explicitly allowed by admin policy, but is
// not force- or default-installed.
scoped_refptr<const Extension> AddAllowedPolicyExtension(
const std::string& name,
mojom::ManifestLocation location,
int manifest_version) {
return AddPolicyInstalledExtension(name, location, manifest_version,
"allowed");
}
MV2DeprecationImpactChecker* impact_checker() {
return impact_checker_.get();
}
MV2PolicyLevel policy_level() const { return mv2_policy_level_; }
private:
// Helper function to add a policy extension entry.
scoped_refptr<const Extension> AddPolicyInstalledExtension(
const std::string& name,
mojom::ManifestLocation location,
int manifest_version,
const std::string& installation_mode) {
scoped_refptr<const Extension> extension =
ExtensionBuilder(name)
.SetLocation(location)
.SetManifestVersion(manifest_version)
.Build();
sync_preferences::TestingPrefServiceSyncable* pref_service =
testing_pref_service();
const base::Value* existing_value =
pref_service->GetManagedPref(pref_names::kExtensionManagement);
base::Value::Dict new_value;
if (existing_value) {
new_value = existing_value->Clone().TakeDict();
}
new_value.Set(extension->id(),
base::Value::Dict()
.Set("installation_mode", installation_mode)
.Set("update_url", "http://example.com/"));
pref_service->SetManagedPref(pref_names::kExtensionManagement,
std::move(new_value));
return extension;
}
const MV2ExperimentStage experiment_stage_;
const MV2PolicyLevel mv2_policy_level_;
std::unique_ptr<MV2DeprecationImpactChecker> impact_checker_;
};
MV2DeprecationImpactCheckerUnitTest::MV2DeprecationImpactCheckerUnitTest()
: experiment_stage_(std::get<0>(GetParam())),
mv2_policy_level_(std::get<1>(GetParam())) {}
class MV2DeprecationImpactCheckerUnitTestWithAllowlist
: public MV2DeprecationImpactCheckerUnitTest {
public:
MV2DeprecationImpactCheckerUnitTestWithAllowlist() {
std::string params =
base::StringPrintf("%s,%s", kTestHashedIdA, kTestHashedIdB);
feature_list_.InitAndEnableFeatureWithParameters(
extensions_features::kExtensionManifestV2ExceptionList,
{{extensions_features::kExtensionManifestV2ExceptionListParam.name,
params}});
}
~MV2DeprecationImpactCheckerUnitTestWithAllowlist() override = default;
private:
base::test::ScopedFeatureList feature_list_;
};
INSTANTIATE_TEST_SUITE_P(
,
MV2DeprecationImpactCheckerUnitTest,
testing::Combine(
testing::Values(MV2ExperimentStage::kWarning,
MV2ExperimentStage::kDisableWithReEnable,
MV2ExperimentStage::kUnsupported),
testing::Values(MV2PolicyLevel::kUnset,
MV2PolicyLevel::kAllowed,
MV2PolicyLevel::kDisallowed,
MV2PolicyLevel::kAllowedForAdminInstalledOnly)),
[](const testing::TestParamInfo<TestVariant>& info) {
return DescribeTestVariant(info.param);
});
INSTANTIATE_TEST_SUITE_P(
,
MV2DeprecationImpactCheckerUnitTestWithAllowlist,
testing::Combine(
testing::Values(MV2ExperimentStage::kWarning,
MV2ExperimentStage::kDisableWithReEnable,
MV2ExperimentStage::kUnsupported),
testing::Values(MV2PolicyLevel::kUnset,
MV2PolicyLevel::kAllowed,
MV2PolicyLevel::kDisallowed,
MV2PolicyLevel::kAllowedForAdminInstalledOnly)),
[](const testing::TestParamInfo<TestVariant>& info) {
return DescribeTestVariant(info.param);
});
// Tests that user-visible MV2 extensions are properly considered affected by
// the MV2 deprecation experiment.
TEST_P(MV2DeprecationImpactCheckerUnitTest,
UserVisibleMV2ExtensionsAreAffected) {
scoped_refptr<const Extension> user_installed =
ExtensionBuilder("user installed")
.SetLocation(mojom::ManifestLocation::kInternal)
.SetManifestVersion(2)
.Build();
scoped_refptr<const Extension> external_registry =
ExtensionBuilder("external registry")
.SetLocation(mojom::ManifestLocation::kExternalRegistry)
.SetManifestVersion(2)
.Build();
scoped_refptr<const Extension> external_pref =
ExtensionBuilder("external pref")
.SetLocation(mojom::ManifestLocation::kExternalPref)
.SetManifestVersion(2)
.Build();
scoped_refptr<const Extension> external_pref_download =
ExtensionBuilder("external pref download")
.SetLocation(mojom::ManifestLocation::kExternalPrefDownload)
.SetManifestVersion(2)
.Build();
// Unpacked (and commandline) extensions *are* affected by the MV2
// deprecation. They will be treated differently depending on the experiment
// stage, but should be included in e.g. the warning.
scoped_refptr<const Extension> unpacked =
ExtensionBuilder("unpacked")
.SetLocation(mojom::ManifestLocation::kUnpacked)
.SetManifestVersion(2)
.Build();
scoped_refptr<const Extension> commandline =
ExtensionBuilder("commandline")
.SetLocation(mojom::ManifestLocation::kCommandLine)
.SetManifestVersion(2)
.Build();
// These user-facing MV2 extensions would be affected if the experiment is
// active and the policy is anything other than set to "Allowed" (which
// allows all MV2 extensions).
bool expected_affected = policy_level() != MV2PolicyLevel::kAllowed;
EXPECT_EQ(expected_affected,
impact_checker()->IsExtensionAffected(*user_installed));
EXPECT_EQ(expected_affected,
impact_checker()->IsExtensionAffected(*external_registry));
EXPECT_EQ(expected_affected,
impact_checker()->IsExtensionAffected(*external_pref));
EXPECT_EQ(expected_affected,
impact_checker()->IsExtensionAffected(*external_pref_download));
EXPECT_EQ(expected_affected,
impact_checker()->IsExtensionAffected(*unpacked));
EXPECT_EQ(expected_affected,
impact_checker()->IsExtensionAffected(*commandline));
}
// Checks that certain special cases of extensions, such as default-installed
// and installed by OEM, are also affected by the MV2 deprecation experiments.
TEST_P(MV2DeprecationImpactCheckerUnitTest,
DefaultInstalledMV2ExtensionsAreAffected) {
scoped_refptr<const Extension> default_installed =
ExtensionBuilder("default installed")
.SetLocation(mojom::ManifestLocation::kInternal)
.SetManifestVersion(2)
.AddFlags(Extension::WAS_INSTALLED_BY_DEFAULT)
.Build();
scoped_refptr<const Extension> oem_installed =
ExtensionBuilder("oem installed")
.SetLocation(mojom::ManifestLocation::kInternal)
.SetManifestVersion(2)
.AddFlags(Extension::WAS_INSTALLED_BY_DEFAULT &
Extension::WAS_INSTALLED_BY_OEM)
.Build();
// These extensions should be affected if the experiment is enabled and the
// policy isn't set to allow all MV2 extensions.
bool expected_affected = policy_level() != MV2PolicyLevel::kAllowed;
EXPECT_EQ(expected_affected,
impact_checker()->IsExtensionAffected(*default_installed));
EXPECT_EQ(expected_affected,
impact_checker()->IsExtensionAffected(*oem_installed));
}
// Tests that component extensions are not included in the MV2 deprecation
// experiment (they're implementation details of the browser).
TEST_P(MV2DeprecationImpactCheckerUnitTest, ComponentExtensionsAreNotAffected) {
scoped_refptr<const Extension> component =
ExtensionBuilder("component")
.SetLocation(mojom::ManifestLocation::kComponent)
.SetManifestVersion(2)
.Build();
scoped_refptr<const Extension> external_component =
ExtensionBuilder("external component")
.SetLocation(mojom::ManifestLocation::kExternalComponent)
.SetManifestVersion(2)
.Build();
// Component extensions are never affected by the experiment.
EXPECT_FALSE(impact_checker()->IsExtensionAffected(*component));
EXPECT_FALSE(impact_checker()->IsExtensionAffected(*external_component));
}
// Tests that MV3 extensions, of any location, are not affected by the MV2
// deprecation experiment.
TEST_P(MV2DeprecationImpactCheckerUnitTest, NoMV3ExtensionsAreAffected) {
struct {
mojom::ManifestLocation manifest_location;
const char* name;
} test_cases[] = {
{mojom::ManifestLocation::kInternal, "internal"},
{mojom::ManifestLocation::kExternalPref, "external pref"},
{mojom::ManifestLocation::kExternalRegistry, "external registry"},
{mojom::ManifestLocation::kUnpacked, "unpacked"},
{mojom::ManifestLocation::kComponent, "component"},
{mojom::ManifestLocation::kExternalPrefDownload,
"external pref download"},
{mojom::ManifestLocation::kExternalPolicyDownload,
"external policy download"},
{mojom::ManifestLocation::kCommandLine, "command line"},
{mojom::ManifestLocation::kExternalPolicy, "external policy"},
{mojom::ManifestLocation::kExternalComponent, "external component"},
};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.name);
scoped_refptr<const Extension> extension =
ExtensionBuilder(test_case.name)
.SetManifestVersion(3)
.SetLocation(test_case.manifest_location)
.Build();
EXPECT_FALSE(impact_checker()->IsExtensionAffected(*extension));
}
}
// Tests that MV2 policy-installed extensions are affected if and only if the
// policy does not exempt them.
TEST_P(MV2DeprecationImpactCheckerUnitTest,
MV2PolicyInstalledExtensionsMayBeAffected) {
scoped_refptr<const Extension> forced_policy = AddForceInstalledExtension(
"forced policy", mojom::ManifestLocation::kExternalPolicy, 2);
scoped_refptr<const Extension> forced_policy_download =
AddForceInstalledExtension(
"forced policy download",
mojom::ManifestLocation::kExternalPolicyDownload, 2);
scoped_refptr<const Extension> recommended_policy =
AddRecommendedPolicyExtension(
"recommended policy", mojom::ManifestLocation::kExternalPolicy, 2);
scoped_refptr<const Extension> recommended_policy_download =
AddRecommendedPolicyExtension(
"recommended policy download",
mojom::ManifestLocation::kExternalPolicyDownload, 2);
bool policy_installed_mv2_extensions_allowed =
policy_level() == MV2PolicyLevel::kAllowed ||
policy_level() == MV2PolicyLevel::kAllowedForAdminInstalledOnly;
// Policy installs are affected if they are not exempt by policy and if the
// experiment is active.
bool policy_installs_affected = !policy_installed_mv2_extensions_allowed;
EXPECT_EQ(policy_installs_affected,
impact_checker()->IsExtensionAffected(*forced_policy));
EXPECT_EQ(policy_installs_affected,
impact_checker()->IsExtensionAffected(*forced_policy_download));
EXPECT_EQ(policy_installs_affected,
impact_checker()->IsExtensionAffected(*recommended_policy));
EXPECT_EQ(policy_installs_affected, impact_checker()->IsExtensionAffected(
*recommended_policy_download));
}
// Tests that MV2 extensions that are allowed by policy, but not policy-
// installed, are treated as other MV2 extensions.
TEST_P(MV2DeprecationImpactCheckerUnitTest,
MV2PolicyAllowedExtensionsMayBeAffected) {
scoped_refptr<const Extension> allowed_policy = AddAllowedPolicyExtension(
"allowed policy", mojom::ManifestLocation::kExternalPolicy, 2);
scoped_refptr<const Extension> allowed_policy_download =
AddAllowedPolicyExtension(
"allowed policy download",
mojom::ManifestLocation::kExternalPolicyDownload, 2);
// "allowed" policy installs (as opposed to recommend or force-installed
// installs) are affected if the experiment is on and the policy does not
// allow *all* MV2 extensions.
bool all_mv2_extensions_allowed = policy_level() == MV2PolicyLevel::kAllowed;
bool allowed_installs_affected = !all_mv2_extensions_allowed;
EXPECT_EQ(allowed_installs_affected,
impact_checker()->IsExtensionAffected(*allowed_policy));
EXPECT_EQ(allowed_installs_affected,
impact_checker()->IsExtensionAffected(*allowed_policy_download));
}
// Tests that any MV3 extension installed by policy is never affected by
// MV2 experiments.
TEST_P(MV2DeprecationImpactCheckerUnitTest,
MV3PolicyInstalledExtensionsNeverAffected) {
scoped_refptr<const Extension> forced_policy = AddForceInstalledExtension(
"forced policy", mojom::ManifestLocation::kExternalPolicy, 3);
scoped_refptr<const Extension> forced_policy_download =
AddForceInstalledExtension(
"forced policy download",
mojom::ManifestLocation::kExternalPolicyDownload, 3);
scoped_refptr<const Extension> recommended_policy =
AddRecommendedPolicyExtension(
"recommended policy", mojom::ManifestLocation::kExternalPolicy, 3);
scoped_refptr<const Extension> recommended_policy_download =
AddRecommendedPolicyExtension(
"recommended policy download",
mojom::ManifestLocation::kExternalPolicyDownload, 3);
scoped_refptr<const Extension> allowed_policy = AddAllowedPolicyExtension(
"allowed policy", mojom::ManifestLocation::kExternalPolicy, 3);
scoped_refptr<const Extension> allowed_policy_download =
AddAllowedPolicyExtension(
"allowed policy download",
mojom::ManifestLocation::kExternalPolicyDownload, 3);
EXPECT_FALSE(impact_checker()->IsExtensionAffected(*forced_policy));
EXPECT_FALSE(impact_checker()->IsExtensionAffected(*forced_policy_download));
EXPECT_FALSE(impact_checker()->IsExtensionAffected(*recommended_policy));
EXPECT_FALSE(
impact_checker()->IsExtensionAffected(*recommended_policy_download));
EXPECT_FALSE(impact_checker()->IsExtensionAffected(*allowed_policy));
EXPECT_FALSE(impact_checker()->IsExtensionAffected(*allowed_policy_download));
}
// Tests that non-extension "extension-like" things (such as platform apps and
// hosted apps) are not affected by the MV2 deprecation experiment.
TEST_P(MV2DeprecationImpactCheckerUnitTest, NonExtensionsAreNotAffected) {
scoped_refptr<const Extension> platform_app =
ExtensionBuilder("app", ExtensionBuilder::Type::PLATFORM_APP)
.SetManifestVersion(2)
.Build();
ASSERT_TRUE(platform_app->is_platform_app());
static constexpr char kHostedAppManifest[] =
R"({
"name": "hosted app",
"manifest_version": 2,
"version": "0.1",
"app": {"urls": ["http://example.com/"]}
})";
scoped_refptr<const Extension> hosted_app =
ExtensionBuilder()
.SetManifest(base::test::ParseJsonDict(kHostedAppManifest))
.Build();
ASSERT_TRUE(hosted_app->is_hosted_app());
EXPECT_FALSE(impact_checker()->IsExtensionAffected(*platform_app));
EXPECT_FALSE(impact_checker()->IsExtensionAffected(*hosted_app));
}
// Tests the allowlist is taken into account.
TEST_P(MV2DeprecationImpactCheckerUnitTestWithAllowlist, AllowlistWorks) {
scoped_refptr<const Extension> ext_a =
ExtensionBuilder("ext-a")
.SetLocation(mojom::ManifestLocation::kInternal)
.SetManifestVersion(2)
.SetID(std::string(32, 'a'))
.Build();
scoped_refptr<const Extension> ext_b =
ExtensionBuilder("ext-b")
.SetLocation(mojom::ManifestLocation::kInternal)
.SetManifestVersion(2)
.SetID(std::string(32, 'b'))
.Build();
scoped_refptr<const Extension> ext_c =
ExtensionBuilder("ext-c")
.SetLocation(mojom::ManifestLocation::kInternal)
.SetManifestVersion(2)
.SetID(std::string(32, 'c'))
.Build();
// User-facing MV2 extensions would be affected if the experiment is active
// and the policy is anything other than set to "Allowed" (which allows all
// MV2 extensions).
bool expected_affected = policy_level() != MV2PolicyLevel::kAllowed;
// `ext_a` and `ext_b` are in the allowlist, so aren't affected.
EXPECT_FALSE(impact_checker()->IsExtensionAffected(*ext_a));
EXPECT_FALSE(impact_checker()->IsExtensionAffected(*ext_b));
// `ext_c` is not in the allowlist, so is affected if the experiment is active
// and the policy isn't set.
EXPECT_EQ(expected_affected, impact_checker()->IsExtensionAffected(*ext_c));
}
} // namespace extensions