Bug 1780747 - Register DNR schema and permissions r=rpl,geckoview-reviewers,owlish,flod

This patch adds the minimum necessary to register the
declarativeNetRequest API and its permissions, behind prefs.

Tests have been added/updated to verify that the permissions and API
access are enforced correctly (effectiveness of preferences, API
visibility, permission warnings).

Before landing this, we need to register the permission warning in
Android-Components too, as mentioned in the bug (i.e. bug 1671453).

Differential Revision: https://phabricator.services.mozilla.com/D152503
This commit is contained in:
Rob Wu 2022-08-09 12:16:34 +00:00
Родитель 4b8c434c26
Коммит 7e7534ced4
13 изменённых файлов: 470 добавлений и 0 удалений

Просмотреть файл

@ -127,6 +127,7 @@ webextPerms.description.browserSettings=Read and modify browser settings
webextPerms.description.browsingData=Clear recent browsing history, cookies, and related data
webextPerms.description.clipboardRead=Get data from the clipboard
webextPerms.description.clipboardWrite=Input data to the clipboard
webextPerms.description.declarativeNetRequest=Block content on any page
webextPerms.description.devtools=Extend developer tools to access your data in open tabs
webextPerms.description.downloads=Download files and read and modify the browsers download history
webextPerms.description.downloads.open=Open files downloaded to your computer

Просмотреть файл

@ -49,6 +49,7 @@ webextPerms.description.browserSettings=Read and modify browser settings
webextPerms.description.browsingData=Clear recent browsing history, cookies, and related data
webextPerms.description.clipboardRead=Get data from the clipboard
webextPerms.description.clipboardWrite=Input data to the clipboard
webextPerms.description.declarativeNetRequest=Block content on any page
webextPerms.description.devtools=Extend developer tools to access your data in open tabs
webextPerms.description.downloads=Download files and read and modify the browsers download history
webextPerms.description.downloads.open=Open files downloaded to your computer

Просмотреть файл

