diff --git a/browser/components/enterprisepolicies/schemas/policies-schema.json b/browser/components/enterprisepolicies/schemas/policies-schema.json index 5747b725377a..220ccfba435b 100644 --- a/browser/components/enterprisepolicies/schemas/policies-schema.json +++ b/browser/components/enterprisepolicies/schemas/policies-schema.json @@ -710,6 +710,9 @@ }, "temporarily_allow_weak_signatures": { "type": "boolean" + }, + "private_browsing": { + "type": "boolean" } } } diff --git a/browser/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings2.js b/browser/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings2.js index dd610ec7e553..cc42bc1c7acc 100644 --- a/browser/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings2.js +++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings2.js @@ -2,6 +2,10 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; +const { ExtensionPermissions } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionPermissions.sys.mjs" +); + const ADDON_ID = "policytest@mozilla.com"; const BASE_URL = "http://mochi.test:8888/browser/browser/components/enterprisepolicies/tests/browser"; @@ -18,6 +22,59 @@ async function isExtensionLockedAndUpdateDisabled(win, addonID) { is(updateRow.hidden, true, "Update row should be hidden"); } +add_task(async function test_addon_private_browser_access_locked() { + async function installWithExtensionSettings(extensionSettings = {}) { + let installPromise = waitForAddonInstall(ADDON_ID); + await setupPolicyEngineWithJson({ + policies: { + ExtensionSettings: { + "policytest@mozilla.com": { + install_url: `${BASE_URL}/policytest_v0.1.xpi`, + installation_mode: "force_installed", + updates_disabled: true, + ...extensionSettings, + }, + }, + }, + }); + await installPromise; + let addon = await AddonManager.getAddonByID(ADDON_ID); + isnot(addon, null, "Addon not installed."); + is(addon.version, "0.1", "Addon version is correct"); + return addon; + } + + let addon = await installWithExtensionSettings(); + is( + Boolean( + addon.permissions & AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS + ), + true, + "Addon should be able to change private browsing setting (not set in policy)." + ); + await addon.uninstall(); + + addon = await installWithExtensionSettings({ private_browsing: true }); + is( + Boolean( + addon.permissions & AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS + ), + false, + "Addon should NOT be able to change private browsing setting (set to true in policy)." + ); + await addon.uninstall(); + + addon = await installWithExtensionSettings({ private_browsing: false }); + is( + Boolean( + addon.permissions & AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS + ), + false, + "Addon should NOT be able to change private browsing setting (set to false in policy)." + ); + await addon.uninstall(); +}); + add_task(async function test_addon_install() { let installPromise = waitForAddonInstall(ADDON_ID); await setupPolicyEngineWithJson({ diff --git a/browser/components/enterprisepolicies/tests/xpcshell/.eslintrc.js b/browser/components/enterprisepolicies/tests/xpcshell/.eslintrc.js new file mode 100644 index 000000000000..e57058ecb1c4 --- /dev/null +++ b/browser/components/enterprisepolicies/tests/xpcshell/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + env: { + webextensions: true, + }, +}; diff --git a/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js b/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js index 8fa2d72d1451..f2165f9461f3 100644 --- a/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js +++ b/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js @@ -15,6 +15,7 @@ const { ExtensionTestUtils } = ChromeUtils.importESModule( AddonTestUtils.init(this); AddonTestUtils.overrideCertDB(); AddonTestUtils.appInfo = getAppInfo(); +AddonTestUtils.usePrivilegedSignatures = false; ExtensionTestUtils.init(this); const server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); @@ -555,7 +556,7 @@ add_task(async function test_policy_installed_only_addon() { // Setting back the extension settings with installation_mode set to force_installed // will install the extension again, and so we need to wait for that and uninstall // it first (otherwise the addon may endup being installed when the test task is - // completed and trigger an intermittent failre). + // completed and trigger an intermittent failure). installPromise = waitForAddonInstall(policyOnlyID); await setupPolicyEngineWithJson({ policies: { @@ -613,3 +614,103 @@ add_task(async function test_policy_installed_only_addon() { "Got the expect error logged on installing enterprise only extension" ); }); + +add_task(async function test_private_browsing() { + async function assertPrivateBrowsingAccess({ + addonId, + extensionSettings, + extensionManifest = {}, + expectedPrivateBrowsingAccess, + message, + }) { + await setupPolicyEngineWithJson({ + policies: { + ExtensionSettings: { + [addonId]: { ...extensionSettings }, + }, + }, + }); + + let ext = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { id: addonId }, + }, + ...extensionManifest, + }, + useAddonManager: "temporary", + background() { + browser.test.onMessage.addListener(async msg => { + switch (msg) { + case "checkPrivateBrowsing": { + let isAllowed = + await browser.extension.isAllowedIncognitoAccess(); + browser.test.sendMessage("privateBrowsing", isAllowed); + break; + } + } + }); + }, + }); + + await ext.startup(); + + ext.sendMessage("checkPrivateBrowsing"); + let isAllowedIncognitoAccess = await ext.awaitMessage("privateBrowsing"); + Assert.equal( + isAllowedIncognitoAccess, + expectedPrivateBrowsingAccess, + message + ); + + let addon = await AddonManager.getAddonByID(ext.id); + // Sanity check (to ensure the test extension used in this test are not privileged). + ok(!addon.isPrivileged, "Addon should not be privileged"); + + let expectLocked = typeof extensionSettings?.private_browsing === "boolean"; + Assert.equal( + !( + addon.permissions & AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS + ), + expectLocked, + `Addon should ${expectLocked ? "NOT" : ""} be able to change private browsing setting.` + ); + + await ext.unload(); + } + + await assertPrivateBrowsingAccess({ + addonId: "privatebrowsing-granted-nonprivileged@test", + extensionSettings: { private_browsing: true }, + expectedPrivateBrowsingAccess: true, + message: + "Should have access to private browsing (set to true in policy extension setting)", + }); + + await assertPrivateBrowsingAccess({ + addonId: "privatebrowsing-revoked-nonprivileged@test", + extensionSettings: { private_browsing: false }, + expectedPrivateBrowsingAccess: false, + message: + "Should NOT have access to private browsing (set to false in policy extension setting)", + }); + + await assertPrivateBrowsingAccess({ + addonId: "privatebrowsing-nosetting-nonprivileged@test", + extensionSettings: {}, + expectedPrivateBrowsingAccess: false, + message: + "Should NOT have access to private browsing (NOT set in policy extension setting)", + }); + + await assertPrivateBrowsingAccess({ + addonId: "privatebrowsing-notallowed-nonprivileged@test", + extensionSettings: { private_browsing: true }, + extensionManifest: { + incognito: "not_allowed", + }, + expectedPrivateBrowsingAccess: false, + message: + "incognito 'not_allowed' extensions should NOT have access to private browser", + }); +}); diff --git a/toolkit/components/extensions/Extension.sys.mjs b/toolkit/components/extensions/Extension.sys.mjs index 47a6d1a3e1de..99ffd549c32c 100644 --- a/toolkit/components/extensions/Extension.sys.mjs +++ b/toolkit/components/extensions/Extension.sys.mjs @@ -3738,7 +3738,8 @@ export class Extension extends ExtensionData { // We automatically add permissions to system/built-in extensions. // Extensions expliticy stating not_allowed will never get permission. let isAllowed = this.permissions.has(PRIVATE_ALLOWED_PERMISSION); - if (this.manifest.incognito === "not_allowed") { + const hasIncognitoNotAllowed = this.manifest.incognito === "not_allowed"; + if (hasIncognitoNotAllowed) { // If an extension previously had permission, but upgrades/downgrades to // a version that specifies "not_allowed" in manifest, remove the // permission. @@ -3764,6 +3765,32 @@ export class Extension extends ExtensionData { this.permissions.add(PRIVATE_ALLOWED_PERMISSION); } + // On builds where Enterprise Policies are supported, grant or revoke + // the private browsing access for extensions that are not app provided + // (system and builtin add-ons) or hidden. + if ( + Services.policies && + !this.isAppProvided && + !this.isHidden && + !hasIncognitoNotAllowed && + this.type === "extension" + ) { + const settings = Services.policies.getExtensionSettings(this.id); + if (settings?.private_browsing) { + lazy.ExtensionPermissions.add(this.id, { + permissions: [PRIVATE_ALLOWED_PERMISSION], + origins: [], + }); + this.permissions.add(PRIVATE_ALLOWED_PERMISSION); + } else if (settings?.private_browsing === false) { + lazy.ExtensionPermissions.remove(this.id, { + permissions: [PRIVATE_ALLOWED_PERMISSION], + origins: [], + }); + this.permissions.delete(PRIVATE_ALLOWED_PERMISSION); + } + } + // We only want to update the SVG_CONTEXT_PROPERTIES_PERMISSION during // install and upgrade/downgrade startups. if (INSTALL_AND_UPDATE_STARTUP_REASONS.has(this.startupReason)) { diff --git a/toolkit/mozapps/extensions/internal/XPIDatabase.sys.mjs b/toolkit/mozapps/extensions/internal/XPIDatabase.sys.mjs index 0b885f6d75f9..8654a28eb323 100644 --- a/toolkit/mozapps/extensions/internal/XPIDatabase.sys.mjs +++ b/toolkit/mozapps/extensions/internal/XPIDatabase.sys.mjs @@ -910,9 +910,12 @@ export class AddonInternal { } } + let settings = Services.policies?.getExtensionSettings(this.id) || {}; // The permission to "toggle the private browsing access" is locked down // when the extension has opted out or it gets the permission automatically - // on every extension startup (as system, privileged and builtin addons). + // on every extension startup (as system, privileged and builtin addons) or + // when private browsing access as been set and locked through enterprise + // policy settings. if ( (this.type === "extension" || // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based implementation is also removed. @@ -920,7 +923,8 @@ export class AddonInternal { this.incognito !== "not_allowed" && this.signedState !== lazy.AddonManager.SIGNEDSTATE_PRIVILEGED && this.signedState !== lazy.AddonManager.SIGNEDSTATE_SYSTEM && - !this.location.isBuiltin + !this.location.isBuiltin && + !("private_browsing" in settings) ) { permissions |= lazy.AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS; } diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_builtin_location.js b/toolkit/mozapps/extensions/test/xpcshell/test_builtin_location.js index 30801e9f7281..5934a30d31b2 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_builtin_location.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_builtin_location.js @@ -11,9 +11,10 @@ AddonTestUtils.createAppInfo( "1" ); -async function getWrapper(id, hidden) { +async function getWrapper(id, hidden, { manifest } = {}) { let wrapper = await installBuiltinExtension({ manifest: { + ...manifest, browser_specific_settings: { gecko: { id } }, hidden, }, @@ -147,3 +148,79 @@ add_task(async function test_builtin_update() { await wrapper.unload(); await AddonTestUtils.promiseShutdownManager(); }); + +// Tests private_browsing policy extension settings doesn't apply +// to built-in extensions. +add_task( + // This test is skipped on builds that don't support enterprise policies + // (e.g. android builds). + { skip_if: () => !Services.policies }, + async function test_builtin_private_browsing_policy_setting() { + await AddonTestUtils.promiseStartupManager(); + + const { EnterprisePolicyTesting } = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" + ); + + let id = "builtin-addon@tests.mozilla.org"; + let idNotAllowed = "builtin-not-allowed@tests.mozilla.org"; + + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + ExtensionSettings: { + [id]: { + private_browsing: false, + }, + [idNotAllowed]: { + private_browsing: true, + }, + }, + }, + }); + + // Sanity check the policy data has been loaded. + Assert.equal( + Services.policies.getExtensionSettings(id)?.private_browsing, + false, + `Got expected policies setting private_browsing for ${id}` + ); + Assert.equal( + Services.policies.getExtensionSettings(idNotAllowed)?.private_browsing, + true, + `Got expected policies setting private_browsing for ${idNotAllowed}` + ); + + async function assertPrivateBrowsingAllowed({ + addonId, + manifest, + expectedPrivateBrowsingAllowed, + }) { + let wrapper = await getWrapper(addonId, undefined, { manifest }); + let addon = await promiseAddonByID(addonId); + notEqual(addon, null, `${addonId} is installed`); + ok(addon.isActive, `${addonId} is active`); + ok(addon.isPrivileged, `${addonId} is privileged`); + ok(wrapper.extension.isAppProvided, `${addonId} is app provided`); + equal( + wrapper.extension.privateBrowsingAllowed, + expectedPrivateBrowsingAllowed, + `Builtin Addon ${addonId} should ${expectedPrivateBrowsingAllowed ? "" : "NOT"} have access private browsing access` + ); + await wrapper.unload(); + } + + await assertPrivateBrowsingAllowed({ + addonId: id, + expectedPrivateBrowsingAllowed: true, + }); + await assertPrivateBrowsingAllowed({ + addonId: idNotAllowed, + manifest: { + incognito: "not_allowed", + }, + expectedPrivateBrowsingAllowed: false, + }); + + await AddonTestUtils.promiseShutdownManager(); + } +); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_profile_location.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_profile_location.js index 9dbf8f707078..c291603eea8c 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_system_profile_location.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_profile_location.js @@ -13,9 +13,14 @@ AddonTestUtils.createAppInfo( AddonTestUtils.usePrivilegedSignatures = id => id.startsWith("system"); -async function promiseInstallSystemProfileExtension(id, hidden) { +async function promiseInstallSystemProfileExtension( + id, + hidden, + { manifest } = {} +) { let xpi = await AddonTestUtils.createTempWebExtensionFile({ manifest: { + ...manifest, browser_specific_settings: { gecko: { id } }, hidden, }, @@ -202,3 +207,83 @@ add_task(async function test_system_profile_location_require_system_cert() { await addon.uninstall(); await AddonTestUtils.promiseShutdownManager(); }); + +// Tests private_browsing policy extension settings doesn't apply +// to system addons. +add_task( + // This test is skipped on builds that don't support enterprise policies + // (e.g. android builds). + { skip_if: () => !Services.policies }, + async function test_builtin_private_browsing_policy_setting() { + await AddonTestUtils.promiseStartupManager(); + + const { EnterprisePolicyTesting } = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" + ); + + let id = "systemaddon@tests.mozilla.org"; + let idNotAllowed = "systemaddon-not-allowed@tests.mozilla.org"; + + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + ExtensionSettings: { + [id]: { + private_browsing: false, + }, + [idNotAllowed]: { + private_browsing: true, + }, + }, + }, + }); + + // Sanity check the policy data has been loaded. + Assert.equal( + Services.policies.getExtensionSettings(id)?.private_browsing, + false, + `Got expected policies setting private_browsing for ${id}` + ); + Assert.equal( + Services.policies.getExtensionSettings(idNotAllowed)?.private_browsing, + true, + `Got expected policies setting private_browsing for ${idNotAllowed}` + ); + + async function assertPrivateBrowsingAllowed({ + addonId, + manifest, + expectedPrivateBrowsingAllowed, + }) { + let wrapper = await promiseInstallSystemProfileExtension( + addonId, + undefined, + { manifest } + ); + let addon = await promiseAddonByID(addonId); + notEqual(addon, null, `${addonId} is installed`); + ok(addon.isActive, `${addonId} is active`); + ok(addon.isPrivileged, `${addonId} is privileged`); + ok(wrapper.extension.isAppProvided, `${addonId} is app provided`); + equal( + wrapper.extension.privateBrowsingAllowed, + expectedPrivateBrowsingAllowed, + `Builtin Addon ${addonId} should ${expectedPrivateBrowsingAllowed ? "" : "NOT"} have access private browsing access` + ); + await wrapper.unload(); + } + + await assertPrivateBrowsingAllowed({ + addonId: id, + expectedPrivateBrowsingAllowed: true, + }); + await assertPrivateBrowsingAllowed({ + addonId: idNotAllowed, + manifest: { + incognito: "not_allowed", + }, + expectedPrivateBrowsingAllowed: false, + }); + + await AddonTestUtils.promiseShutdownManager(); + } +);