blob: 1b8bca6328622035ab26b620bf6f031dcc6eea2e [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/extensions/api/developer_private/extension_info_generator.h"
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_file_value_serializer.h"
#include "base/json/json_writer.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/types/optional_util.h"
#include "base/values.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/api/developer_private/inspectable_views_finder.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/cws_info_service.h"
#include "chrome/browser/extensions/error_console/error_console.h"
#include "chrome/browser/extensions/extension_action_test_util.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_service_test_with_install.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/extensions/permissions/permissions_test_util.h"
#include "chrome/browser/extensions/permissions/permissions_updater.h"
#include "chrome/browser/extensions/permissions/scripting_permissions_modifier.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/supervised_user/supervised_user_extensions_delegate_impl.h"
#include "chrome/browser/supervised_user/supervised_user_test_util.h"
#include "chrome/browser/ui/toolbar/toolbar_actions_model.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/extensions/api/developer_private.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/branded_strings.h"
#include "chrome/grit/generated_resources.h"
#include "components/crx_file/id_util.h"
#include "components/supervised_user/core/common/features.h"
#include "extensions/browser/blocklist_state.h"
#include "extensions/browser/disable_reason.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/supervised_user_extensions_delegate.h"
#include "extensions/common/api/extension_action/action_info.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/extension_urls.h"
#include "extensions/common/features/feature_channel.h"
#include "extensions/common/permissions/permission_message.h"
#include "extensions/common/permissions/permission_set.h"
#include "extensions/common/permissions/permissions_data.h"
#include "extensions/common/url_pattern.h"
#include "extensions/common/url_pattern_set.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
namespace extensions {
using mojom::ManifestLocation;
namespace developer = api::developer_private;
namespace {
const char kAllHostsPermission[] = "*://*/*";
std::optional<base::Value::Dict> DeserializeJSONTestData(
const base::FilePath& path,
std::string* error) {
JSONFileValueDeserializer deserializer(path);
std::unique_ptr<base::Value> value = deserializer.Deserialize(nullptr, error);
if (!value || !value->is_dict()) {
return std::nullopt;
}
return std::move(*value).TakeDict();
}
// Returns a pointer to the ExtensionInfo for an extension with |id| if it
// is present in |list|.
const developer::ExtensionInfo* GetInfoFromList(
const ExtensionInfoGenerator::ExtensionInfoList& list,
const ExtensionId& id) {
for (const auto& item : list) {
if (item.id == id)
return &item;
}
return nullptr;
}
// Converts the SiteControls hosts list to a JSON string. This makes test
// validation considerably more concise and readable.
std::string SiteControlsToString(
const std::vector<developer::SiteControl>& controls) {
base::Value::List list;
for (const auto& control : controls) {
list.Append(control.ToValue());
}
std::string json;
CHECK(base::JSONWriter::Write(list, &json));
return json;
}
class MockCWSInfoService : public CWSInfoService {
public:
MOCK_METHOD(std::optional<CWSInfoServiceInterface::CWSInfo>,
GetCWSInfo,
(const Extension&),
(const, override));
static CWSInfoService::CWSInfo GetCWSInfoNone() {
return CWSInfoService::CWSInfo{
/*is_present=*/true,
/*is_live=*/true,
/*last_update_time=*/base::Time::Now(),
/*violation_type=*/
extensions::CWSInfoService::CWSViolationType::kNone,
/*unpublished_long_ago=*/false,
/*no_privacy_practice=*/false};
}
static CWSInfoService::CWSInfo GetCWSInfoMalware() {
return CWSInfoService::CWSInfo{
/*is_present=*/true,
/*is_live=*/false,
/*last_update_time=*/base::Time::Now(),
/*violation_type=*/
extensions::CWSInfoService::CWSViolationType::kMalware,
/*unpublished_long_ago=*/false,
/*no_privacy_practice=*/false};
}
static CWSInfoService::CWSInfo GetCWSInfoNoPrivacyPractice() {
return CWSInfoService::CWSInfo{
/*is_present=*/true,
/*is_live=*/false,
/*last_update_time=*/base::Time::Now(),
/*violation_type=*/
extensions::CWSInfoService::CWSViolationType::kNone,
/*unpublished_long_ago=*/false,
/*no_privacy_practice=*/true};
}
};
} // namespace
class ExtensionInfoGeneratorUnitTest : public ExtensionServiceTestWithInstall {
public:
ExtensionInfoGeneratorUnitTest() {}
ExtensionInfoGeneratorUnitTest(const ExtensionInfoGeneratorUnitTest&) =
delete;
ExtensionInfoGeneratorUnitTest& operator=(
const ExtensionInfoGeneratorUnitTest&) = delete;
~ExtensionInfoGeneratorUnitTest() override = default;
protected:
void SetUp() override {
ExtensionServiceTestWithInstall::SetUp();
InitializeExtensionService(GetExtensionServiceInitParams());
extension_action_test_util::CreateToolbarModelForProfile(profile());
feature_list_.InitWithFeatures(
{features::kSafetyHubExtensionsUwSTrigger,
features::kSafetyHubExtensionsOffStoreTrigger,
features::kSafetyHubExtensionsNoPrivacyPracticesTrigger},
/*disabled_features=*/{});
}
// Returns the initialization parameters for the extension service.
virtual ExtensionServiceInitParams GetExtensionServiceInitParams() {
return {};
}
void OnInfoGenerated(std::unique_ptr<developer::ExtensionInfo>* info_out,
ExtensionInfoGenerator::ExtensionInfoList list) {
EXPECT_EQ(1u, list.size());
if (!list.empty()) {
*info_out =
std::make_unique<developer::ExtensionInfo>(std::move(list[0]));
}
std::move(quit_closure_).Run();
}
std::unique_ptr<developer::ExtensionInfo> GenerateExtensionInfo(
const ExtensionId& extension_id) {
std::unique_ptr<developer::ExtensionInfo> info;
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
std::unique_ptr<ExtensionInfoGenerator> generator(
new ExtensionInfoGenerator(browser_context()));
generator->SetCWSInfoServiceForTesting(&mock_cws_info_service_);
generator->CreateExtensionInfo(
extension_id,
base::BindOnce(&ExtensionInfoGeneratorUnitTest::OnInfoGenerated,
base::Unretained(this), base::Unretained(&info)));
run_loop.Run();
return info;
}
// Used to represent all possible triggers for the extension safety
// check.
enum class SafetyCheckWarningReason {
kMalware,
kPolicy,
kUnpublished,
kOffstore,
kUnwantedSoftware,
kNoPrivacyPractice,
kNone,
};
// Ensures that the warning_reason and the safety check strings inside
// the info variable match.
void CheckSafetyCheckDisplayString(SafetyCheckWarningReason warning_reason,
developer::ExtensionInfo* info,
bool extension_state = true) {
int detail_page_string;
int panel_string;
switch (warning_reason) {
case SafetyCheckWarningReason::kMalware:
detail_page_string = IDS_SAFETY_CHECK_EXTENSIONS_MALWARE;
panel_string = IDS_EXTENSIONS_SC_MALWARE;
break;
case SafetyCheckWarningReason::kPolicy:
detail_page_string = IDS_SAFETY_CHECK_EXTENSIONS_POLICY_VIOLATION;
panel_string = extension_state ? IDS_EXTENSIONS_SC_POLICY_VIOLATION_ON
: IDS_EXTENSIONS_SC_POLICY_VIOLATION_OFF;
break;
case SafetyCheckWarningReason::kUnpublished:
detail_page_string = IDS_SAFETY_CHECK_EXTENSIONS_UNPUBLISHED;
panel_string = extension_state ? IDS_EXTENSIONS_SC_UNPUBLISHED_ON
: IDS_EXTENSIONS_SC_UNPUBLISHED_OFF;
break;
case SafetyCheckWarningReason::kOffstore:
detail_page_string = IDS_EXTENSIONS_SAFETY_CHECK_OFFSTORE;
panel_string = extension_state
? IDS_EXTENSIONS_SAFETY_CHECK_OFFSTORE_ON
: IDS_EXTENSIONS_SAFETY_CHECK_OFFSTORE_OFF;
break;
case SafetyCheckWarningReason::kUnwantedSoftware:
detail_page_string = IDS_SAFETY_CHECK_EXTENSIONS_POLICY_VIOLATION;
panel_string = extension_state ? IDS_EXTENSIONS_SC_POLICY_VIOLATION_ON
: IDS_EXTENSIONS_SC_POLICY_VIOLATION_OFF;
break;
case SafetyCheckWarningReason::kNoPrivacyPractice:
detail_page_string = IDS_EXTENSIONS_SAFETY_CHECK_NO_PRIVACY_PRACTICES;
panel_string =
extension_state
? IDS_EXTENSIONS_SAFETY_CHECK_NO_PRIVACY_PRACTICES_ON
: IDS_EXTENSIONS_SAFETY_CHECK_NO_PRIVACY_PRACTICES_OFF;
break;
case SafetyCheckWarningReason::kNone:
EXPECT_FALSE(info->safety_check_text->detail_string.has_value());
EXPECT_FALSE(info->safety_check_text->panel_string.has_value());
return;
}
EXPECT_EQ(info->safety_check_text->detail_string,
l10n_util::GetStringUTF8(detail_page_string));
EXPECT_EQ(info->safety_check_text->panel_string,
l10n_util::GetStringUTF8(panel_string));
}
void OnInfosGenerated(ExtensionInfoGenerator::ExtensionInfoList* out,
ExtensionInfoGenerator::ExtensionInfoList list) {
*out = std::move(list);
std::move(quit_closure_).Run();
}
ExtensionInfoGenerator::ExtensionInfoList GenerateExtensionsInfo() {
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
ExtensionInfoGenerator generator(browser_context());
ExtensionInfoGenerator::ExtensionInfoList result;
generator.CreateExtensionsInfo(
true, /* include_disabled */
true, /* include_terminated */
base::BindOnce(&ExtensionInfoGeneratorUnitTest::OnInfosGenerated,
base::Unretained(this), base::Unretained(&result)));
run_loop.Run();
return result;
}
const scoped_refptr<const Extension> CreateExtension(
const std::string& name,
base::Value::List permissions,
mojom::ManifestLocation location,
const std::string& update_url =
extension_urls::kChromeWebstoreUpdateURL) {
const ExtensionId kId = crx_file::id_util::GenerateId(name);
scoped_refptr<const Extension> extension =
ExtensionBuilder()
.SetManifest(base::Value::Dict()
.Set("name", name)
.Set("description", "an extension")
.Set("manifest_version", 2)
.Set("version", "1.0.0")
.Set("permissions", std::move(permissions))
.Set("update_url", update_url))
.SetLocation(location)
.SetID(kId)
.Build();
service()->AddExtension(extension.get());
PermissionsUpdater updater(profile());
updater.InitializePermissions(extension.get());
updater.GrantActivePermissions(extension.get());
return extension;
}
std::unique_ptr<developer::ExtensionInfo> CreateExtensionInfoFromPath(
const base::FilePath& extension_path,
mojom::ManifestLocation location) {
ChromeTestExtensionLoader loader(browser_context());
// Unit tests are single process and as such, attempting to wait for an
// extension renderer process will cause the test to time out.
loader.set_wait_for_renderers(false);
loader.set_location(location);
loader.set_creation_flags(Extension::REQUIRE_KEY);
scoped_refptr<const Extension> extension =
loader.LoadExtension(extension_path);
CHECK(extension.get());
return GenerateExtensionInfo(extension->id());
}
void CompareExpectedAndActualOutput(
const base::FilePath& extension_path,
InspectableViewsFinder::ViewList views,
const base::FilePath& expected_output_path) {
std::string error;
std::optional<base::Value::Dict> expected_output_data =
DeserializeJSONTestData(expected_output_path, &error);
ASSERT_TRUE(expected_output_data);
EXPECT_EQ(std::string(), error);
// Produce test output.
std::unique_ptr<developer::ExtensionInfo> info =
CreateExtensionInfoFromPath(extension_path,
mojom::ManifestLocation::kUnpacked);
info->views = std::move(views);
base::Value::Dict actual_output_data = info->ToValue();
// Compare the outputs.
// Ignore unknown fields in the actual output data.
std::string paths_details = " - expected (" +
expected_output_path.MaybeAsASCII() + ") vs. actual (" +
extension_path.MaybeAsASCII() + ")";
std::string expected_string;
std::string actual_string;
for (auto field : *expected_output_data) {
const base::Value& expected_value = field.second;
base::Value* actual_value =
actual_output_data.FindByDottedPath(field.first);
EXPECT_TRUE(actual_value) << field.first + " is missing" + paths_details;
if (!actual_value)
continue;
if (*actual_value != expected_value) {
base::JSONWriter::Write(expected_value, &expected_string);
base::JSONWriter::Write(*actual_value, &actual_string);
EXPECT_EQ(expected_string, actual_string)
<< field.first << paths_details;
}
}
}
testing::NiceMock<MockCWSInfoService> mock_cws_info_service_;
private:
base::test::ScopedFeatureList feature_list_;
base::OnceClosure quit_closure_;
};
// Test some of the basic fields.
TEST_F(ExtensionInfoGeneratorUnitTest, BasicInfoTest) {
profile()->GetPrefs()->SetBoolean(prefs::kExtensionsUIDeveloperMode, true);
const char kName[] = "extension name";
const char kVersion[] = "1.0.0.1";
ExtensionId id = crx_file::id_util::GenerateId("alpha");
base::Value::Dict manifest =
base::Value::Dict()
.Set("name", kName)
.Set("version", kVersion)
.Set("manifest_version", 3)
.Set("description", "an extension")
.Set("host_permissions", base::Value::List()
.Append("file://*/*")
.Append("*://*.google.com/*")
.Append("*://*.example.com/*")
.Append("*://*.foo.bar/*")
.Append("*://*.chromium.org/*"))
.Set("permissions", base::Value::List().Append("tabs"));
base::Value::Dict manifest_copy = manifest.Clone();
scoped_refptr<const Extension> extension =
ExtensionBuilder()
.SetManifest(std::move(manifest))
.SetLocation(ManifestLocation::kUnpacked)
.SetPath(data_dir())
.SetID(id)
.Build();
service()->AddExtension(extension.get());
ErrorConsole* error_console = ErrorConsole::Get(profile());
const GURL kContextUrl("http://example.com");
error_console->ReportError(std::make_unique<RuntimeError>(
extension->id(), false, u"source", u"message",
StackTrace(1, StackFrame(1, 1, u"source", u"function")), kContextUrl,
logging::LOGGING_ERROR, 1, 1));
error_console->ReportError(std::make_unique<ManifestError>(
extension->id(), u"message", "key", std::u16string()));
error_console->ReportError(std::make_unique<RuntimeError>(
extension->id(), false, u"source", u"message",
StackTrace(1, StackFrame(1, 1, u"source", u"function")), kContextUrl,
logging::LOGGING_WARNING, 1, 1));
// It's not feasible to validate every field here, because that would be
// a duplication of the logic in the method itself. Instead, test a handful
// of fields for sanity.
std::unique_ptr<api::developer_private::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
ASSERT_TRUE(info.get());
EXPECT_EQ(kName, info->name);
EXPECT_EQ(id, info->id);
EXPECT_EQ(kVersion, info->version);
EXPECT_EQ(info->location, developer::Location::kUnpacked);
ASSERT_TRUE(info->path);
EXPECT_EQ(data_dir(), base::FilePath::FromUTF8Unsafe(*info->path));
EXPECT_EQ(api::developer_private::ExtensionState::kEnabled, info->state);
EXPECT_EQ(api::developer_private::ExtensionType::kExtension, info->type);
EXPECT_TRUE(info->file_access.is_enabled);
EXPECT_FALSE(info->file_access.is_active);
EXPECT_TRUE(info->incognito_access.is_enabled);
EXPECT_FALSE(info->incognito_access.is_active);
EXPECT_TRUE(base::StartsWith(info->icon_url, "data:image/png;base64,"));
EXPECT_FALSE(*info->pinned_to_toolbar);
// Strip out the kHostReadWrite permission created by the extension requesting
// host permissions above; runtime host permissions mean these are always
// present but not necessarily operative. There should only be one entry,
// though. This is necessary because the code below wants to assert that every
// entry in |messages| has a matching entry in
// |info->permissions.simple_permissions|, and kHostReadWrite is not a simple
// permission.
PermissionMessages messages;
for (const PermissionMessage& message :
extension->permissions_data()->GetPermissionMessages()) {
if (!message.permissions().ContainsID(
extensions::mojom::APIPermissionID::kHostReadWrite)) {
messages.push_back(message);
}
}
ASSERT_EQ(messages.size(), info->permissions.simple_permissions.size());
size_t i = 0;
for (const PermissionMessage& message : messages) {
const api::developer_private::Permission& info_permission =
info->permissions.simple_permissions[i];
EXPECT_EQ(message.message(), base::UTF8ToUTF16(info_permission.message));
const std::vector<std::u16string>& submessages = message.submessages();
ASSERT_EQ(submessages.size(), info_permission.submessages.size());
for (size_t j = 0; j < submessages.size(); ++j) {
EXPECT_EQ(submessages[j],
base::UTF8ToUTF16(info_permission.submessages[j]));
}
++i;
}
EXPECT_TRUE(info->permissions.runtime_host_permissions);
EXPECT_TRUE(info->permissions.can_access_site_data);
ASSERT_EQ(2u, info->runtime_errors.size());
const api::developer_private::RuntimeError& runtime_error =
info->runtime_errors[0];
EXPECT_EQ(extension->id(), runtime_error.extension_id);
EXPECT_EQ(api::developer_private::ErrorType::kRuntime, runtime_error.type);
EXPECT_EQ(api::developer_private::ErrorLevel::kError, runtime_error.severity);
EXPECT_EQ(kContextUrl, GURL(runtime_error.context_url));
EXPECT_EQ(1u, runtime_error.stack_trace.size());
ASSERT_EQ(1u, info->manifest_errors.size());
const api::developer_private::RuntimeError& runtime_error_verbose =
info->runtime_errors[1];
EXPECT_EQ(api::developer_private::ErrorLevel::kWarn,
runtime_error_verbose.severity);
const api::developer_private::ManifestError& manifest_error =
info->manifest_errors[0];
EXPECT_EQ(extension->id(), manifest_error.extension_id);
// Test an extension that isn't unpacked.
manifest_copy.Set("update_url",
"https://clients2.google.com/service/update2/crx");
id = crx_file::id_util::GenerateId("beta");
extension = ExtensionBuilder()
.SetManifest(std::move(manifest_copy))
.SetLocation(ManifestLocation::kExternalPref)
.SetID(id)
.Build();
service()->AddExtension(extension.get());
info = GenerateExtensionInfo(extension->id());
EXPECT_EQ(developer::Location::kThirdParty, info->location);
EXPECT_FALSE(info->path);
}
// Tests that the correct location field is returned for an extension that's
// installed by default.
TEST_F(ExtensionInfoGeneratorUnitTest, ExtensionInfoInstalledByDefault) {
profile()->GetPrefs()->SetBoolean(prefs::kExtensionsUIDeveloperMode, true);
base::Value::Dict manifest =
base::Value::Dict()
.Set("name", "installed by default")
.Set("version", "1.2")
.Set("manifest_version", 3)
.Set("update_url", "https://clients2.google.com/service/update2/crx");
scoped_refptr<const Extension> extension =
ExtensionBuilder()
.SetManifest(std::move(manifest))
.SetLocation(ManifestLocation::kExternalPref)
.SetPath(data_dir())
.SetID(crx_file::id_util::GenerateId("alpha"))
.AddFlags(Extension::WAS_INSTALLED_BY_DEFAULT)
.Build();
service()->AddExtension(extension.get());
std::unique_ptr<api::developer_private::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
EXPECT_EQ(info->location, developer::Location::kInstalledByDefault);
}
// Tests that the correct location field is returned for an extension that's
// installed by the OEM.
TEST_F(ExtensionInfoGeneratorUnitTest, ExtensionInfoInstalledByOem) {
profile()->GetPrefs()->SetBoolean(prefs::kExtensionsUIDeveloperMode, true);
base::Value::Dict manifest =
base::Value::Dict()
.Set("name", "installed by OEM")
.Set("version", "1.2")
.Set("manifest_version", 3)
.Set("update_url", "https://clients2.google.com/service/update2/crx");
scoped_refptr<const Extension> extension =
ExtensionBuilder()
.SetManifest(std::move(manifest))
.SetLocation(ManifestLocation::kExternalPref)
.SetPath(data_dir())
.SetID(crx_file::id_util::GenerateId("alpha"))
.AddFlags(Extension::WAS_INSTALLED_BY_DEFAULT |
Extension::WAS_INSTALLED_BY_OEM)
.Build();
service()->AddExtension(extension.get());
std::unique_ptr<api::developer_private::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
EXPECT_EQ(info->location, developer::Location::kThirdParty);
}
// Test three generated json outputs.
TEST_F(ExtensionInfoGeneratorUnitTest, GenerateExtensionsJSONData) {
// Test Extension1
base::FilePath extension_path =
data_dir().AppendASCII("good")
.AppendASCII("Extensions")
.AppendASCII("behllobkkfkfnphdnhnkndlbkcpglgmj")
.AppendASCII("1.0.0.0");
base::FilePath expected_outputs_path =
data_dir().AppendASCII("api_test")
.AppendASCII("developer")
.AppendASCII("generated_output");
{
InspectableViewsFinder::ViewList views;
views.push_back(InspectableViewsFinder::ConstructView(
GURL("chrome-extension://behllobkkfkfnphdnhnkndlbkcpglgmj/bar.html"),
42, 88, true, false, api::developer_private::ViewType::kTabContents));
views.push_back(InspectableViewsFinder::ConstructView(
GURL("chrome-extension://behllobkkfkfnphdnhnkndlbkcpglgmj/dog.html"), 0,
0, false, true, api::developer_private::ViewType::kTabContents));
CompareExpectedAndActualOutput(
extension_path, std::move(views),
expected_outputs_path.AppendASCII(
"behllobkkfkfnphdnhnkndlbkcpglgmj.json"));
}
#if !BUILDFLAG(IS_CHROMEOS_ASH)
// Test Extension2
extension_path = data_dir()
.AppendASCII("good")
.AppendASCII("Extensions")
.AppendASCII("hpiknbiabeeppbpihjehijgoemciehgk")
.AppendASCII("2");
{
// It's OK to have duplicate URLs, so long as the IDs are different.
InspectableViewsFinder::ViewList views;
views.push_back(InspectableViewsFinder::ConstructView(
GURL("chrome-extension://hpiknbiabeeppbpihjehijgoemciehgk/bar.html"),
42, 88, true, false, api::developer_private::ViewType::kTabContents));
views.push_back(InspectableViewsFinder::ConstructView(
GURL("chrome-extension://hpiknbiabeeppbpihjehijgoemciehgk/bar.html"), 0,
0, false, true, api::developer_private::ViewType::kTabContents));
CompareExpectedAndActualOutput(
extension_path, std::move(views),
expected_outputs_path.AppendASCII(
"hpiknbiabeeppbpihjehijgoemciehgk.json"));
}
#endif
// Test Extension3
extension_path = data_dir().AppendASCII("good")
.AppendASCII("Extensions")
.AppendASCII("bjafgdebaacbbbecmhlhpofkepfkgcpa")
.AppendASCII("1.0");
CompareExpectedAndActualOutput(extension_path,
InspectableViewsFinder::ViewList(),
expected_outputs_path.AppendASCII(
"bjafgdebaacbbbecmhlhpofkepfkgcpa.json"));
}
TEST_F(ExtensionInfoGeneratorUnitTest, SafetyCheckStringsTest_Malware) {
const scoped_refptr<const Extension> extension =
CreateExtension("test", base::Value::List(), ManifestLocation::kInternal);
{
// CWSInfo - Malware.
EXPECT_CALL(mock_cws_info_service_, GetCWSInfo)
.Times(1)
.WillOnce(testing::Return(MockCWSInfoService::GetCWSInfoMalware()));
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
CheckSafetyCheckDisplayString(SafetyCheckWarningReason::kMalware,
info.get());
}
{
// Blocklist - Malware.
service()->BlocklistExtensionForTest(extension->id());
EXPECT_CALL(mock_cws_info_service_, GetCWSInfo)
.Times(1)
.WillOnce(testing::Return(MockCWSInfoService::GetCWSInfoMalware()));
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
CheckSafetyCheckDisplayString(SafetyCheckWarningReason::kMalware,
info.get());
}
}
TEST_F(ExtensionInfoGeneratorUnitTest,
SafetyCheckStringsTest_NoPrivacyPractice) {
// CWSInfo - No Privacy Practice.
const scoped_refptr<const Extension> extension =
CreateExtension("test", base::Value::List(), ManifestLocation::kInternal);
EXPECT_CALL(mock_cws_info_service_, GetCWSInfo)
.Times(1)
.WillOnce(
testing::Return(MockCWSInfoService::GetCWSInfoNoPrivacyPractice()));
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
CheckSafetyCheckDisplayString(SafetyCheckWarningReason::kNoPrivacyPractice,
info.get());
}
TEST_F(ExtensionInfoGeneratorUnitTest, SafetyCheckStringsTest_OffStore) {
const scoped_refptr<const Extension> extension_unpacked =
CreateExtension("test", base::Value::List(), ManifestLocation::kUnpacked);
CWSInfoService::CWSInfo cws_no_trigger = {
/*is_present=*/true,
/*is_live=*/true,
/*last_update_time=*/base::Time::Now(),
/*violation_type=*/
extensions::CWSInfoService::CWSViolationType::kNone,
/*unpublished_long_ago=*/false,
/*no_privacy_practice=*/false};
{
// CWSInfo - No Trigger - Unpacked extension not in dev mode.
EXPECT_CALL(mock_cws_info_service_, GetCWSInfo)
.Times(1)
.WillOnce(testing::Return(cws_no_trigger));
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension_unpacked->id());
CheckSafetyCheckDisplayString(SafetyCheckWarningReason::kOffstore,
info.get());
}
{
// CWSInfo - No Trigger - Unpacked extension in dev mode.
profile()->GetPrefs()->SetBoolean(prefs::kExtensionsUIDeveloperMode, true);
EXPECT_CALL(mock_cws_info_service_, GetCWSInfo)
.Times(1)
.WillOnce(testing::Return(cws_no_trigger));
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension_unpacked->id());
CheckSafetyCheckDisplayString(SafetyCheckWarningReason::kNone, info.get());
}
{
// CWSInfo - No Trigger - Extension does not update from the webstore.
const scoped_refptr<const Extension> extension_not_webstore =
CreateExtension("test", base::Value::List(),
ManifestLocation::kInternal, "https://example.com");
EXPECT_CALL(mock_cws_info_service_, GetCWSInfo)
.Times(1)
.WillOnce(testing::Return(cws_no_trigger));
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension_not_webstore->id());
CheckSafetyCheckDisplayString(SafetyCheckWarningReason::kOffstore,
info.get());
}
{
// CWSInfo - Normal extension without CWS info.
const scoped_refptr<const Extension> extension_normal = CreateExtension(
"test", base::Value::List(), ManifestLocation::kInternal);
CWSInfoService::CWSInfo cws_not_present;
profile()->GetPrefs()->SetBoolean(prefs::kExtensionsUIDeveloperMode, false);
EXPECT_CALL(mock_cws_info_service_, GetCWSInfo)
.Times(1)
.WillOnce(testing::Return(cws_not_present));
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension_normal->id());
CheckSafetyCheckDisplayString(SafetyCheckWarningReason::kOffstore,
info.get());
}
{
// CWSInfo - Command line extension without CWS info.
const scoped_refptr<const Extension> extension_command_line =
CreateExtension("test", base::Value::List(),
ManifestLocation::kCommandLine);
CWSInfoService::CWSInfo cws_not_present;
profile()->GetPrefs()->SetBoolean(prefs::kExtensionsUIDeveloperMode, false);
EXPECT_CALL(mock_cws_info_service_, GetCWSInfo)
.Times(1)
.WillOnce(testing::Return(cws_not_present));
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension_command_line->id());
CheckSafetyCheckDisplayString(SafetyCheckWarningReason::kNone, info.get());
}
}
TEST_F(ExtensionInfoGeneratorUnitTest, SafetyCheckStringsTest_PolicyViolation) {
const scoped_refptr<const Extension> extension =
CreateExtension("test", base::Value::List(), ManifestLocation::kInternal);
{
// CWSInfo - Policy with enabled state.
CWSInfoService::CWSInfo cws_info_policy = {
/*is_present=*/true,
/*is_live=*/false,
/*last_update_time=*/base::Time::Now(),
/*violation_type=*/
extensions::CWSInfoService::CWSViolationType::kPolicy,
/*unpublished_long_ago=*/false,
/*no_privacy_practice=*/false};
EXPECT_CALL(mock_cws_info_service_, GetCWSInfo)
.Times(1)
.WillOnce(testing::Return(cws_info_policy));
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
CheckSafetyCheckDisplayString(SafetyCheckWarningReason::kPolicy,
info.get());
}
{
// Blocklist - Policy with disabled state.
service()->GreylistExtensionForTest(
extension->id(),
BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION);
EXPECT_CALL(mock_cws_info_service_, GetCWSInfo)
.Times(1)
.WillOnce(testing::Return(MockCWSInfoService::GetCWSInfoNone()));
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
CheckSafetyCheckDisplayString(SafetyCheckWarningReason::kPolicy, info.get(),
false);
}
}
TEST_F(ExtensionInfoGeneratorUnitTest,
SafetyCheckStringsTest_PotentiallyUnwanted) {
const scoped_refptr<const Extension> extension =
CreateExtension("test", base::Value::List(), ManifestLocation::kInternal);
// Blocklist - Potentially unwanted.
service()->GreylistExtensionForTest(
extension->id(), BitMapBlocklistState::BLOCKLISTED_POTENTIALLY_UNWANTED);
EXPECT_CALL(mock_cws_info_service_, GetCWSInfo)
.Times(1)
.WillOnce(testing::Return(MockCWSInfoService::GetCWSInfoNone()));
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
CheckSafetyCheckDisplayString(SafetyCheckWarningReason::kUnwantedSoftware,
info.get(), false);
}
TEST_F(ExtensionInfoGeneratorUnitTest, SafetyCheckStringsTest_DifferentStates) {
const scoped_refptr<const Extension> extension =
CreateExtension("test", base::Value::List(), ManifestLocation::kInternal);
// Extension is greylisted for policy violation, but CWSInfo is malware.
service()->GreylistExtensionForTest(
extension->id(), BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION);
EXPECT_CALL(mock_cws_info_service_, GetCWSInfo)
.Times(1)
.WillOnce(testing::Return(MockCWSInfoService::GetCWSInfoMalware()));
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
// Return the higher violation - malware.
CheckSafetyCheckDisplayString(SafetyCheckWarningReason::kMalware, info.get());
}
TEST_F(ExtensionInfoGeneratorUnitTest, SafetyCheckStringsTest_Unpublished) {
const scoped_refptr<const Extension> extension =
CreateExtension("test", base::Value::List(), ManifestLocation::kInternal);
CWSInfoService::CWSInfo cws_info_unpublished = {
/*is_present=*/true,
/*is_live=*/false,
/*last_update_time=*/base::Time::Now(),
/*violation_type=*/
extensions::CWSInfoService::CWSViolationType::kNone,
/*unpublished_long_ago=*/true,
/*no_privacy_practice=*/false};
{
// CWSInfo - Unpublished with enabled state.
EXPECT_CALL(mock_cws_info_service_, GetCWSInfo)
.Times(1)
.WillOnce(testing::Return(cws_info_unpublished));
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
CheckSafetyCheckDisplayString(SafetyCheckWarningReason::kUnpublished,
info.get());
}
{
// CWSInfo - Unpublished with disabled state.
service()->DisableExtension(
extension->id(),
disable_reason::DISABLE_PUBLISHED_IN_STORE_REQUIRED_BY_POLICY);
EXPECT_CALL(mock_cws_info_service_, GetCWSInfo)
.Times(1)
.WillOnce(testing::Return(cws_info_unpublished));
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
CheckSafetyCheckDisplayString(SafetyCheckWarningReason::kUnpublished,
info.get(), false);
}
}
TEST_F(ExtensionInfoGeneratorUnitTest, SafetyCheckStringsTest_Empty) {
const scoped_refptr<const Extension> extension =
CreateExtension("test", base::Value::List(), ManifestLocation::kInternal);
{
// CWSInfo present and no blocklist states.
EXPECT_CALL(mock_cws_info_service_, GetCWSInfo)
.Times(1)
.WillOnce(testing::Return(MockCWSInfoService::GetCWSInfoNone()));
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
CheckSafetyCheckDisplayString(SafetyCheckWarningReason::kNone, info.get());
}
{
EXPECT_CALL(mock_cws_info_service_, GetCWSInfo)
.Times(1)
.WillOnce(testing::Return(std::nullopt));
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
CheckSafetyCheckDisplayString(SafetyCheckWarningReason::kNone, info.get());
}
}
// Tests the generation of the runtime host permissions entries.
TEST_F(ExtensionInfoGeneratorUnitTest, RuntimeHostPermissions) {
scoped_refptr<const Extension> all_urls_extension = CreateExtension(
"all_urls", base::Value::List().Append(kAllHostsPermission),
ManifestLocation::kInternal);
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(all_urls_extension->id());
// The extension should be set to run on all sites.
ASSERT_TRUE(info->permissions.runtime_host_permissions);
const developer::RuntimeHostPermissions* runtime_hosts =
base::OptionalToPtr(info->permissions.runtime_host_permissions);
EXPECT_EQ(developer::HostAccess::kOnAllSites, runtime_hosts->host_access);
EXPECT_EQ(R"([{"granted":true,"host":"*://*/*"}])",
SiteControlsToString(runtime_hosts->hosts));
EXPECT_TRUE(runtime_hosts->has_all_hosts);
// With runtime host permissions, no host permissions are added to
// |simple_permissions|.
EXPECT_THAT(info->permissions.simple_permissions, testing::IsEmpty());
// Withholding host permissions should result in the extension being set to
// run on click.
ScriptingPermissionsModifier permissions_modifier(profile(),
all_urls_extension);
permissions_modifier.SetWithholdHostPermissions(true);
info = GenerateExtensionInfo(all_urls_extension->id());
runtime_hosts =
base::OptionalToPtr(info->permissions.runtime_host_permissions);
EXPECT_EQ(developer::HostAccess::kOnClick, runtime_hosts->host_access);
EXPECT_EQ(R"([{"granted":false,"host":"*://*/*"}])",
SiteControlsToString(runtime_hosts->hosts));
EXPECT_TRUE(runtime_hosts->has_all_hosts);
EXPECT_THAT(info->permissions.simple_permissions, testing::IsEmpty());
// Granting a host permission should set the extension to run on specific
// sites, and those sites should be in the specific_site_controls.hosts set.
permissions_modifier.GrantHostPermission(GURL("https://example.com"));
info = GenerateExtensionInfo(all_urls_extension->id());
runtime_hosts =
base::OptionalToPtr(info->permissions.runtime_host_permissions);
EXPECT_EQ(developer::HostAccess::kOnSpecificSites,
runtime_hosts->host_access);
EXPECT_EQ(
R"([{"granted":true,"host":"https://example.com/*"},)"
R"({"granted":false,"host":"*://*/*"}])",
SiteControlsToString(runtime_hosts->hosts));
EXPECT_TRUE(runtime_hosts->has_all_hosts);
EXPECT_THAT(info->permissions.simple_permissions, testing::IsEmpty());
// An extension that doesn't request any host permissions should not have
// runtime access controls.
scoped_refptr<const Extension> no_urls_extension = CreateExtension(
"no urls", base::Value::List(), ManifestLocation::kInternal);
info = GenerateExtensionInfo(no_urls_extension->id());
EXPECT_FALSE(info->permissions.runtime_host_permissions);
EXPECT_FALSE(info->permissions.can_access_site_data);
}
// Tests that specific_site_controls is correctly populated when permissions
// are granted by the user beyond what the extension originally requested in the
// manifest.
TEST_F(ExtensionInfoGeneratorUnitTest,
RuntimeHostPermissionsBeyondRequestedScope) {
scoped_refptr<const Extension> extension =
CreateExtension("extension", base::Value::List().Append("http://*/*"),
ManifestLocation::kInternal);
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
// Withhold permissions, and grant *://chromium.org/*.
ScriptingPermissionsModifier permissions_modifier(profile(), extension);
permissions_modifier.SetWithholdHostPermissions(true);
URLPattern all_chromium(Extension::kValidHostPermissionSchemes,
"*://chromium.org/*");
PermissionSet all_chromium_set(APIPermissionSet(), ManifestPermissionSet(),
URLPatternSet({all_chromium}),
URLPatternSet({all_chromium}));
permissions_test_util::GrantRuntimePermissionsAndWaitForCompletion(
profile(), *extension, all_chromium_set);
// The extension should only be granted http://chromium.org/* (since that's
// the intersection with what it requested).
URLPattern http_chromium(Extension::kValidHostPermissionSchemes,
"http://chromium.org/*");
EXPECT_EQ(PermissionSet(APIPermissionSet(), ManifestPermissionSet(),
URLPatternSet({http_chromium}), URLPatternSet()),
extension->permissions_data()->active_permissions());
// The generated info should use the entirety of the granted permission,
// which is *://chromium.org/*.
info = GenerateExtensionInfo(extension->id());
ASSERT_TRUE(info->permissions.runtime_host_permissions);
const developer::RuntimeHostPermissions* runtime_hosts =
base::OptionalToPtr(info->permissions.runtime_host_permissions);
EXPECT_EQ(developer::HostAccess::kOnSpecificSites,
runtime_hosts->host_access);
EXPECT_EQ(
R"([{"granted":true,"host":"*://chromium.org/*"},)"
R"({"granted":false,"host":"http://*/*"}])",
SiteControlsToString(runtime_hosts->hosts));
EXPECT_TRUE(runtime_hosts->has_all_hosts);
}
// Tests that specific_site_controls is correctly populated when the extension
// requests access to specific hosts.
TEST_F(ExtensionInfoGeneratorUnitTest, RuntimeHostPermissionsSpecificHosts) {
scoped_refptr<const Extension> extension =
CreateExtension("extension",
base::Value::List()
.Append("https://example.com/*")
.Append("https://chromium.org/*"),
ManifestLocation::kInternal);
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
// Withhold permissions, and grant *://chromium.org/*.
ScriptingPermissionsModifier permissions_modifier(profile(), extension);
permissions_modifier.SetWithholdHostPermissions(true);
URLPattern all_chromium(Extension::kValidHostPermissionSchemes,
"https://chromium.org/*");
PermissionSet all_chromium_set(APIPermissionSet(), ManifestPermissionSet(),
URLPatternSet({all_chromium}),
URLPatternSet({all_chromium}));
permissions_test_util::GrantRuntimePermissionsAndWaitForCompletion(
profile(), *extension, all_chromium_set);
// The generated info should use the entirety of the granted permission,
// which is *://chromium.org/*.
info = GenerateExtensionInfo(extension->id());
ASSERT_TRUE(info->permissions.runtime_host_permissions);
const developer::RuntimeHostPermissions* runtime_hosts =
base::OptionalToPtr(info->permissions.runtime_host_permissions);
EXPECT_EQ(developer::HostAccess::kOnSpecificSites,
runtime_hosts->host_access);
EXPECT_EQ(
R"([{"granted":true,"host":"https://chromium.org/*"},)"
R"({"granted":false,"host":"https://example.com/*"}])",
SiteControlsToString(runtime_hosts->hosts));
EXPECT_FALSE(runtime_hosts->has_all_hosts);
}
// Tests that requesting all_url style permissions as a runtime granted pattern
// correctly is treated as having access to all sites.
TEST_F(ExtensionInfoGeneratorUnitTest, RuntimeHostPermissionsAllURLs) {
scoped_refptr<const Extension> all_urls_extension = CreateExtension(
"all_urls", base::Value::List().Append(kAllHostsPermission),
ManifestLocation::kInternal);
// Withholding host permissions should result in the extension being set to
// run on click.
ScriptingPermissionsModifier permissions_modifier(profile(),
all_urls_extension);
permissions_modifier.SetWithholdHostPermissions(true);
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(all_urls_extension->id());
const developer::RuntimeHostPermissions* runtime_hosts =
base::OptionalToPtr(info->permissions.runtime_host_permissions);
EXPECT_EQ(developer::HostAccess::kOnClick, runtime_hosts->host_access);
EXPECT_EQ(R"([{"granted":false,"host":"*://*/*"}])",
SiteControlsToString(runtime_hosts->hosts));
// Grant the requested pattern ("*://*/*").
URLPattern all_url(Extension::kValidHostPermissionSchemes,
kAllHostsPermission);
PermissionSet all_url_set(APIPermissionSet(), ManifestPermissionSet(),
URLPatternSet({all_url}), URLPatternSet({all_url}));
PermissionsUpdater(profile()).GrantRuntimePermissions(
*all_urls_extension, all_url_set, base::DoNothing());
// Now the extension should look like it has access to all hosts, while still
// also counting as having permission withholding enabled.
info = GenerateExtensionInfo(all_urls_extension->id());
runtime_hosts =
base::OptionalToPtr(info->permissions.runtime_host_permissions);
EXPECT_EQ(developer::HostAccess::kOnAllSites, runtime_hosts->host_access);
EXPECT_EQ(R"([{"granted":true,"host":"*://*/*"}])",
SiteControlsToString(runtime_hosts->hosts));
}
// Tests the population of withheld runtime hosts when they overlap with granted
// patterns.
TEST_F(ExtensionInfoGeneratorUnitTest, WithheldUrlsOverlapping) {
scoped_refptr<const Extension> extension =
CreateExtension("extension",
base::Value::List()
.Append("*://example.com/*")
.Append("https://chromium.org/*"),
ManifestLocation::kInternal);
ScriptingPermissionsModifier modifier(profile(), extension);
modifier.SetWithholdHostPermissions(true);
{
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
ASSERT_TRUE(info->permissions.runtime_host_permissions);
// Initially, no hosts are granted.
EXPECT_EQ(
R"([{"granted":false,"host":"*://example.com/*"},)"
R"({"granted":false,"host":"https://chromium.org/*"}])",
SiteControlsToString(
info->permissions.runtime_host_permissions->hosts));
EXPECT_FALSE(info->permissions.runtime_host_permissions->has_all_hosts);
EXPECT_EQ(developer::HostAccess::kOnClick,
info->permissions.runtime_host_permissions->host_access);
}
// Grant http://example.com, which is a subset of the requested host pattern
// (*://example.com).
modifier.GrantHostPermission(GURL("http://example.com/"));
{
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
ASSERT_TRUE(info->permissions.runtime_host_permissions);
// We should display that http://example.com is granted, but *://example.com
// is still requested. This is technically correct.
// TODO(devlin): This is an edge case, so it's okay for it to be a little
// rough (as long as it's not incorrect), but it would be nice to polish it
// out. Ideally, for extensions requesting specific hosts, we'd only allow
// granting/revoking specific patterns (e.g., all example.com sites).
EXPECT_EQ(
R"([{"granted":true,"host":"http://example.com/*"},)"
R"({"granted":false,"host":"*://example.com/*"},)"
R"({"granted":false,"host":"https://chromium.org/*"}])",
SiteControlsToString(
info->permissions.runtime_host_permissions->hosts));
EXPECT_FALSE(info->permissions.runtime_host_permissions->has_all_hosts);
EXPECT_EQ(developer::HostAccess::kOnSpecificSites,
info->permissions.runtime_host_permissions->host_access);
}
// Grant the requested pattern ("*://example.com/*").
{
URLPattern example_com(Extension::kValidHostPermissionSchemes,
"*://example.com/*");
PermissionSet example_com_set(APIPermissionSet(), ManifestPermissionSet(),
URLPatternSet({example_com}),
URLPatternSet({example_com}));
PermissionsUpdater(profile()).GrantRuntimePermissions(
*extension, example_com_set, base::DoNothing());
}
{
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
ASSERT_TRUE(info->permissions.runtime_host_permissions);
// The http://example.com/* pattern should be omitted, since it's consumed
// by the *://example.com/* pattern.
EXPECT_EQ(
R"([{"granted":true,"host":"*://example.com/*"},)"
R"({"granted":false,"host":"https://chromium.org/*"}])",
SiteControlsToString(
info->permissions.runtime_host_permissions->hosts));
EXPECT_FALSE(info->permissions.runtime_host_permissions->has_all_hosts);
EXPECT_EQ(developer::HostAccess::kOnSpecificSites,
info->permissions.runtime_host_permissions->host_access);
}
// Grant permission beyond what was requested (*://*.example.com, when
// subdomains weren't in the extension manifest).
{
URLPattern example_com(Extension::kValidHostPermissionSchemes,
"*://*.example.com/*");
PermissionSet example_com_set(APIPermissionSet(), ManifestPermissionSet(),
URLPatternSet({example_com}),
URLPatternSet({example_com}));
PermissionsUpdater(profile()).GrantRuntimePermissions(
*extension, example_com_set, base::DoNothing());
}
{
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
ASSERT_TRUE(info->permissions.runtime_host_permissions);
// The full granted pattern should be visible.
EXPECT_EQ(
R"([{"granted":true,"host":"*://*.example.com/*"},)"
R"({"granted":false,"host":"https://chromium.org/*"}])",
SiteControlsToString(
info->permissions.runtime_host_permissions->hosts));
EXPECT_FALSE(info->permissions.runtime_host_permissions->has_all_hosts);
EXPECT_EQ(developer::HostAccess::kOnSpecificSites,
info->permissions.runtime_host_permissions->host_access);
}
}
// Tests the population of withheld runtime hosts when they overlap with granted
// patterns.
TEST_F(ExtensionInfoGeneratorUnitTest,
WithheldUrlsOverlappingWithContentScript) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("extension")
.AddPermissions({"*://example.com/*", "*://chromium.org/*"})
.AddContentScript("script.js", {"*://example.com/foo"})
.Build();
{
ExtensionRegistry::Get(profile())->AddEnabled(extension);
PermissionsUpdater updater(profile());
updater.InitializePermissions(extension.get());
updater.GrantActivePermissions(extension.get());
}
ScriptingPermissionsModifier modifier(profile(), extension);
modifier.SetWithholdHostPermissions(true);
{
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
ASSERT_TRUE(info->permissions.runtime_host_permissions);
// Initially, no hosts are granted.
EXPECT_EQ(
R"([{"granted":false,"host":"*://chromium.org/*"},)"
R"({"granted":false,"host":"*://example.com/*"}])",
SiteControlsToString(
info->permissions.runtime_host_permissions->hosts));
EXPECT_FALSE(info->permissions.runtime_host_permissions->has_all_hosts);
EXPECT_EQ(developer::HostAccess::kOnClick,
info->permissions.runtime_host_permissions->host_access);
}
}
// Tests that file:// access checkbox shows up for extensions with activeTab
// permission. See crbug.com/850643.
TEST_F(ExtensionInfoGeneratorUnitTest, ActiveTabFileUrls) {
scoped_refptr<const Extension> extension =
CreateExtension("activeTab", base::Value::List().Append("activeTab"),
ManifestLocation::kInternal);
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
EXPECT_TRUE(extension->wants_file_access());
EXPECT_TRUE(info->file_access.is_enabled);
EXPECT_FALSE(info->file_access.is_active);
}
// Test that `permissions.can_access_site_data` is set to true for extensions
// with API permissions that can access site data, without specifying host
// permissions.
TEST_F(ExtensionInfoGeneratorUnitTest,
CanAccessSiteDataWithoutHostPermissions) {
scoped_refptr<const Extension> active_tab_extension =
CreateExtension("activeTab", base::Value::List().Append("activeTab"),
ManifestLocation::kInternal);
scoped_refptr<const Extension> debugger_extension =
CreateExtension("activeTab", base::Value::List().Append("debugger"),
ManifestLocation::kInternal);
std::unique_ptr<developer::ExtensionInfo> active_tab_info =
GenerateExtensionInfo(active_tab_extension->id());
std::unique_ptr<developer::ExtensionInfo> debugger_info =
GenerateExtensionInfo(debugger_extension->id());
EXPECT_TRUE(active_tab_info->permissions.can_access_site_data);
EXPECT_TRUE(debugger_info->permissions.can_access_site_data);
}
// Tests that blocklisted extensions are returned by the ExtensionInfoGenerator.
TEST_F(ExtensionInfoGeneratorUnitTest, Blocklisted) {
const scoped_refptr<const Extension> extension1 = CreateExtension(
"test1", base::Value::List(), ManifestLocation::kInternal);
const scoped_refptr<const Extension> extension2 = CreateExtension(
"test2", base::Value::List(), ManifestLocation::kInternal);
const ExtensionId& id1 = extension1->id();
const ExtensionId& id2 = extension2->id();
ASSERT_NE(id1, id2);
ExtensionInfoGenerator::ExtensionInfoList info_list =
GenerateExtensionsInfo();
const developer::ExtensionInfo* info1 = GetInfoFromList(info_list, id1);
const developer::ExtensionInfo* info2 = GetInfoFromList(info_list, id2);
ASSERT_NE(nullptr, info1);
ASSERT_NE(nullptr, info2);
EXPECT_EQ(developer::ExtensionState::kEnabled, info1->state);
EXPECT_EQ(developer::ExtensionState::kEnabled, info2->state);
service()->BlocklistExtensionForTest(id1);
info_list = GenerateExtensionsInfo();
info1 = GetInfoFromList(info_list, id1);
info2 = GetInfoFromList(info_list, id2);
ASSERT_NE(nullptr, info1);
ASSERT_NE(nullptr, info2);
EXPECT_EQ(developer::ExtensionState::kBlacklisted, info1->state);
EXPECT_EQ(developer::ExtensionState::kEnabled, info2->state);
// Verify getExtensionInfo() returns data on blocklisted extensions.
auto info3 = GenerateExtensionInfo(id1);
ASSERT_NE(nullptr, info3);
EXPECT_EQ(developer::ExtensionState::kBlacklisted, info3->state);
}
// Test generating extension action commands properly.
TEST_F(ExtensionInfoGeneratorUnitTest, ExtensionActionCommands) {
struct {
const char* name;
const char* command_key;
ActionInfo::Type action_type;
const int manifest_version;
} test_cases[] = {
{"browser action", "_execute_browser_action", ActionInfo::Type::kBrowser,
2},
{"page action", "_execute_page_action", ActionInfo::Type::kPage, 2},
{"action", "_execute_action", ActionInfo::Type::kAction, 3},
};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.name);
base::Value::Dict command_dict =
base::Value::Dict()
.Set("suggested_key",
base::Value::Dict().Set("default", "Ctrl+Shift+P"))
.Set("description", "Execute!");
scoped_refptr<const Extension> extension =
ExtensionBuilder(test_case.name)
.SetAction(test_case.action_type)
.SetManifestKey("commands",
base::Value::Dict().Set(test_case.command_key,
std::move(command_dict)))
.SetManifestVersion(test_case.manifest_version)
.Build();
service()->AddExtension(extension.get());
auto info = GenerateExtensionInfo(extension->id());
ASSERT_TRUE(info);
ASSERT_EQ(1u, info->commands.size());
EXPECT_EQ(test_case.command_key, info->commands[0].name);
EXPECT_TRUE(info->commands[0].is_extension_action);
}
}
// Tests that the parent_disabled_permissions disable reason is never set for
// regular users. Prevents a regression to crbug/1100395.
TEST_F(ExtensionInfoGeneratorUnitTest,
NoParentDisabledPermissionsForRegularUsers) {
// Preconditions.
ASSERT_FALSE(profile()->IsChild());
base::FilePath base_path = data_dir().AppendASCII("permissions_increase");
base::FilePath pem_path = base_path.AppendASCII("permissions.pem");
base::FilePath path = base_path.AppendASCII("v1");
const Extension* extension = PackAndInstallCRX(path, pem_path, INSTALL_NEW);
// The extension must now be installed and enabled.
ASSERT_TRUE(extension);
ASSERT_TRUE(registry()->enabled_extensions().Contains(extension->id()));
// Save the id, as |extension| will be destroyed during updating.
ExtensionId extension_id = extension->id();
// Update to a new version with increased permissions.
path = base_path.AppendASCII("v2");
PackCRXAndUpdateExtension(extension_id, path, pem_path, DISABLED);
// The extension should be disabled pending approval for permission increases.
EXPECT_TRUE(registry()->disabled_extensions().Contains(extension_id));
// Due to a permissions increase, prefs will contain escalation information.
ExtensionPrefs* prefs = ExtensionPrefs::Get(profile());
EXPECT_TRUE(prefs->DidExtensionEscalatePermissions(extension_id));
std::unique_ptr<api::developer_private::ExtensionInfo> info =
GenerateExtensionInfo(extension_id);
// Verify that the kite icon error tooltip doesn't appear for regular users.
EXPECT_FALSE(info->disable_reasons.parent_disabled_permissions);
}
// Test that the generator returns if the extension can be pinned to the toolbar
// and if it can, whether or not it's pinned.
TEST_F(ExtensionInfoGeneratorUnitTest, IsPinnedToToolbar) {
// By default, the extension is not pinned to the toolbar but can be.
const scoped_refptr<const Extension> extension = CreateExtension(
"test1", base::Value::List(), ManifestLocation::kInternal);
std::unique_ptr<developer::ExtensionInfo> info =
GenerateExtensionInfo(extension->id());
EXPECT_FALSE(*info->pinned_to_toolbar);
// Pin the extension to the toolbar and test that this is reflected in the
// generated info.
ToolbarActionsModel* toolbar_actions_model =
ToolbarActionsModel::Get(profile());
toolbar_actions_model->SetActionVisibility(extension->id(), true);
info = GenerateExtensionInfo(extension->id());
EXPECT_TRUE(*info->pinned_to_toolbar);
// Disable the extension. Since disabled extensions have no action, the
// `pinned_to_toolbar` field should not exist.
service()->DisableExtension(extension->id(),
disable_reason::DISABLE_USER_ACTION);
info = GenerateExtensionInfo(extension->id());
EXPECT_FALSE(info->pinned_to_toolbar.has_value());
}
#if BUILDFLAG(ENABLE_SUPERVISED_USERS)
// Whether parental controls apply to extensions.
enum class ExtensionsParentalControlState : int { kEnabled = 0, kDisabled = 1 };
// Whether the parental controls on Extensions are managed by the preference
// `SkipParentApprovalToInstallExtensions`, which corresponds to the
// "Allow to add extensions without asking permission" Family Link switch
// or by the preference `kSupervisedUserExtensionsMayRequestPermissions`
// which corresponds to the "Permissions for Sites, Apps and Extensions"
// Family Link switch.
enum class ExtensionManagementFamilyLinkSwitch : int {
kManagedByExtensionsSwitch = 0,
kManagedByPermissionsSwitch = 1
};
// Tests for supervised users (child accounts). Supervised users are not allowed
// to install apps or extensions unless their parent approves.
class ExtensionInfoGeneratorUnitTestSupervised
: public ExtensionInfoGeneratorUnitTest,
public testing::WithParamInterface<
std::tuple<ExtensionsParentalControlState,
ExtensionManagementFamilyLinkSwitch>> {
public:
ExtensionInfoGeneratorUnitTestSupervised() = default;
~ExtensionInfoGeneratorUnitTestSupervised() override = default;
// ExtensionInfoGeneratorUnitTest:
ExtensionServiceInitParams GetExtensionServiceInitParams() override {
ExtensionServiceInitParams params =
ExtensionInfoGeneratorUnitTest::GetExtensionServiceInitParams();
params.profile_is_supervised = true;
return params;
}
void SetUp() override {
ExtensionInfoGeneratorUnitTest::SetUp();
// Set up custodians (parents) for the child.
supervised_user_test_util::AddCustodians(profile());
// Set the pref to allow the child to request extension install.
supervised_user_test_util::
SetSupervisedUserExtensionsMayRequestPermissionsPref(profile(), true);
}
void TearDown() override {
ExtensionInfoGeneratorUnitTest::TearDown();
}
bool ApplyParentalControlsOnExtensions() {
return std::get<0>(GetParam()) == ExtensionsParentalControlState::kEnabled;
}
ExtensionManagementFamilyLinkSwitch GetExtensionManagementFamilyLinkSwitch() {
return std::get<1>(GetParam());
}
};
// Tests that when an extension:
// 1) is disabled pending permission updates and
// 2) the parent has turned off the "Permissions for sites, apps and extensions"
// toggle on Family Link and
// 3) the extension parental controls are managed by the Family link
// "Permissions for sites, apps and extensions" (legacy flow), instead of the
// "Allow to add extensions without asking permission" Family Link switch (new
// flow)" then supervised users will see a kite error icon with a tooltip.
TEST_P(ExtensionInfoGeneratorUnitTestSupervised,
ParentDisabledPermissionsForSupervisedUsers) {
// Extension permissions for supervised users is already enabled on ChromeOS.
base::test::ScopedFeatureList feature_list;
std::vector<base::test::FeatureRef> enabled_features;
std::vector<base::test::FeatureRef> disabled_features;
if (ApplyParentalControlsOnExtensions()) {
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
enabled_features.push_back(
supervised_user::
kEnableExtensionsPermissionsForSupervisedUsersOnDesktop);
#endif
if (GetExtensionManagementFamilyLinkSwitch() ==
ExtensionManagementFamilyLinkSwitch::kManagedByExtensionsSwitch) {
enabled_features.push_back(
supervised_user::
kEnableSupervisedUserSkipParentApprovalToInstallExtensions);
} else {
disabled_features.push_back(
supervised_user::
kEnableSupervisedUserSkipParentApprovalToInstallExtensions);
}
}
feature_list.InitWithFeatures(enabled_features, disabled_features);
ASSERT_TRUE(profile()->IsChild());
std::unique_ptr<SupervisedUserExtensionsDelegate>
supervised_user_extensions_delegate =
std::make_unique<SupervisedUserExtensionsDelegateImpl>(profile());
base::FilePath base_path = data_dir().AppendASCII("permissions_increase");
base::FilePath pem_path = base_path.AppendASCII("permissions.pem");
base::FilePath path = base_path.AppendASCII("v1");
// When extension parental controls apply, on the default behaviour
// the extensions will be installed but disabled until custodian approvals are
// performed. When extension parental controls do not apply, the extensions
// will be installed and enabled.
InstallState install_state =
ApplyParentalControlsOnExtensions() ? INSTALL_WITHOUT_LOAD : INSTALL_NEW;
const Extension* extension = PackAndInstallCRX(path, pem_path, install_state);
ASSERT_TRUE(extension);
if (ApplyParentalControlsOnExtensions()) {
EXPECT_TRUE(registry()->disabled_extensions().Contains(extension->id()));
} else {
EXPECT_TRUE(registry()->enabled_extensions().Contains(extension->id()));
}
// Save the id, as |extension| will be destroyed during updating.
ExtensionId extension_id = extension->id();
ExtensionPrefs* prefs = ExtensionPrefs::Get(profile());
if (ApplyParentalControlsOnExtensions()) {
EXPECT_TRUE(prefs->HasDisableReason(
extension_id, disable_reason::DISABLE_CUSTODIAN_APPROVAL_REQUIRED));
// Simulate parent approval for the extension installation.
supervised_user_extensions_delegate->AddExtensionApproval(*extension);
} else {
EXPECT_FALSE(prefs->IsExtensionDisabled(extension_id));
}
// The extension should be enabled.
EXPECT_TRUE(registry()->enabled_extensions().Contains(extension_id));
// Update to a new version with increased permissions.
path = base_path.AppendASCII("v2");
PackCRXAndUpdateExtension(extension_id, path, pem_path, DISABLED);
// The extension should be disabled.
EXPECT_TRUE(registry()->disabled_extensions().Contains(extension_id));
// Due to a permission increase, prefs will contain escalation information.
EXPECT_TRUE(prefs->DidExtensionEscalatePermissions(extension_id));
// Simulate the parent disallowing the child from approving permission
// updates. If extensions parental controls don't apply, or the extensions
// are managed by the `SkipParentApprovalToInstallExtensions` preference
// then this has no effect.
supervised_user_test_util::
SetSupervisedUserExtensionsMayRequestPermissionsPref(profile(), false);
// The extension should be disabled only if the extension parental controls
// are enabled and the extensions are governed by the
// `kSupervisedUserExtensionsMayRequestPermissions` preference.
std::unique_ptr<api::developer_private::ExtensionInfo> info =
GenerateExtensionInfo(extension_id);
bool is_extension_disabled =
ApplyParentalControlsOnExtensions() &&
GetExtensionManagementFamilyLinkSwitch() ==
ExtensionManagementFamilyLinkSwitch::kManagedByPermissionsSwitch;
EXPECT_EQ(info->disable_reasons.parent_disabled_permissions,
is_extension_disabled);
}
INSTANTIATE_TEST_SUITE_P(
ExtensionsForSupervisedUsers,
ExtensionInfoGeneratorUnitTestSupervised,
testing::Combine(
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
testing::Values(ExtensionsParentalControlState::kEnabled,
ExtensionsParentalControlState::kDisabled),
#else
// For ChromeOS the extension parental controls are on by default.
testing::Values(ExtensionsParentalControlState::kEnabled),
#endif
testing::Values(
ExtensionManagementFamilyLinkSwitch::kManagedByPermissionsSwitch,
ExtensionManagementFamilyLinkSwitch::kManagedByExtensionsSwitch)),
[](const auto& info) {
return std::string(std::get<0>(info.param) ==
ExtensionsParentalControlState::kEnabled
? "WithExtensionParentalControls"
: "WithoutExtensionParentalControls") +
std::string(std::get<1>(info.param) ==
ExtensionManagementFamilyLinkSwitch::
kManagedByExtensionsSwitch
? "ManagedByExtensionsFamilyLinkSwitch"
: "ManagedByPermissionsFamilyLinkSwitch");
});
#endif // BUILDFLAG(ENABLE_SUPERVISED_USERS)
} // namespace extensions