Commit 71f24f63 authored by Devlin Cronin's avatar Devlin Cronin Committed by Commit Bot

[Extensions Click-to-Script] Support content scripts in the permissions API

Add support for extensions requesting content script permissions in the
permissions API. This allows extensions to request withheld content
script permissions through chrome.permissions.request().

Add tests to cover the same.

Bug: 889654
Change-Id: I0b77215ec154e42d1152df3277307d0383cda204
Reviewed-on: https://chromium-review.googlesource.com/c/1347061Reviewed-by: default avatarKaran Bhatia <karandeepb@chromium.org>
Commit-Queue: Devlin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#611945}
parent 0990c9da
...@@ -90,7 +90,9 @@ ExtensionFunction::ResponseAction PermissionsContainsFunction::Run() { ...@@ -90,7 +90,9 @@ ExtensionFunction::ResponseAction PermissionsContainsFunction::Run() {
active_permissions.explicit_hosts().Contains( active_permissions.explicit_hosts().Contains(
unpack_result->optional_explicit_hosts) && unpack_result->optional_explicit_hosts) &&
active_permissions.explicit_hosts().Contains( active_permissions.explicit_hosts().Contains(
unpack_result->required_explicit_hosts); unpack_result->required_explicit_hosts) &&
active_permissions.scriptable_hosts().Contains(
unpack_result->required_scriptable_hosts);
return RespondNow(ArgumentList( return RespondNow(ArgumentList(
api::permissions::Contains::Results::Create(has_all_permissions))); api::permissions::Contains::Results::Create(has_all_permissions)));
...@@ -143,7 +145,8 @@ ExtensionFunction::ResponseAction PermissionsRemoveFunction::Run() { ...@@ -143,7 +145,8 @@ ExtensionFunction::ResponseAction PermissionsRemoveFunction::Run() {
// withheld. I don't think that will be a common use case, and so is probably // withheld. I don't think that will be a common use case, and so is probably
// fine. // fine.
if (!unpack_result->required_apis.empty() || if (!unpack_result->required_apis.empty() ||
!unpack_result->required_explicit_hosts.is_empty()) { !unpack_result->required_explicit_hosts.is_empty() ||
!unpack_result->required_scriptable_hosts.is_empty()) {
return RespondNow(Error(kCantRemoveRequiredPermissionsError)); return RespondNow(Error(kCantRemoveRequiredPermissionsError));
} }
...@@ -242,7 +245,8 @@ ExtensionFunction::ResponseAction PermissionsRequestFunction::Run() { ...@@ -242,7 +245,8 @@ ExtensionFunction::ResponseAction PermissionsRequestFunction::Run() {
// Do the same for withheld permissions. // Do the same for withheld permissions.
requested_withheld_ = std::make_unique<const PermissionSet>( requested_withheld_ = std::make_unique<const PermissionSet>(
APIPermissionSet(), ManifestPermissionSet(), APIPermissionSet(), ManifestPermissionSet(),
unpack_result->required_explicit_hosts, URLPatternSet()); unpack_result->required_explicit_hosts,
unpack_result->required_scriptable_hosts);
requested_withheld_ = requested_withheld_ =
PermissionSet::CreateDifference(*requested_withheld_, active_permissions); PermissionSet::CreateDifference(*requested_withheld_, active_permissions);
......
...@@ -142,9 +142,12 @@ bool UnpackOriginPermissions(const std::vector<std::string>& origins_input, ...@@ -142,9 +142,12 @@ bool UnpackOriginPermissions(const std::vector<std::string>& origins_input,
bool allow_file_access, bool allow_file_access,
UnpackPermissionSetResult* result, UnpackPermissionSetResult* result,
std::string* error) { std::string* error) {
int user_script_schemes = UserScript::ValidUserScriptSchemes();
int explicit_schemes = Extension::kValidHostPermissionSchemes; int explicit_schemes = Extension::kValidHostPermissionSchemes;
if (!allow_file_access) if (!allow_file_access) {
user_script_schemes &= ~URLPattern::SCHEME_FILE;
explicit_schemes &= ~URLPattern::SCHEME_FILE; explicit_schemes &= ~URLPattern::SCHEME_FILE;
}
for (const auto& origin_str : origins_input) { for (const auto& origin_str : origins_input) {
URLPattern explicit_origin(explicit_schemes); URLPattern explicit_origin(explicit_schemes);
...@@ -156,15 +159,28 @@ bool UnpackOriginPermissions(const std::vector<std::string>& origins_input, ...@@ -156,15 +159,28 @@ bool UnpackOriginPermissions(const std::vector<std::string>& origins_input,
return false; return false;
} }
bool used_origin = false;
if (required_permissions.explicit_hosts().ContainsPattern( if (required_permissions.explicit_hosts().ContainsPattern(
explicit_origin)) { explicit_origin)) {
used_origin = true;
result->required_explicit_hosts.AddPattern(explicit_origin); result->required_explicit_hosts.AddPattern(explicit_origin);
} else if (optional_permissions.explicit_hosts().ContainsPattern( } else if (optional_permissions.explicit_hosts().ContainsPattern(
explicit_origin)) { explicit_origin)) {
used_origin = true;
result->optional_explicit_hosts.AddPattern(explicit_origin); result->optional_explicit_hosts.AddPattern(explicit_origin);
} else {
result->unlisted_hosts.AddPattern(explicit_origin);
} }
URLPattern scriptable_origin(user_script_schemes);
if (scriptable_origin.Parse(origin_str) ==
URLPattern::ParseResult::kSuccess &&
required_permissions.scriptable_hosts().ContainsPattern(
scriptable_origin)) {
used_origin = true;
result->required_scriptable_hosts.AddPattern(scriptable_origin);
}
if (!used_origin)
result->unlisted_hosts.AddPattern(explicit_origin);
} }
return true; return true;
...@@ -198,6 +214,8 @@ std::unique_ptr<Permissions> PackPermissionSet(const PermissionSet& set) { ...@@ -198,6 +214,8 @@ std::unique_ptr<Permissions> PackPermissionSet(const PermissionSet& set) {
for (const URLPattern& pattern : set.explicit_hosts()) for (const URLPattern& pattern : set.explicit_hosts())
permissions->origins->push_back(pattern.GetAsString()); permissions->origins->push_back(pattern.GetAsString());
// TODO(devlin): Add scriptable hosts.
return permissions; return permissions;
} }
......
...@@ -37,8 +37,8 @@ struct UnpackPermissionSetResult { ...@@ -37,8 +37,8 @@ struct UnpackPermissionSetResult {
APIPermissionSet required_apis; APIPermissionSet required_apis;
// Explicit hosts that are in the extension's "required" permission set. // Explicit hosts that are in the extension's "required" permission set.
URLPatternSet required_explicit_hosts; URLPatternSet required_explicit_hosts;
// TODO(devlin): Add scriptable host support. // Scriptable hosts that are in the extension's "required" permission set.
// https://crbug.com/889654. URLPatternSet required_scriptable_hosts;
// API permissions that are in the extension's "optional" permission set. // API permissions that are in the extension's "optional" permission set.
APIPermissionSet optional_apis; APIPermissionSet optional_apis;
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
#include "extensions/common/extension.h" #include "extensions/common/extension.h"
#include "extensions/common/permissions/permission_set.h" #include "extensions/common/permissions/permission_set.h"
#include "extensions/common/url_pattern_set.h" #include "extensions/common/url_pattern_set.h"
#include "extensions/common/user_script.h"
#include "testing/gmock/include/gmock/gmock.h" #include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h" #include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h" #include "url/gurl.h"
...@@ -188,25 +189,50 @@ TEST(ExtensionPermissionsAPIHelpers, Unpack_HostSeparation) { ...@@ -188,25 +189,50 @@ TEST(ExtensionPermissionsAPIHelpers, Unpack_HostSeparation) {
auto explicit_url_pattern = [](const char* pattern) { auto explicit_url_pattern = [](const char* pattern) {
return URLPattern(Extension::kValidHostPermissionSchemes, pattern); return URLPattern(Extension::kValidHostPermissionSchemes, pattern);
}; };
auto scriptable_url_pattern = [](const char* pattern) {
return URLPattern(UserScript::ValidUserScriptSchemes(), pattern);
};
constexpr char kRequiredExplicit1[] = "https://required_explicit1.com/*"; constexpr char kRequiredExplicit1[] = "https://required_explicit1.com/*";
constexpr char kRequiredExplicit2[] = "https://required_explicit2.com/*"; constexpr char kRequiredExplicit2[] = "https://required_explicit2.com/*";
constexpr char kOptionalExplicit1[] = "https://optional_explicit1.com/*"; constexpr char kOptionalExplicit1[] = "https://optional_explicit1.com/*";
constexpr char kOptionalExplicit2[] = "https://optional_explicit2.com/*"; constexpr char kOptionalExplicit2[] = "https://optional_explicit2.com/*";
constexpr char kRequiredScriptable1[] = "https://required_scriptable1.com/*";
constexpr char kRequiredScriptable2[] = "https://required_scriptable2.com/*";
constexpr char kRequiredExplicitAndScriptable1[] =
"https://required_explicit_and_scriptable1.com/*";
constexpr char kRequiredExplicitAndScriptable2[] =
"https://required_explicit_and_scriptable2.com/*";
constexpr char kOptionalExplicitAndRequiredScriptable1[] =
"https://optional_explicit_and_scriptable1.com/*";
constexpr char kOptionalExplicitAndRequiredScriptable2[] =
"https://optional_explicit_and_scriptable2.com/*";
constexpr char kUnlisted1[] = "https://unlisted1.com/*"; constexpr char kUnlisted1[] = "https://unlisted1.com/*";
URLPatternSet required_explicit_hosts({ URLPatternSet required_explicit_hosts({
explicit_url_pattern(kRequiredExplicit1), explicit_url_pattern(kRequiredExplicit1),
explicit_url_pattern(kRequiredExplicit2), explicit_url_pattern(kRequiredExplicit2),
explicit_url_pattern(kRequiredExplicitAndScriptable1),
explicit_url_pattern(kRequiredExplicitAndScriptable2),
});
URLPatternSet required_scriptable_hosts({
scriptable_url_pattern(kRequiredScriptable1),
scriptable_url_pattern(kRequiredScriptable2),
scriptable_url_pattern(kRequiredExplicitAndScriptable1),
scriptable_url_pattern(kRequiredExplicitAndScriptable2),
scriptable_url_pattern(kOptionalExplicitAndRequiredScriptable1),
scriptable_url_pattern(kOptionalExplicitAndRequiredScriptable2),
}); });
URLPatternSet optional_explicit_hosts({ URLPatternSet optional_explicit_hosts({
explicit_url_pattern(kOptionalExplicit1), explicit_url_pattern(kOptionalExplicit1),
explicit_url_pattern(kOptionalExplicit2), explicit_url_pattern(kOptionalExplicit2),
explicit_url_pattern(kOptionalExplicitAndRequiredScriptable1),
explicit_url_pattern(kOptionalExplicitAndRequiredScriptable2),
}); });
PermissionSet required_permissions(APIPermissionSet(), PermissionSet required_permissions(
ManifestPermissionSet(), APIPermissionSet(), ManifestPermissionSet(), required_explicit_hosts,
required_explicit_hosts, URLPatternSet()); required_scriptable_hosts);
PermissionSet optional_permissions(APIPermissionSet(), PermissionSet optional_permissions(APIPermissionSet(),
ManifestPermissionSet(), ManifestPermissionSet(),
optional_explicit_hosts, URLPatternSet()); optional_explicit_hosts, URLPatternSet());
...@@ -214,7 +240,9 @@ TEST(ExtensionPermissionsAPIHelpers, Unpack_HostSeparation) { ...@@ -214,7 +240,9 @@ TEST(ExtensionPermissionsAPIHelpers, Unpack_HostSeparation) {
Permissions permissions_object; Permissions permissions_object;
permissions_object.origins = permissions_object.origins =
std::make_unique<std::vector<std::string>>(std::vector<std::string>( std::make_unique<std::vector<std::string>>(std::vector<std::string>(
{kRequiredExplicit1, kOptionalExplicit1, kUnlisted1})); {kRequiredExplicit1, kOptionalExplicit1, kRequiredScriptable1,
kRequiredExplicitAndScriptable1,
kOptionalExplicitAndRequiredScriptable1, kUnlisted1}));
std::string error; std::string error;
std::unique_ptr<UnpackPermissionSetResult> unpack_result = std::unique_ptr<UnpackPermissionSetResult> unpack_result =
...@@ -224,9 +252,15 @@ TEST(ExtensionPermissionsAPIHelpers, Unpack_HostSeparation) { ...@@ -224,9 +252,15 @@ TEST(ExtensionPermissionsAPIHelpers, Unpack_HostSeparation) {
EXPECT_TRUE(error.empty()) << error; EXPECT_TRUE(error.empty()) << error;
EXPECT_THAT(GetPatternsAsStrings(unpack_result->required_explicit_hosts), EXPECT_THAT(GetPatternsAsStrings(unpack_result->required_explicit_hosts),
testing::UnorderedElementsAre(kRequiredExplicit1)); testing::UnorderedElementsAre(kRequiredExplicit1,
kRequiredExplicitAndScriptable1));
EXPECT_THAT(GetPatternsAsStrings(unpack_result->optional_explicit_hosts), EXPECT_THAT(GetPatternsAsStrings(unpack_result->optional_explicit_hosts),
testing::UnorderedElementsAre(kOptionalExplicit1)); testing::UnorderedElementsAre(
kOptionalExplicit1, kOptionalExplicitAndRequiredScriptable1));
EXPECT_THAT(GetPatternsAsStrings(unpack_result->required_scriptable_hosts),
testing::UnorderedElementsAre(
kRequiredScriptable1, kRequiredExplicitAndScriptable1,
kOptionalExplicitAndRequiredScriptable1));
EXPECT_THAT(GetPatternsAsStrings(unpack_result->unlisted_hosts), EXPECT_THAT(GetPatternsAsStrings(unpack_result->unlisted_hosts),
testing::UnorderedElementsAre(kUnlisted1)); testing::UnorderedElementsAre(kUnlisted1));
} }
......
...@@ -100,6 +100,7 @@ class PermissionsAPIUnitTest : public ExtensionServiceTestWithInstall { ...@@ -100,6 +100,7 @@ class PermissionsAPIUnitTest : public ExtensionServiceTestWithInstall {
bool RunContainsFunction(const std::string& manifest_permission, bool RunContainsFunction(const std::string& manifest_permission,
const std::string& args_string, const std::string& args_string,
bool allow_file_access) { bool allow_file_access) {
SCOPED_TRACE(args_string);
ListBuilder required_permissions; ListBuilder required_permissions;
required_permissions.Append(manifest_permission); required_permissions.Append(manifest_permission);
scoped_refptr<const Extension> extension = CreateExtensionWithPermissions( scoped_refptr<const Extension> extension = CreateExtensionWithPermissions(
...@@ -186,9 +187,12 @@ TEST_F(PermissionsAPIUnitTest, ContainsAndGetAllWithRuntimeHostPermissions) { ...@@ -186,9 +187,12 @@ TEST_F(PermissionsAPIUnitTest, ContainsAndGetAllWithRuntimeHostPermissions) {
scoped_feature_list.InitAndEnableFeature( scoped_feature_list.InitAndEnableFeature(
extensions_features::kRuntimeHostPermissions); extensions_features::kRuntimeHostPermissions);
constexpr char kExampleCom[] = "https://example.com/*";
constexpr char kContentScriptCom[] = "https://contentscript.com/*";
scoped_refptr<const Extension> extension = scoped_refptr<const Extension> extension =
ExtensionBuilder("extension") ExtensionBuilder("extension")
.AddPermissions({"https://example.com/"}) .AddPermission(kExampleCom)
.AddContentScript("foo.js", {kContentScriptCom, kExampleCom})
.Build(); .Build();
AddExtensionAndGrantPermissions(*extension); AddExtensionAndGrantPermissions(*extension);
...@@ -198,6 +202,7 @@ TEST_F(PermissionsAPIUnitTest, ContainsAndGetAllWithRuntimeHostPermissions) { ...@@ -198,6 +202,7 @@ TEST_F(PermissionsAPIUnitTest, ContainsAndGetAllWithRuntimeHostPermissions) {
service()->AddExtension(extension.get()); service()->AddExtension(extension.get());
auto contains_origin = [this, &extension](const char* origin) { auto contains_origin = [this, &extension](const char* origin) {
SCOPED_TRACE(origin);
auto function = base::MakeRefCounted<PermissionsContainsFunction>(); auto function = base::MakeRefCounted<PermissionsContainsFunction>();
function->set_extension(extension.get()); function->set_extension(extension.get());
if (!extension_function_test_utils::RunFunction( if (!extension_function_test_utils::RunFunction(
...@@ -235,10 +240,10 @@ TEST_F(PermissionsAPIUnitTest, ContainsAndGetAllWithRuntimeHostPermissions) { ...@@ -235,10 +240,10 @@ TEST_F(PermissionsAPIUnitTest, ContainsAndGetAllWithRuntimeHostPermissions) {
return origins; return origins;
}; };
// Currently, the extension should have access to example.com (since // Currently, the extension should have access to example.com and
// permissions are not withheld). // contentscript.com (since permissions are not withheld).
constexpr char kExampleCom[] = "https://example.com/*";
EXPECT_TRUE(contains_origin(kExampleCom)); EXPECT_TRUE(contains_origin(kExampleCom));
EXPECT_TRUE(contains_origin(kContentScriptCom));
EXPECT_THAT(get_all(), testing::ElementsAre(kExampleCom)); EXPECT_THAT(get_all(), testing::ElementsAre(kExampleCom));
ScriptingPermissionsModifier modifier(profile(), extension); ScriptingPermissionsModifier modifier(profile(), extension);
...@@ -247,6 +252,7 @@ TEST_F(PermissionsAPIUnitTest, ContainsAndGetAllWithRuntimeHostPermissions) { ...@@ -247,6 +252,7 @@ TEST_F(PermissionsAPIUnitTest, ContainsAndGetAllWithRuntimeHostPermissions) {
// Once we withhold the permission, the contains function should correctly // Once we withhold the permission, the contains function should correctly
// report the value. // report the value.
EXPECT_FALSE(contains_origin(kExampleCom)); EXPECT_FALSE(contains_origin(kExampleCom));
EXPECT_FALSE(contains_origin(kContentScriptCom));
EXPECT_THAT(get_all(), testing::IsEmpty()); EXPECT_THAT(get_all(), testing::IsEmpty());
constexpr char kChromiumOrg[] = "https://chromium.org/"; constexpr char kChromiumOrg[] = "https://chromium.org/";
...@@ -258,6 +264,30 @@ TEST_F(PermissionsAPIUnitTest, ContainsAndGetAllWithRuntimeHostPermissions) { ...@@ -258,6 +264,30 @@ TEST_F(PermissionsAPIUnitTest, ContainsAndGetAllWithRuntimeHostPermissions) {
// able to use them anyway (since they aren't active). // able to use them anyway (since they aren't active).
EXPECT_FALSE(contains_origin(kChromiumOrg)); EXPECT_FALSE(contains_origin(kChromiumOrg));
EXPECT_THAT(get_all(), testing::IsEmpty()); EXPECT_THAT(get_all(), testing::IsEmpty());
// Fun edge case: example.com is requested as both a scriptable and an
// explicit host. It is technically possible that it may be granted *only* as
// one of the two (e.g., only explicit granted).
{
URLPatternSet explicit_hosts(
{URLPattern(Extension::kValidHostPermissionSchemes, kExampleCom)});
permissions_test_util::GrantRuntimePermissionsAndWaitForCompletion(
profile(), *extension,
PermissionSet(APIPermissionSet(), ManifestPermissionSet(),
explicit_hosts, URLPatternSet()));
const GURL example_url("https://example.com");
const PermissionSet& active_permissions =
extension->permissions_data()->active_permissions();
EXPECT_TRUE(active_permissions.explicit_hosts().MatchesURL(example_url));
EXPECT_FALSE(active_permissions.scriptable_hosts().MatchesURL(example_url));
}
// In this case, contains() should return *false* (because not all the
// permissions are active, but getAll() should include example.com (because it
// has been [partially] granted). In practice, this case should be
// exceptionally rare, and we're mostly just making sure that there's some
// sane behavior.
EXPECT_FALSE(contains_origin(kExampleCom));
EXPECT_THAT(get_all(), testing::ElementsAre(kExampleCom));
} }
// Tests requesting withheld permissions with the permissions.request() API. // Tests requesting withheld permissions with the permissions.request() API.
...@@ -300,6 +330,91 @@ TEST_F(PermissionsAPIUnitTest, RequestingWithheldPermissions) { ...@@ -300,6 +330,91 @@ TEST_F(PermissionsAPIUnitTest, RequestingWithheldPermissions) {
kGoogleCom)); kGoogleCom));
} }
// Tests requesting withheld content script permissions with the
// permissions.request() API.
TEST_F(PermissionsAPIUnitTest, RequestingWithheldContentScriptPermissions) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
extensions_features::kRuntimeHostPermissions);
constexpr char kContentScriptPattern[] = "https://contentscript.com/*";
// Create an extension with required host permissions, and withhold those
// permissions.
scoped_refptr<const Extension> extension =
ExtensionBuilder("extension")
.AddContentScript("foo.js", {kContentScriptPattern})
.Build();
AddExtensionAndGrantPermissions(*extension);
ScriptingPermissionsModifier(profile(), extension)
.SetWithholdHostPermissions(true);
const GURL kContentScriptCom("https://contentscript.com");
const PermissionsData* permissions_data = extension->permissions_data();
EXPECT_TRUE(
permissions_data->active_permissions().effective_hosts().is_empty());
// Request one of the withheld permissions.
std::unique_ptr<const PermissionSet> prompted_permissions;
EXPECT_TRUE(
RunRequestFunction(*extension, browser(),
R"([{"origins": ["https://contentscript.com/*"]}])",
&prompted_permissions));
ASSERT_TRUE(prompted_permissions);
EXPECT_THAT(GetPatternsAsStrings(prompted_permissions->effective_hosts()),
testing::UnorderedElementsAre(kContentScriptPattern));
// The withheld permission should be granted.
EXPECT_THAT(GetPatternsAsStrings(
permissions_data->active_permissions().effective_hosts()),
testing::UnorderedElementsAre(kContentScriptPattern));
EXPECT_TRUE(
permissions_data->withheld_permissions().effective_hosts().is_empty());
}
// Tests requesting a withheld host permission that is both an explicit and a
// scriptable host with the permissions.request() API.
TEST_F(PermissionsAPIUnitTest,
RequestingWithheldExplicitAndScriptablePermissionsInTheSameCall) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
extensions_features::kRuntimeHostPermissions);
constexpr char kContentScriptPattern[] = "https://example.com/*";
// Create an extension with required host permissions, and withhold those
// permissions.
scoped_refptr<const Extension> extension =
ExtensionBuilder("extension")
.AddPermission("https://example.com/*")
.AddContentScript("foo.js", {kContentScriptPattern})
.Build();
AddExtensionAndGrantPermissions(*extension);
ScriptingPermissionsModifier(profile(), extension)
.SetWithholdHostPermissions(true);
const GURL kExampleCom("https://example.com");
const PermissionsData* permissions_data = extension->permissions_data();
EXPECT_TRUE(
permissions_data->active_permissions().effective_hosts().is_empty());
// Request one of the withheld permissions.
std::unique_ptr<const PermissionSet> prompted_permissions;
EXPECT_TRUE(RunRequestFunction(*extension, browser(),
R"([{"origins": ["https://example.com/*"]}])",
&prompted_permissions));
ASSERT_TRUE(prompted_permissions);
EXPECT_THAT(GetPatternsAsStrings(prompted_permissions->effective_hosts()),
testing::UnorderedElementsAre(kContentScriptPattern));
// The withheld permission should be granted to both explicit and scriptable
// hosts.
EXPECT_TRUE(
permissions_data->active_permissions().explicit_hosts().MatchesURL(
kExampleCom));
EXPECT_TRUE(
permissions_data->active_permissions().scriptable_hosts().MatchesURL(
kExampleCom));
}
// Tests an extension re-requesting an optional host after the user removes it. // Tests an extension re-requesting an optional host after the user removes it.
TEST_F(PermissionsAPIUnitTest, ReRequestingWithheldOptionalPermissions) { TEST_F(PermissionsAPIUnitTest, ReRequestingWithheldOptionalPermissions) {
base::test::ScopedFeatureList feature_list; base::test::ScopedFeatureList feature_list;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment