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() {
active_permissions.explicit_hosts().Contains(
unpack_result->optional_explicit_hosts) &&
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(
api::permissions::Contains::Results::Create(has_all_permissions)));
......@@ -143,7 +145,8 @@ ExtensionFunction::ResponseAction PermissionsRemoveFunction::Run() {
// withheld. I don't think that will be a common use case, and so is probably
// fine.
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));
}
......@@ -242,7 +245,8 @@ ExtensionFunction::ResponseAction PermissionsRequestFunction::Run() {
// Do the same for withheld permissions.
requested_withheld_ = std::make_unique<const PermissionSet>(
APIPermissionSet(), ManifestPermissionSet(),
unpack_result->required_explicit_hosts, URLPatternSet());
unpack_result->required_explicit_hosts,
unpack_result->required_scriptable_hosts);
requested_withheld_ =
PermissionSet::CreateDifference(*requested_withheld_, active_permissions);
......
......@@ -142,9 +142,12 @@ bool UnpackOriginPermissions(const std::vector<std::string>& origins_input,
bool allow_file_access,
UnpackPermissionSetResult* result,
std::string* error) {
int user_script_schemes = UserScript::ValidUserScriptSchemes();
int explicit_schemes = Extension::kValidHostPermissionSchemes;
if (!allow_file_access)
if (!allow_file_access) {
user_script_schemes &= ~URLPattern::SCHEME_FILE;
explicit_schemes &= ~URLPattern::SCHEME_FILE;
}
for (const auto& origin_str : origins_input) {
URLPattern explicit_origin(explicit_schemes);
......@@ -156,15 +159,28 @@ bool UnpackOriginPermissions(const std::vector<std::string>& origins_input,
return false;
}
bool used_origin = false;
if (required_permissions.explicit_hosts().ContainsPattern(
explicit_origin)) {
used_origin = true;
result->required_explicit_hosts.AddPattern(explicit_origin);
} else if (optional_permissions.explicit_hosts().ContainsPattern(
explicit_origin)) {
used_origin = true;
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;
......@@ -198,6 +214,8 @@ std::unique_ptr<Permissions> PackPermissionSet(const PermissionSet& set) {
for (const URLPattern& pattern : set.explicit_hosts())
permissions->origins->push_back(pattern.GetAsString());
// TODO(devlin): Add scriptable hosts.
return permissions;
}
......
......@@ -37,8 +37,8 @@ struct UnpackPermissionSetResult {
APIPermissionSet required_apis;
// Explicit hosts that are in the extension's "required" permission set.
URLPatternSet required_explicit_hosts;
// TODO(devlin): Add scriptable host support.
// https://crbug.com/889654.
// Scriptable hosts that are in the extension's "required" permission set.
URLPatternSet required_scriptable_hosts;
// API permissions that are in the extension's "optional" permission set.
APIPermissionSet optional_apis;
......
......@@ -16,6 +16,7 @@
#include "extensions/common/extension.h"
#include "extensions/common/permissions/permission_set.h"
#include "extensions/common/url_pattern_set.h"
#include "extensions/common/user_script.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
......@@ -188,25 +189,50 @@ TEST(ExtensionPermissionsAPIHelpers, Unpack_HostSeparation) {
auto explicit_url_pattern = [](const char* 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 kRequiredExplicit2[] = "https://required_explicit2.com/*";
constexpr char kOptionalExplicit1[] = "https://optional_explicit1.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/*";
URLPatternSet required_explicit_hosts({
explicit_url_pattern(kRequiredExplicit1),
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({
explicit_url_pattern(kOptionalExplicit1),
explicit_url_pattern(kOptionalExplicit2),
explicit_url_pattern(kOptionalExplicitAndRequiredScriptable1),
explicit_url_pattern(kOptionalExplicitAndRequiredScriptable2),
});
PermissionSet required_permissions(APIPermissionSet(),
ManifestPermissionSet(),
required_explicit_hosts, URLPatternSet());
PermissionSet required_permissions(
APIPermissionSet(), ManifestPermissionSet(), required_explicit_hosts,
required_scriptable_hosts);
PermissionSet optional_permissions(APIPermissionSet(),
ManifestPermissionSet(),
optional_explicit_hosts, URLPatternSet());
......@@ -214,7 +240,9 @@ TEST(ExtensionPermissionsAPIHelpers, Unpack_HostSeparation) {
Permissions permissions_object;
permissions_object.origins =
std::make_unique<std::vector<std::string>>(std::vector<std::string>(
{kRequiredExplicit1, kOptionalExplicit1, kUnlisted1}));
{kRequiredExplicit1, kOptionalExplicit1, kRequiredScriptable1,
kRequiredExplicitAndScriptable1,
kOptionalExplicitAndRequiredScriptable1, kUnlisted1}));
std::string error;
std::unique_ptr<UnpackPermissionSetResult> unpack_result =
......@@ -224,9 +252,15 @@ TEST(ExtensionPermissionsAPIHelpers, Unpack_HostSeparation) {
EXPECT_TRUE(error.empty()) << error;
EXPECT_THAT(GetPatternsAsStrings(unpack_result->required_explicit_hosts),
testing::UnorderedElementsAre(kRequiredExplicit1));
testing::UnorderedElementsAre(kRequiredExplicit1,
kRequiredExplicitAndScriptable1));
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),
testing::UnorderedElementsAre(kUnlisted1));
}
......
......@@ -100,6 +100,7 @@ class PermissionsAPIUnitTest : public ExtensionServiceTestWithInstall {
bool RunContainsFunction(const std::string& manifest_permission,
const std::string& args_string,
bool allow_file_access) {
SCOPED_TRACE(args_string);
ListBuilder required_permissions;
required_permissions.Append(manifest_permission);
scoped_refptr<const Extension> extension = CreateExtensionWithPermissions(
......@@ -186,9 +187,12 @@ TEST_F(PermissionsAPIUnitTest, ContainsAndGetAllWithRuntimeHostPermissions) {
scoped_feature_list.InitAndEnableFeature(
extensions_features::kRuntimeHostPermissions);
constexpr char kExampleCom[] = "https://example.com/*";
constexpr char kContentScriptCom[] = "https://contentscript.com/*";
scoped_refptr<const Extension> extension =
ExtensionBuilder("extension")
.AddPermissions({"https://example.com/"})
.AddPermission(kExampleCom)
.AddContentScript("foo.js", {kContentScriptCom, kExampleCom})
.Build();
AddExtensionAndGrantPermissions(*extension);
......@@ -198,6 +202,7 @@ TEST_F(PermissionsAPIUnitTest, ContainsAndGetAllWithRuntimeHostPermissions) {
service()->AddExtension(extension.get());
auto contains_origin = [this, &extension](const char* origin) {
SCOPED_TRACE(origin);
auto function = base::MakeRefCounted<PermissionsContainsFunction>();
function->set_extension(extension.get());
if (!extension_function_test_utils::RunFunction(
......@@ -235,10 +240,10 @@ TEST_F(PermissionsAPIUnitTest, ContainsAndGetAllWithRuntimeHostPermissions) {
return origins;
};
// Currently, the extension should have access to example.com (since
// permissions are not withheld).
constexpr char kExampleCom[] = "https://example.com/*";
// Currently, the extension should have access to example.com and
// contentscript.com (since permissions are not withheld).
EXPECT_TRUE(contains_origin(kExampleCom));
EXPECT_TRUE(contains_origin(kContentScriptCom));
EXPECT_THAT(get_all(), testing::ElementsAre(kExampleCom));
ScriptingPermissionsModifier modifier(profile(), extension);
......@@ -247,6 +252,7 @@ TEST_F(PermissionsAPIUnitTest, ContainsAndGetAllWithRuntimeHostPermissions) {
// Once we withhold the permission, the contains function should correctly
// report the value.
EXPECT_FALSE(contains_origin(kExampleCom));
EXPECT_FALSE(contains_origin(kContentScriptCom));
EXPECT_THAT(get_all(), testing::IsEmpty());
constexpr char kChromiumOrg[] = "https://chromium.org/";
......@@ -258,6 +264,30 @@ TEST_F(PermissionsAPIUnitTest, ContainsAndGetAllWithRuntimeHostPermissions) {
// able to use them anyway (since they aren't active).
EXPECT_FALSE(contains_origin(kChromiumOrg));
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.
......@@ -300,6 +330,91 @@ TEST_F(PermissionsAPIUnitTest, RequestingWithheldPermissions) {
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.
TEST_F(PermissionsAPIUnitTest, ReRequestingWithheldOptionalPermissions) {
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