diff --git a/toolkit/components/extensions/Extension.jsm b/toolkit/components/extensions/Extension.jsm index 353f990b782a..26aa735d74cc 100644 --- a/toolkit/components/extensions/Extension.jsm +++ b/toolkit/components/extensions/Extension.jsm @@ -699,13 +699,15 @@ class ExtensionData { * that contains seperate host origins and permissions arrays. * * @param {Array} permissionsArray + * @param {Array} [hostPermissions] * @returns {Object} permissions object */ - permissionsObject(permissionsArray) { + permissionsObject(permissionsArray = [], hostPermissions = []) { let permissions = new Set(); let origins = new Set(); let { restrictSchemes, isPrivileged } = this; - for (let perm of permissionsArray || []) { + + for (let perm of permissionsArray.concat(hostPermissions)) { let type = classifyPermission(perm, restrictSchemes, isPrivileged); if (type.origin) { origins.add(perm); @@ -713,6 +715,7 @@ class ExtensionData { permissions.add(perm); } } + return { permissions, origins, @@ -732,7 +735,8 @@ class ExtensionData { } let { permissions, origins } = this.permissionsObject( - this.manifest.permissions + this.manifest.permissions, + this.manifest.host_permissions ); if ( @@ -1038,7 +1042,9 @@ class ExtensionData { isPrivileged && manifest.permissions.includes("mozillaAddons") ); - for (let perm of manifest.permissions) { + let host_permissions = manifest.host_permissions ?? []; + + for (let perm of manifest.permissions.concat(host_permissions)) { if (perm === "geckoProfiler" && !isPrivileged) { const acceptedExtensions = Services.prefs.getStringPref( "extensions.geckoProfiler.acceptedExtensionIds", @@ -1310,6 +1316,7 @@ class ExtensionData { await this.apiManager.lazyInit(); this.webAccessibleResources = manifestData.webAccessibleResources; + this.allowedOrigins = new MatchPatternSet(manifestData.originPermissions, { restrictSchemes: this.restrictSchemes, }); diff --git a/toolkit/components/extensions/ExtensionTestCommon.jsm b/toolkit/components/extensions/ExtensionTestCommon.jsm index 015e24691182..8a1331f14f0e 100644 --- a/toolkit/components/extensions/ExtensionTestCommon.jsm +++ b/toolkit/components/extensions/ExtensionTestCommon.jsm @@ -277,6 +277,15 @@ ExtensionTestCommon = class ExtensionTestCommon { provide(manifest, ["manifest_version"], 2); provide(manifest, ["version"], "1.0"); + // Make it easier to test same manifest in both MV2 and MV3 configurations. + if (manifest.manifest_version === 2 && manifest.host_permissions) { + manifest.permissions = [].concat( + manifest.permissions || [], + manifest.host_permissions + ); + delete manifest.host_permissions; + } + if (data.background) { let bgScript = uuidGen.generateUUID().number + ".js"; diff --git a/toolkit/components/extensions/schemas/manifest.json b/toolkit/components/extensions/schemas/manifest.json index 33c5f853154a..5264825d3268 100644 --- a/toolkit/components/extensions/schemas/manifest.json +++ b/toolkit/components/extensions/schemas/manifest.json @@ -213,13 +213,37 @@ }, "permissions": { - "type": "array", "default": [], + "optional": true, + "choices": [ + { + "max_manifest_version": 2, + "type": "array", + "items": { + "$ref": "PermissionOrOrigin", + "onError": "warn" + } + }, + { + "min_manifest_version": 3, + "type": "array", + "items": { + "$ref": "Permission", + "onError": "warn" + } + } + ] + }, + + "host_permissions": { + "min_manifest_version": 3, + "type": "array", "items": { - "$ref": "PermissionOrOrigin", + "$ref": "MatchPattern", "onError": "warn" }, - "optional": true + "optional": true, + "default": [] }, "optional_permissions": { diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_csp.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_csp.js index 8ff9e8e8b5b0..d0035cbdf15d 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_csp.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_csp.js @@ -403,7 +403,8 @@ async function runCSPTest(test) { js: ["content_script.js"], }, ], - permissions: ["webRequest", "webRequestBlocking", ""], + permissions: ["webRequest", "webRequestBlocking"], + host_permissions: [""], background: { scripts: ["background.js"] }, }, files: { diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js b/toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js index 24563cc79c22..6de2df0f6b8f 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js @@ -23,6 +23,8 @@ AddonTestUtils.overrideCertDB(); AddonTestUtils.usePrivilegedSignatures = id => id.startsWith("privileged"); createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); +Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); + async function getManifestPermissions(extensionData) { let extension = ExtensionTestCommon.generate(extensionData); // Some tests contain invalid permissions; ignore the warnings about their invalidity. @@ -341,30 +343,40 @@ add_task(async function host_permissions() { ], }, ]; - for (let { - description, - manifest, - expectedOrigins, - expectedWarnings, - options, - } of permissionTestCases) { - let manifestPermissions = await getManifestPermissions({ + for (let manifest_version of [2, 3]) { + for (let { + description, manifest, - }); - - deepEqual( - manifestPermissions.origins, expectedOrigins, - `Expected origins (${description})` - ); - deepEqual( - manifestPermissions.permissions, - [], - `Expected no non-host permissions (${description})` - ); + expectedWarnings, + options, + } of permissionTestCases) { + manifest = Object.assign({}, manifest, { manifest_version }); + if (manifest_version > 2) { + manifest.host_permissions = manifest.permissions; + manifest.permissions = []; + } - let warnings = getPermissionWarnings(manifestPermissions, options); - deepEqual(warnings, expectedWarnings, `Expected warnings (${description})`); + let manifestPermissions = await getManifestPermissions({ manifest }); + + deepEqual( + manifestPermissions.origins, + expectedOrigins, + `Expected origins (${description})` + ); + deepEqual( + manifestPermissions.permissions, + [], + `Expected no non-host permissions (${description})` + ); + + let warnings = getPermissionWarnings(manifestPermissions, options); + deepEqual( + warnings, + expectedWarnings, + `Expected warnings (${description})` + ); + } } }); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js index d203586d87f1..aeb4df90107d 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js @@ -7,6 +7,8 @@ const { ExtensionPermissions } = ChromeUtils.import( "resource://gre/modules/ExtensionPermissions.jsm" ); +Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); + // ExtensionParent.jsm is being imported lazily because when it is imported Services.appinfo will be // retrieved and cached (as a side-effect of Schemas.jsm being imported), and so Services.appinfo // will not be returning the version set by AddonTestUtils.createAppInfo and this test will @@ -105,7 +107,7 @@ add_task(async function test_permissions_on_startup() { await extension.unload(); }); -add_task(async function test_permissions() { +async function test_permissions(manifest_version) { const REQUIRED_PERMISSIONS = ["downloads"]; const REQUIRED_ORIGINS = ["*://site.com/", "*://*.domain.com/"]; const REQUIRED_ORIGINS_NORMALIZED = ["*://site.com/*", "*://*.domain.com/*"]; @@ -153,7 +155,9 @@ add_task(async function test_permissions() { let extension = ExtensionTestUtils.loadExtension({ background, manifest: { - permissions: [...REQUIRED_PERMISSIONS, ...REQUIRED_ORIGINS], + manifest_version, + permissions: REQUIRED_PERMISSIONS, + host_permissions: REQUIRED_ORIGINS, optional_permissions: [...OPTIONAL_PERMISSIONS, ...OPTIONAL_ORIGINS], }, useAddonManager: "permanent", @@ -339,7 +343,9 @@ add_task(async function test_permissions() { deepEqual(result, perms, "Back to default permissions after removing more"); await extension.unload(); -}); +} +add_task(() => test_permissions(2)); +add_task(() => test_permissions(3)); add_task(async function test_startup() { async function background() { @@ -413,14 +419,15 @@ add_task(async function test_startup() { }); // Test that we don't prompt for permissions an extension already has. -add_task(async function test_alreadyGranted() { - const REQUIRED_PERMISSIONS = [ - "geolocation", +async function test_alreadyGranted(manifest_version) { + const REQUIRED_PERMISSIONS = ["geolocation"]; + const REQUIRED_ORIGINS = [ "*://required-host.com/", "*://*.required-domain.com/", ]; const OPTIONAL_PERMISSIONS = [ ...REQUIRED_PERMISSIONS, + ...REQUIRED_ORIGINS, "clipboardRead", "*://optional-host.com/", "*://*.optional-domain.com/", @@ -448,7 +455,9 @@ add_task(async function test_alreadyGranted() { }, manifest: { + manifest_version, permissions: REQUIRED_PERMISSIONS, + host_permissions: REQUIRED_ORIGINS, optional_permissions: OPTIONAL_PERMISSIONS, }, @@ -537,7 +546,9 @@ add_task(async function test_alreadyGranted() { }); await extension.unload(); -}); +} +add_task(() => test_alreadyGranted(2)); +add_task(() => test_alreadyGranted(3)); // IMPORTANT: Do not change this list without review from a Web Extensions peer! @@ -638,7 +649,7 @@ add_task(async function test_optional_all_urls() { }); // Check that optional permissions are not included in update prompts -add_task(async function test_permissions_prompt() { +async function test_permissions_prompt(manifest_version) { function background() { browser.test.onMessage.addListener(async (msg, arg) => { if (msg == "request") { @@ -653,10 +664,11 @@ add_task(async function test_permissions_prompt() { manifest: { name: "permissions test", description: "permissions test", - manifest_version: 2, + manifest_version, version: "1.0", - permissions: ["tabs", "https://test1.example.com/*"], + permissions: ["tabs"], + host_permissions: ["https://test1.example.com/*"], optional_permissions: ["clipboardWrite", ""], content_scripts: [ @@ -687,12 +699,13 @@ add_task(async function test_permissions_prompt() { manifest: { name: "permissions test", description: "permissions test", - manifest_version: 2, + manifest_version, version: "2.0", applications: { gecko: { id: extension.id } }, - permissions: [...PERMS, ...ORIGINS], + permissions: PERMS, + host_permissions: ORIGINS, optional_permissions: ["clipboardWrite", ""], }, }); @@ -727,7 +740,9 @@ add_task(async function test_permissions_prompt() { ); await extension.unload(); -}); +} +add_task(() => test_permissions_prompt(2)); +add_task(() => test_permissions_prompt(3)); // Check that internal permissions can not be set and are not returned by the API. add_task(async function test_internal_permissions() {