@ -210,6 +210,9 @@ if (
}
}
const PREF_DNR_ENABLED = "extensions.dnr.enabled";
const PREF_DNR_FEEDBACK = "extensions.dnr.feedback";
// Message included in warnings and errors related to privileged permissions and
// privileged manifest properties. Provides a link to the firefox-source-docs.mozilla.org
// section related to developing and sign Privileged Add-ons.
@ -263,6 +266,23 @@ function isMozillaExtension(extension) {
return isSigned && isMozillaLineExtension;
}
function isDNRPermissionAllowed(perm) {
// DNR is under development and therefore disabled by default for now.
if (!Services.prefs.getBoolPref(PREF_DNR_ENABLED, false)) {
return false;
}
// APIs tied to declarativeNetRequestFeedback are for debugging purposes and
// are only supposed to be available when the (add-on dev) user opts in.
if (
perm === "declarativeNetRequestFeedback" &&
!Services.prefs.getBoolPref(PREF_DNR_FEEDBACK, false)
) {
return false;
}
return true;
}
/**
* Classify an individual permission from a webextension manifest
* as a host/origin permission, an api permission, or a regular permission.
@ -295,6 +315,11 @@ function classifyPermission(perm, restrictSchemes, isPrivileged) {
return { api: match[2] };
} else if (!isPrivileged && PRIVILEGED_PERMS.has(match[1])) {
return { invalid: perm, privileged: true };
} else if (
perm.startsWith("declarativeNetRequest") &&
!isDNRPermissionAllowed(perm)
) {
return { invalid: perm };
}
return { permission: perm };
}

Просмотреть файл

@ -60,6 +60,14 @@
["cookies"]
]
},
"declarativeNetRequest": {
"url": "chrome://extensions/content/parent/ext-declarativeNetRequest.js",
"schema": "chrome://extensions/content/schemas/declarative_net_request.json",
"scopes": ["addon_parent"],
"paths": [
["declarativeNetRequest"]
]
},
"dns": {
"url": "chrome://extensions/content/parent/ext-dns.js",
"schema": "chrome://extensions/content/schemas/dns.json",

Просмотреть файл

@ -19,6 +19,7 @@ toolkit.jar:
content/extensions/parent/ext-contextualIdentities.js (parent/ext-contextualIdentities.js)
content/extensions/parent/ext-clipboard.js (parent/ext-clipboard.js)
content/extensions/parent/ext-cookies.js (parent/ext-cookies.js)
content/extensions/parent/ext-declarativeNetRequest.js (parent/ext-declarativeNetRequest.js)
content/extensions/parent/ext-dns.js (parent/ext-dns.js)
content/extensions/parent/ext-downloads.js (parent/ext-downloads.js)
content/extensions/parent/ext-extension.js (parent/ext-extension.js)

Просмотреть файл

@ -0,0 +1,22 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.declarativeNetRequest = class extends ExtensionAPI {
getAPI(context) {
return {
declarativeNetRequest: {
async testMatchOutcome(request) {
// TODO bug 1745758: Implement rule evaluation engine.
// Since rule registration has not been implemented yet, the result
// is always an empty list.
return [];
},
},
};
}
};

Просмотреть файл

@ -0,0 +1,136 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
[
{
"namespace": "manifest",
"types": [
{
"$extend": "Permission",
"choices": [{
"type": "string",
"enum": ["declarativeNetRequest"]
}]
},
{
"$extend": "PermissionNoPrompt",
"choices": [{
"type": "string",
"enum": ["declarativeNetRequestFeedback", "declarativeNetRequestWithHostAccess"]
}]
}
]
},
{
"namespace": "declarativeNetRequest",
"description": "Use the declarativeNetRequest API to block or modify network requests by specifying declarative rules.",
"permissions": ["declarativeNetRequest", "declarativeNetRequestWithHostAccess"],
"types": [
{
"id": "ResourceType",
"type": "string",
"description": "How the requested resource will be used. Comparable to the webRequest.ResourceType type.",
"enum": [
"main_frame",
"sub_frame",
"stylesheet",
"script",
"image",
"object",
"object_subrequest",
"xmlhttprequest",
"xslt",
"ping",
"beacon",
"xml_dtd",
"font",
"media",
"websocket",
"csp_report",
"imageset",
"web_manifest",
"speculative",
"other"
]
},
{
"id": "MatchedRule",
"type": "object",
"properties": {
"ruleId": {
"type": "integer",
"description": "A matching rule's ID."
},
"rulesetId": {
"type": "string",
"description": "ID of the Ruleset this rule belongs to."
}
}
}
],
"functions": [
{
"name": "testMatchOutcome",
"type": "function",
"description": "Checks if any of the extension's declarativeNetRequest rules would match a hypothetical request.",
"permissions": ["declarativeNetRequestFeedback"],
"async": "callback",
"parameters": [
{
"name": "request",
"type": "object",
"description": "The details of the request to test.",
"properties": {
"url": {
"type": "string",
"description": "The URL of the hypothetical request."
},
"initiator": {
"type": "string",
"description": "The initiator URL (if any) for the hypothetical request.",
"optional": true
},
"method": {
"type": "string",
"description": "Standard HTTP method of the hypothetical request.",
"optional": true,
"default": "get"
},
"type": {
"$ref": "ResourceType",
"description": "The resource type of the hypothetical request."
},
"tabId": {
"type": "integer",
"description": "The ID of the tab in which the hypothetical request takes place. Does not need to correspond to a real tab ID. Default is -1, meaning that the request isn't related to a tab.",
"optional": true,
"default": -1
}
}
},
{
"name": "callback",
"type": "function",
"description": "Called with the details of matched rules.",
"parameters": [
{
"name": "result",
"type": "object",
"properties": {
"matchedRules": {
"type": "array",
"description": "The rules (if any) that match the hypothetical request.",
"items": {
"$ref": "MatchedRule"
}
}
}
}
]
}
]
}
]
}
]

Просмотреть файл

@ -16,6 +16,7 @@ toolkit.jar:
content/extensions/schemas/content_scripts.json
content/extensions/schemas/contextual_identities.json
content/extensions/schemas/cookies.json
content/extensions/schemas/declarative_net_request.json
content/extensions/schemas/dns.json
content/extensions/schemas/downloads.json
content/extensions/schemas/events.json

Просмотреть файл

@ -0,0 +1,212 @@
"use strict";
AddonTestUtils.init(this);
const PREF_DNR_FEEDBACK_DEFAULT_VALUE = Services.prefs.getBoolPref(
"extensions.dnr.feedback",
false
);
async function testAvailability({
allowDNRFeedback = false,
testExpectations,
...extensionData
}) {
function background(testExpectations) {
let {
declarativeNetRequest_available = false,
testMatchOutcome_available = false,
} = testExpectations;
browser.test.assertEq(
declarativeNetRequest_available,
!!browser.declarativeNetRequest,
"declarativeNetRequest API namespace availability"
);
browser.test.assertEq(
testMatchOutcome_available,
!!browser.declarativeNetRequest?.testMatchOutcome,
"declarativeNetRequest.testMatchOutcome availability"
);
browser.test.sendMessage("done");
}
let extension = ExtensionTestUtils.loadExtension({
...extensionData,
background: `(${background})(${JSON.stringify(testExpectations)});`,
});
Services.prefs.setBoolPref("extensions.dnr.feedback", allowDNRFeedback);
try {
await extension.startup();
await extension.awaitMessage("done");
await extension.unload();
} finally {
Services.prefs.clearUserPref("extensions.dnr.feedback");
}
}
add_setup(async () => {
// TODO bug 1782685: Remove this check.
Assert.equal(
Services.prefs.getBoolPref("extensions.dnr.enabled", false),
false,
"DNR is disabled by default"
);
Services.prefs.setBoolPref("extensions.dnr.enabled", true);
});
// Verifies that DNR is disabled by default (until true in bug 1782685).
add_task(
{
pref_set: [["extensions.dnr.enabled", false]],
},
async function dnr_disabled_by_default() {
let { messages } = await promiseConsoleOutput(async () => {
await testAvailability({
allowDNRFeedback: PREF_DNR_FEEDBACK_DEFAULT_VALUE,
testExpectations: {
declarativeNetRequest_available: false,
},
manifest: {
permissions: [
"declarativeNetRequest",
"declarativeNetRequestFeedback",
"declarativeNetRequestWithHostAccess",
],
},
});
});
AddonTestUtils.checkMessages(messages, {
expected: [
{
message: /Reading manifest: Invalid extension permission: declarativeNetRequest$/,
},
{
message: /Reading manifest: Invalid extension permission: declarativeNetRequestFeedback/,
},
{
message: /Reading manifest: Invalid extension permission: declarativeNetRequestWithHostAccess/,
},
],
});
}
);
add_task(async function dnr_feedback_apis_disabled_by_default() {
let { messages } = await promiseConsoleOutput(async () => {
await testAvailability({
allowDNRFeedback: PREF_DNR_FEEDBACK_DEFAULT_VALUE,
testExpectations: {
declarativeNetRequest_available: true,
},
manifest: {
permissions: [
"declarativeNetRequest",
"declarativeNetRequestFeedback",
"declarativeNetRequestWithHostAccess",
],
},
});
});
AddonTestUtils.checkMessages(messages, {
expected: [
{
message: /Reading manifest: Invalid extension permission: declarativeNetRequestFeedback/,
},
],
forbidden: [
{
message: /Reading manifest: Invalid extension permission: declarativeNetRequest$/,
},
{
message: /Reading manifest: Invalid extension permission: declarativeNetRequestWithHostAccess/,
},
],
});
});
add_task(async function with_declarativeNetRequest_permission() {
await testAvailability({
allowDNRFeedback: true,
testExpectations: {
declarativeNetRequest_available: true,
// feature allowed, but missing declarativeNetRequestFeedback:
testMatchOutcome_available: false,
},
manifest: {
permissions: ["declarativeNetRequest"],
},
});
});
add_task(async function with_declarativeNetRequestWithHostAccess_permission() {
await testAvailability({
allowDNRFeedback: true,
testExpectations: {
declarativeNetRequest_available: true,
// feature allowed, but missing declarativeNetRequestFeedback:
testMatchOutcome_available: false,
},
manifest: {
permissions: ["declarativeNetRequestWithHostAccess"],
},
});
});
add_task(async function with_all_declarativeNetRequest_permissions() {
await testAvailability({
allowDNRFeedback: true,
testExpectations: {
declarativeNetRequest_available: true,
// feature allowed, but missing declarativeNetRequestFeedback:
testMatchOutcome_available: false,
},
manifest: {
permissions: [
"declarativeNetRequest",
"declarativeNetRequestWithHostAccess",
],
},
});
});
add_task(async function no_declarativeNetRequest_permission() {
await testAvailability({
allowDNRFeedback: true,
testExpectations: {
// Just declarativeNetRequestFeedback should not unlock the API.
declarativeNetRequest_available: false,
},
manifest: {
permissions: ["declarativeNetRequestFeedback"],
},
});
});
add_task(async function with_declarativeNetRequestFeedback_permission() {
await testAvailability({
allowDNRFeedback: true,
testExpectations: {
declarativeNetRequest_available: true,
// feature allowed, and all permissions specified:
testMatchOutcome_available: true,
},
manifest: {
permissions: ["declarativeNetRequest", "declarativeNetRequestFeedback"],
},
});
});
add_task(async function declarativeNetRequestFeedback_without_feature() {
await testAvailability({
allowDNRFeedback: false,
testExpectations: {
declarativeNetRequest_available: true,
// all permissions set, but DNR feedback feature not allowed.
testMatchOutcome_available: false,
},
manifest: {
permissions: ["declarativeNetRequest", "declarativeNetRequestFeedback"],
},
});
});

Просмотреть файл

@ -471,6 +471,65 @@ add_task(async function nativeMessaging_permission() {
}
});
add_task(async function declarativeNetRequest_unavailable_by_default() {
let manifestPermissions = await getManifestPermissions({
manifest: {
permissions: ["declarativeNetRequest"],
},
});
deepEqual(
manifestPermissions,
{ origins: [], permissions: [] },
"Expected declarativeNetRequest permission to be ignored/stripped"
);
});
add_task(
{ pref_set: [["extensions.dnr.enabled", true]] },
async function declarativeNetRequest_permission_with_warning() {
let manifestPermissions = await getManifestPermissions({
manifest: {
permissions: ["declarativeNetRequest"],
},
});
deepEqual(
manifestPermissions,
{ origins: [], permissions: ["declarativeNetRequest"] },
"Expected origins and permissions"
);
deepEqual(
getPermissionWarnings(manifestPermissions),
[
bundle.GetStringFromName(
"webextPerms.description.declarativeNetRequest"
),
],
"Expected warnings"
);
}
);
add_task(
{ pref_set: [["extensions.dnr.enabled", true]] },
async function declarativeNetRequest_permission_without_warning() {
let manifestPermissions = await getManifestPermissions({
manifest: {
permissions: ["declarativeNetRequestWithHostAccess"],
},
});
deepEqual(
manifestPermissions,
{ origins: [], permissions: ["declarativeNetRequestWithHostAccess"] },
"Expected origins and permissions"
);
deepEqual(getPermissionWarnings(manifestPermissions), [], "No warnings");
}
);
// Tests that the expected permission warnings are generated for a mix of host
// permissions and API permissions, for a privileged extension that uses the
// mozillaAddons permission.

Просмотреть файл

@ -626,6 +626,8 @@ const GRANTED_WITHOUT_USER_PROMPT = [
"contextMenus",
"contextualIdentities",
"cookies",
"declarativeNetRequestFeedback",
"declarativeNetRequestWithHostAccess",
"dns",
"geckoProfiler",
"identity",

Просмотреть файл

@ -101,6 +101,7 @@ skip-if = appname == "thunderbird" || os == "android" # Containers are not expos
[test_ext_cors_mozextension.js]
[test_ext_csp_frame_ancestors.js]
[test_ext_debugging_utils.js]
[test_ext_dnr_api.js]
[test_ext_dns.js]
skip-if = os == "android" # Android needs alternative for proxy.settings - bug 1723523
[test_ext_downloads.js]

Просмотреть файл

@ -58,6 +58,7 @@ skip-if = toolkit == 'android' # browser_action icon testing not supported on an
[test_ext_manifest_minimum_opera_version.js]
[test_ext_manifest_themes.js]
[test_ext_permission_warnings.js]
skip-if = condprof # Bug 1783828 - condprof fails to pick up schema changes
[test_ext_schemas.js]
head = head.js head_schemas.js
[test_ext_schemas_roots.js]