Bug 1917000 - Turn "userScripts" into optional-only permission r=zombie

Mark "userScripts" as an optional-only permission and update all tests
that expect it to be a required permission, which is not supported.

This also improves the implementation to account for the fact that a
permission can be granted post install.

The disabling of functionality upon revoking the permission will be in
the next patch.

Differential Revision: https://phabricator.services.mozilla.com/D229712
This commit is contained in:
Rob Wu 2024-11-22 16:03:11 +00:00
parent c30c966f38
commit ce07cca0f1
9 changed files with 266 additions and 20 deletions

View File

@ -1886,14 +1886,42 @@ export class ExtensionData {
}
}
const shouldIgnorePermission = (perm, verbose = true) => {
if (perm === "userScripts" && !lazy.userScriptsMV3Enabled) {
if (verbose) {
this.manifestWarning(`Unavailable extension permission: ${perm}`);
}
return true;
}
if (isMV2 && PERMS_NOT_IN_MV2.has(perm)) {
if (verbose) {
this.manifestWarning(
`Permission "${perm}" requires Manifest Version 3.`
);
}
return true;
}
return false;
};
for (let i = manifest.optional_permissions.length - 1; i >= 0; --i) {
if (shouldIgnorePermission(manifest.optional_permissions[i])) {
manifest.optional_permissions.splice(i, 1);
}
}
if (this.id) {
// An extension always gets permission to its own url.
let matcher = new MatchPattern(this.getURL(), { ignorePath: true });
originPermissions.add(matcher.pattern);
// Apply optional permissions
// TODO bug 1766915: Validate that the permissions are available.
let perms = await lazy.ExtensionPermissions.get(this.id);
for (let perm of perms.permissions) {
if (shouldIgnorePermission(perm, /* verbose */ false)) {
continue;
}
permissions.add(perm);
}
for (let origin of perms.origins) {

View File

@ -6,6 +6,10 @@
"use strict";
ChromeUtils.defineESModuleGetters(this, {
ExtensionUserScripts: "resource://gre/modules/ExtensionUserScripts.sys.mjs",
});
var { ExtensionUtils } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionUtils.sys.mjs"
);
@ -111,6 +115,14 @@ this.userScripts = class extends ExtensionAPI {
}
}
if (!extension.userScriptsManager) {
// extension.userScriptsManager is initialized by initExtension() at
// extension startup when the extension has the "userScripts" permission.
// When we get here, it means that "userScripts" was requested after
// startup, and we need to initialize it here.
ExtensionUserScripts.initExtension(extension);
}
const usm = extension.userScriptsManager;
return {

View File

@ -3,7 +3,7 @@
"namespace": "manifest",
"types": [
{
"$extend": "OptionalPermission",
"$extend": "OptionalOnlyPermission",
"choices": [
{
"type": "string",

View File

@ -24,9 +24,27 @@ ExtensionTestUtils.failOnSchemaWarnings(false);
// the properties are exposed to content scripts (through lazy getters).
const ARE_NON_CONTENT_USER_SCRIPTS_APIS_EXPOSED_TO_CONTENT = AppConstants.DEBUG;
const { ExtensionPermissions } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionPermissions.sys.mjs"
);
const server = createHttpServer({ hosts: ["example.com"] });
server.registerPathHandler("/dummy", () => {});
async function grantUserScriptsPermission(extensionId) {
// The userScripts permission is an optional-only permission. Throughout this
// test, whenever we want to verify that the userScripts API is unavailable
// for non-permission reasons, we grant the permission before starting the
// extension, to rule out permission issues as the cause of the failure.
//
// When an extension is started, any optional permissions stored in the
// permission database are included.
await ExtensionPermissions.add(extensionId, {
permissions: ["userScripts"],
origins: [],
});
}
add_setup(() => {
Services.prefs.setBoolPref("extensions.userScripts.mv3.enabled", true);
});
@ -114,10 +132,14 @@ add_task(async function legacy_userScripts_unavailable_in_mv3_content_script() {
// Tests that when the legacy user_scripts key is present and the userScripts
// permission, that none of the MV3-specific functionality is exposed.
add_task(async function legacy_userScripts_plus_userScripts_permission_mv2() {
const extensionId = "@legacy_userScripts_plus_userScripts_permission_mv2";
await grantUserScriptsPermission(extensionId);
let extension = ExtensionTestUtils.loadExtension({
manifest: {
browser_specific_settings: { gecko: { id: extensionId } },
manifest_version: 2,
permissions: ["userScripts", "*://example.com/*"],
permissions: ["*://example.com/*"],
optional_permissions: ["userScripts"],
user_scripts: {
api_script: "api_script.js",
},
@ -248,10 +270,13 @@ add_task(async function legacy_userScripts_plus_userScripts_permission_mv2() {
// Test that there are no traces of the legacy userScripts API in MV3, but only
// the new userScripts API in MV3.
add_task(async function legacy_userScripts_plus_userScripts_permission_mv3() {
const extensionId = "@legacy_userScripts_plus_userScripts_permission_mv3";
await grantUserScriptsPermission(extensionId);
let extension = ExtensionTestUtils.loadExtension({
manifest: {
browser_specific_settings: { gecko: { id: extensionId } },
manifest_version: 3,
permissions: ["userScripts"],
optional_permissions: ["userScripts"],
host_permissions: ["*://example.com/*"],
user_scripts: {
api_script: "api_script.js",
@ -381,10 +406,13 @@ add_task(async function legacy_userScripts_plus_userScripts_permission_mv3() {
});
async function do_test_userScripts_permission_unavailable(manifest_version) {
const extensionId = `@permission_disabled_by_pref_mv${manifest_version}`;
await grantUserScriptsPermission(extensionId);
let extension = ExtensionTestUtils.loadExtension({
manifest: {
browser_specific_settings: { gecko: { id: extensionId } },
manifest_version,
permissions: ["userScripts"],
optional_permissions: ["userScripts"],
},
background() {
browser.test.assertEq(
@ -400,7 +428,7 @@ async function do_test_userScripts_permission_unavailable(manifest_version) {
Assert.deepEqual(
extension.extension.warnings,
["Reading manifest: Invalid extension permission: userScripts"],
["Reading manifest: Unavailable extension permission: userScripts"],
"userScripts permission unavailable when off by pref"
);
@ -422,3 +450,81 @@ add_task(
await do_test_userScripts_permission_unavailable(3);
}
);
add_task(
{
pref_set: [
["extensions.userScripts.mv3.enabled", false],
["extensions.webextOptionalPermissionPrompts", false],
[
// This pref controls the Cu.isInAutomation flag that is needed to use
// browser.test.withHandlingUserInput in xpcshell tests (bug 1598804):
"security.turn_off_all_security_so_that_viruses_can_take_over_this_computer",
true,
],
],
},
async function reject_userScripts_permission_request_when_disabled_by_pref() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
manifest_version: 3,
optional_permissions: ["userScripts"],
},
async background() {
let prom;
browser.test.withHandlingUserInput(() => {
prom = browser.permissions.request({ permissions: ["userScripts"] });
});
await browser.test.assertRejects(
prom,
"Cannot request permission userScripts since it was not declared in optional_permissions",
"userScripts permission cannot be requested when off by pref"
);
browser.test.assertEq(undefined, browser.userScripts, "No API please");
browser.test.sendMessage("done");
},
});
await extension.startup();
await extension.awaitMessage("done");
await extension.unload();
}
);
add_task(
{
pref_set: [
["extensions.webextOptionalPermissionPrompts", false],
[
// This pref controls the Cu.isInAutomation flag that is needed to use
// browser.test.withHandlingUserInput in xpcshell tests (bug 1598804):
"security.turn_off_all_security_so_that_viruses_can_take_over_this_computer",
true,
],
],
},
async function enable_userScripts_via_permissions_request() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
manifest_version: 3,
optional_permissions: ["userScripts"],
},
async background() {
let prom;
browser.test.withHandlingUserInput(() => {
prom = browser.permissions.request({ permissions: ["userScripts"] });
});
browser.test.assertTrue(
await prom,
"permissions.request() can grant userScripts permission"
);
browser.test.assertTrue(browser.userScripts, "userScripts API granted");
browser.test.sendMessage("done");
},
});
await extension.startup();
await extension.awaitMessage("done");
await extension.unload();
}
);

View File

@ -15,6 +15,13 @@ AddonTestUtils.overrideCertDB();
add_setup(async () => {
Services.prefs.setBoolPref("extensions.userScripts.mv3.enabled", true);
// Grant "userScripts" permission via permissions.request() without UI.
Services.prefs.setBoolPref(
"extensions.webextOptionalPermissionPrompts",
false
);
await ExtensionTestUtils.startAddonManager();
});
@ -37,7 +44,7 @@ async function startEvalTesterExtension() {
useAddonManager: "permanent",
manifest: {
manifest_version: 3,
permissions: ["userScripts"],
optional_permissions: ["userScripts"],
host_permissions: ["*://example.com/*"],
},
background() {
@ -52,7 +59,9 @@ async function startEvalTesterExtension() {
}
}
browser.test.onMessage.addListener(async (msg, args) => {
if (msg === "registerUserScriptForWorldId") {
if (msg === "grantUserScriptsPermission") {
await browser.permissions.request({ permissions: ["userScripts"] });
} else if (msg === "registerUserScriptForWorldId") {
const worldId = args;
await browser.userScripts.register([
{
@ -86,6 +95,11 @@ async function startEvalTesterExtension() {
await extension.startup();
await withHandlingUserInput(extension, async () => {
extension.sendMessage("grantUserScriptsPermission");
await extension.awaitMessage("grantUserScriptsPermission:done");
});
async function queryExtension(msg, args) {
info(`queryExtension(${msg}, ${args && JSON.stringify(args)})`);
extension.sendMessage(msg, args);

View File

@ -11,6 +11,19 @@ server.registerPathHandler("/resultCollector", (request, response) => {
AddonTestUtils.init(this);
AddonTestUtils.overrideCertDB();
const { ExtensionPermissions } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionPermissions.sys.mjs"
);
async function grantUserScriptsPermission(extensionId) {
// userScripts is optional-only, and we must grant it. See comment at
// grantUserScriptsPermission in test_ext_userScripts_mv3_availability.js.
await ExtensionPermissions.add(extensionId, {
permissions: ["userScripts"],
origins: [],
});
}
async function collectResults(contentPage) {
return contentPage.spawn([], () => {
return this.content.wrappedJSObject.resultCollector;
@ -25,11 +38,14 @@ add_setup(async () => {
// Tests that user scripts can run in the MAIN world, and only for new document
// loads (not existing ones).
add_task(async function userScript_runs_in_MAIN_world() {
const extensionId = "@userScript_runs_in_MAIN_world";
await grantUserScriptsPermission(extensionId);
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
browser_specific_settings: { gecko: { id: extensionId } },
manifest_version: 3,
permissions: ["userScripts"],
optional_permissions: ["userScripts"],
host_permissions: ["*://example.com/*"],
},
files: {
@ -85,11 +101,14 @@ add_task(async function userScript_runs_in_MAIN_world() {
});
add_task(async function userScript_require_host_permissions() {
const extensionId = "@userScript_require_host_permissions";
await grantUserScriptsPermission(extensionId);
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
browser_specific_settings: { gecko: { id: extensionId } },
manifest_version: 3,
permissions: ["userScripts"],
optional_permissions: ["userScripts"],
host_permissions: ["*://example.net/*"],
},
async background() {
@ -151,11 +170,14 @@ add_task(async function userScript_require_host_permissions() {
// Tests that user scripts can run in the USER_SCRIPT world, and only for new
// document loads (not existing ones).
add_task(async function userScript_runs_in_USER_SCRIPT_world() {
const extensionId = "@userScript_runs_in_USER_SCRIPT_world";
await grantUserScriptsPermission(extensionId);
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
browser_specific_settings: { gecko: { id: extensionId } },
manifest_version: 3,
permissions: ["userScripts"],
optional_permissions: ["userScripts"],
host_permissions: ["*://example.com/*"],
},
files: {

View File

@ -14,17 +14,33 @@ server.registerPathHandler("/dummy", () => {});
AddonTestUtils.init(this);
AddonTestUtils.overrideCertDB();
const { ExtensionPermissions } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionPermissions.sys.mjs"
);
async function grantUserScriptsPermission(extensionId) {
// userScripts is optional-only, and we must grant it. See comment at
// grantUserScriptsPermission in test_ext_userScripts_mv3_availability.js.
await ExtensionPermissions.add(extensionId, {
permissions: ["userScripts"],
origins: [],
});
}
add_setup(async () => {
Services.prefs.setBoolPref("extensions.userScripts.mv3.enabled", true);
await ExtensionTestUtils.startAddonManager();
});
add_task(async function test_runtime_messaging_errors() {
const extensionId = "@test_runtime_messaging_errors";
await grantUserScriptsPermission(extensionId);
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
browser_specific_settings: { gecko: { id: extensionId } },
manifest_version: 3,
permissions: ["userScripts"],
optional_permissions: ["userScripts"],
host_permissions: ["*://example.com/*"],
content_scripts: [
{
@ -154,11 +170,14 @@ add_task(async function test_runtime_messaging_errors() {
// And that the messaging flag persists across restarts.
// Moreover, that runtime.sendMessage can wake up an event page.
add_task(async function test_onUserScriptMessage() {
const extensionId = "@test_onUserScriptMessage";
await grantUserScriptsPermission(extensionId);
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
browser_specific_settings: { gecko: { id: extensionId } },
manifest_version: 3,
permissions: ["userScripts"],
optional_permissions: ["userScripts"],
host_permissions: ["*://example.com/*"],
},
files: {
@ -265,11 +284,14 @@ add_task(async function test_onUserScriptMessage() {
// even if runtime.onMessage is triggered from a content script, that it does
// not have the userScripts-specific "sender.userScriptWorldId" field.
add_task(async function test_configureWorld_messaging_existing_world() {
const extensionId = "@test_configureWorld_messaging_existing_world";
await grantUserScriptsPermission(extensionId);
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
browser_specific_settings: { gecko: { id: extensionId } },
manifest_version: 3,
permissions: ["userScripts"],
optional_permissions: ["userScripts"],
host_permissions: ["*://example.com/*"],
content_scripts: [
{
@ -383,11 +405,14 @@ add_task(async function test_configureWorld_messaging_existing_world() {
// This test tests that runtime.connect() works when called from a user script.
add_task(async function test_onUserScriptConnect() {
const extensionId = "@test_onUserScriptConnect";
await grantUserScriptsPermission(extensionId);
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
browser_specific_settings: { gecko: { id: extensionId } },
manifest_version: 3,
permissions: ["userScripts"],
optional_permissions: ["userScripts"],
host_permissions: ["*://example.com/*"],
},
files: {
@ -510,11 +535,14 @@ add_task(
pref_set: [["docshell.shistory.bfcache.allow_unload_listeners", false]],
},
async function test_onUserScriptConnect_port_disconnect_on_navigate() {
const extensionId = "@test_onUserScriptConnect_port_disconnect_on_navigate";
await grantUserScriptsPermission(extensionId);
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
browser_specific_settings: { gecko: { id: extensionId } },
manifest_version: 3,
permissions: ["userScripts"],
optional_permissions: ["userScripts"],
host_permissions: ["*://example.com/*"],
},
files: {

View File

@ -1,5 +1,9 @@
"use strict";
const { ExtensionPermissions } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionPermissions.sys.mjs"
);
const { ExtensionTestCommon } = ChromeUtils.importESModule(
"resource://testing-common/ExtensionTestCommon.sys.mjs"
);
@ -12,11 +16,18 @@ AddonTestUtils.init(this);
AddonTestUtils.overrideCertDB();
function loadTestExtension({ background }) {
const id = Services.uuid.generateUUID().number;
// "userScripts" is an optional-only permission, so we need to grant it.
// ExtensionPermissions.add() is async, but not waiting here is OK for us:
// Extension startup is blocked on reading permissions, which in turn awaits
// any previous ExtensionPermissions calls in FIFO order.
ExtensionPermissions.add(id, { permissions: ["userScripts"], origins: [] });
return ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
browser_specific_settings: { gecko: { id } },
manifest_version: 3,
permissions: ["userScripts"],
optional_permissions: ["userScripts"],
},
background,
files: {

View File

@ -6,6 +6,19 @@ server.registerPathHandler("/dummy", () => {});
AddonTestUtils.init(this);
AddonTestUtils.overrideCertDB();
const { ExtensionPermissions } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionPermissions.sys.mjs"
);
async function grantUserScriptsPermission(extensionId) {
// userScripts is optional-only, and we must grant it. See comment at
// grantUserScriptsPermission in test_ext_userScripts_mv3_availability.js.
await ExtensionPermissions.add(extensionId, {
permissions: ["userScripts"],
origins: [],
});
}
async function spawnPage(spawnFunc) {
let contentPage = await ExtensionTestUtils.loadContentPage(
"http://example.com/dummy"
@ -21,11 +34,14 @@ add_setup(async () => {
});
add_task(async function default_USER_SCRIPT_world_behavior() {
const extensionId = "@default_USER_SCRIPT_world_behavior";
await grantUserScriptsPermission(extensionId);
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
browser_specific_settings: { gecko: { id: extensionId } },
manifest_version: 3,
permissions: ["userScripts"],
optional_permissions: ["userScripts"],
host_permissions: ["*://example.com/*"],
},
files: {
@ -77,11 +93,14 @@ add_task(async function default_USER_SCRIPT_world_behavior() {
});
add_task(async function multiple_scripts_share_same_default_world() {
const extensionId = "@multiple_scripts_share_same_default_world";
await grantUserScriptsPermission(extensionId);
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
browser_specific_settings: { gecko: { id: extensionId } },
manifest_version: 3,
permissions: ["userScripts"],
optional_permissions: ["userScripts"],
host_permissions: ["*://example.com/*"],
},
async background() {
@ -143,6 +162,8 @@ add_task(async function multiple_scripts_share_same_default_world() {
});
add_task(async function test_worldId_validation() {
const extensionId = "@test_worldId_validation";
await grantUserScriptsPermission(extensionId);
async function background() {
const id = "single user script id";
function testRegister(props) {
@ -231,8 +252,9 @@ add_task(async function test_worldId_validation() {
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
browser_specific_settings: { gecko: { id: extensionId } },
manifest_version: 3,
permissions: ["userScripts"],
optional_permissions: ["userScripts"],
},
background,
});
@ -242,11 +264,14 @@ add_task(async function test_worldId_validation() {
});
add_task(async function test_default_and_many_non_default_worldIds() {
const extensionId = "@test_default_and_many_non_default_worldIds";
await grantUserScriptsPermission(extensionId);
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
browser_specific_settings: { gecko: { id: extensionId } },
manifest_version: 3,
permissions: ["userScripts"],
optional_permissions: ["userScripts"],
host_permissions: ["*://example.com/*"],
},
async background() {