diff --git a/browser/base/content/browser-addons.js b/browser/base/content/browser-addons.js index e262a9650cee..6b1faf1d1f33 100644 --- a/browser/base/content/browser-addons.js +++ b/browser/base/content/browser-addons.js @@ -580,6 +580,16 @@ var gXPInstallObserver = { args = [brandShortName, Services.appinfo.version, install.name]; } + if (install.addon && !Services.policies.mayInstallAddon(install.addon)) { + error = "addonInstallBlockedByPolicy"; + let extensionSettings = Services.policies.getExtensionSettings(install.addon.id); + let message = ""; + if (extensionSettings && "blocked_install_message" in extensionSettings) { + message = " " + extensionSettings.blocked_install_message; + } + args = [install.name, install.addon.id, message]; + } + // Add Learn More link when refusing to install an unsigned add-on if (install.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) { options.learnMoreURL = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons"; diff --git a/browser/components/enterprisepolicies/Policies.jsm b/browser/components/enterprisepolicies/Policies.jsm index 993e4e141334..8e4a76aa8ab1 100644 --- a/browser/components/enterprisepolicies/Policies.jsm +++ b/browser/components/enterprisepolicies/Policies.jsm @@ -542,7 +542,7 @@ var Policies = { await addon.uninstall(); } catch (e) { // This can fail for add-ons that can't be uninstalled. - // Just ignore. + log.debug(`Add-on ID (${addon.id}) couldn't be uninstalled.`); } } } @@ -552,61 +552,29 @@ var Policies = { runOncePerModification("extensionsInstall", JSON.stringify(param.Install), async () => { await uninstallingPromise; for (let location of param.Install) { - let url; - if (location.includes("://")) { - // Assume location is an URI - url = location; - } else { + let uri; + try { + uri = Services.io.newURI(location); + } catch (e) { + // If it's not a URL, it's probably a file path. // Assume location is a file path - let xpiFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + // This is done for legacy support (old API) try { - xpiFile.initWithPath(location); - } catch (e) { + let xpiFile = new FileUtils.File(location); + uri = Services.io.newFileURI(xpiFile); + } catch (ex) { log.error(`Invalid extension path location - ${location}`); - continue; - } - url = Services.io.newFileURI(xpiFile).spec; - } - AddonManager.getInstallForURL(url, { - telemetryInfo: {source: "enterprise-policy"}, - }).then(install => { - if (install.addon && install.addon.appDisabled) { - log.error(`Incompatible add-on - ${location}`); - install.cancel(); return; } - let listener = { - /* eslint-disable-next-line no-shadow */ - onDownloadEnded: (install) => { - if (install.addon && install.addon.appDisabled) { - log.error(`Incompatible add-on - ${location}`); - install.removeListener(listener); - install.cancel(); - } - }, - onDownloadFailed: () => { - install.removeListener(listener); - log.error(`Download failed - ${location}`); - clearRunOnceModification("extensionsInstall"); - }, - onInstallFailed: () => { - install.removeListener(listener); - log.error(`Installation failed - ${location}`); - }, - onInstallEnded: () => { - install.removeListener(listener); - log.debug(`Installation succeeded - ${location}`); - }, - }; - install.addListener(listener); - install.install(); - }); + } + installAddonFromURL(uri.spec); } }); } if ("Locked" in param) { for (let ID of param.Locked) { - manager.disallowFeature(`modify-extension:${ID}`); + manager.disallowFeature(`uninstall-extension:${ID}`); + manager.disallowFeature(`disable-extension:${ID}`); } } }, @@ -614,7 +582,79 @@ var Policies = { "ExtensionSettings": { onBeforeAddons(manager, param) { - manager.setExtensionSettings(param); + try { + manager.setExtensionSettings(param); + } catch (e) { + log.error("Invalid ExtensionSettings"); + } + }, + async onBeforeUIStartup(manager, param) { + let extensionSettings = param; + let blockAllExtensions = false; + if ("*" in extensionSettings) { + if ("installation_mode" in extensionSettings["*"] && + extensionSettings["*"].installation_mode == "blocked") { + blockAllExtensions = true; + // Turn off discovery pane in about:addons + setAndLockPref("extensions.getAddons.showPane", false); + // Block about:debugging + blockAboutPage(manager, "about:debugging"); + } + } + let {addons} = await AddonManager.getActiveAddons(); + let allowedExtensions = []; + for (let extensionID in extensionSettings) { + if (extensionID == "*") { + // Ignore global settings + continue; + } + if ("installation_mode" in extensionSettings[extensionID]) { + if (extensionSettings[extensionID].installation_mode == "force_installed" || + extensionSettings[extensionID].installation_mode == "normal_installed") { + if (!extensionSettings[extensionID].install_url) { + throw new Error(`Missing install_url for ${extensionID}`); + } + if (!addons.find(addon => addon.id == extensionID)) { + installAddonFromURL(extensionSettings[extensionID].install_url, extensionID); + } + manager.disallowFeature(`uninstall-extension:${extensionID}`); + if (extensionSettings[extensionID].installation_mode == "force_installed") { + manager.disallowFeature(`disable-extension:${extensionID}`); + } + allowedExtensions.push(extensionID); + } else if (extensionSettings[extensionID].installation_mode == "allowed") { + allowedExtensions.push(extensionID); + } else if (extensionSettings[extensionID].installation_mode == "blocked") { + if (addons.find(addon => addon.id == extensionID)) { + // Can't use the addon from getActiveAddons since it doesn't have uninstall. + let addon = await AddonManager.getAddonByID(extensionID); + try { + await addon.uninstall(); + } catch (e) { + // This can fail for add-ons that can't be uninstalled. + log.debug(`Add-on ID (${addon.id}) couldn't be uninstalled.`); + } + } + } + } + } + if (blockAllExtensions) { + for (let addon of addons) { + if (addon.isSystem || addon.isBuiltin) { + continue; + } + if (!allowedExtensions.includes(addon.id)) { + try { + // Can't use the addon from getActiveAddons since it doesn't have uninstall. + let addonToUninstall = await AddonManager.getAddonByID(addon.id); + await addonToUninstall.uninstall(); + } catch (e) { + // This can fail for add-ons that can't be uninstalled. + log.debug(`Add-on ID (${addon.id}) couldn't be uninstalled.`); + } + } + } + } }, }, @@ -1304,6 +1344,54 @@ function replacePathVariables(path) { return path; } +/** + * installAddonFromURL + * + * Helper function that installs an addon from a URL + * and verifies that the addon ID matches. +*/ +function installAddonFromURL(url, extensionID) { + AddonManager.getInstallForURL(url, { + telemetryInfo: {source: "enterprise-policy"}, + }).then(install => { + if (install.addon && install.addon.appDisabled) { + log.error(`Incompatible add-on - ${location}`); + install.cancel(); + return; + } + let listener = { + /* eslint-disable-next-line no-shadow */ + onDownloadEnded: (install) => { + if (extensionID && install.addon.id != extensionID) { + log.error(`Add-on downloaded from ${url} had unexpected id (got ${install.addon.id} expected ${extensionID})`); + install.removeListener(listener); + install.cancel(); + } + if (install.addon && install.addon.appDisabled) { + log.error(`Incompatible add-on - ${url}`); + install.removeListener(listener); + install.cancel(); + } + }, + onDownloadFailed: () => { + install.removeListener(listener); + log.error(`Download failed - ${url}`); + clearRunOnceModification("extensionsInstall"); + }, + onInstallFailed: () => { + install.removeListener(listener); + log.error(`Installation failed - ${url}`); + }, + onInstallEnded: () => { + install.removeListener(listener); + log.debug(`Installation succeeded - ${url}`); + }, + }; + install.addListener(listener); + install.install(); + }); +} + let gChromeURLSBlocked = false; // If any about page is blocked, we block the loading of all diff --git a/browser/components/enterprisepolicies/schemas/policies-schema.json b/browser/components/enterprisepolicies/schemas/policies-schema.json index f87605cb62f9..6e4d690ad2ed 100644 --- a/browser/components/enterprisepolicies/schemas/policies-schema.json +++ b/browser/components/enterprisepolicies/schemas/policies-schema.json @@ -323,10 +323,44 @@ "ExtensionSettings": { "type": "object", + "properties": { + "*": { + "type": "object", + "properties": { + "installation_mode": { + "type": "string", + "enum": ["allowed", "blocked"] + }, + "allowed_types": { + "type": "array", + "items": { + "type": "string", + "enum": ["extension", "dictionary", "locale", "theme"] + } + }, + "blocked_install_message": { + "type": "string" + }, + "install_sources": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, "patternProperties": { "^.*$": { "type": "object", "properties": { + "installation_mode": { + "type": "string", + "enum": ["allowed", "blocked", "force_installed", "normal_installed"] + }, + "install_url": { + "type": "string" + }, "blocked_install_message": { "type": "string" } diff --git a/browser/components/enterprisepolicies/tests/browser/browser.ini b/browser/components/enterprisepolicies/tests/browser/browser.ini index ba93bafcd9ec..bd46f3b251f4 100644 --- a/browser/components/enterprisepolicies/tests/browser/browser.ini +++ b/browser/components/enterprisepolicies/tests/browser/browser.ini @@ -9,6 +9,7 @@ support-files = policy_websitefilter_exception.html ../../../../../toolkit/components/antitracking/test/browser/page.html ../../../../../toolkit/components/antitracking/test/browser/subResources.sjs + extensionsettings.html [browser_policies_getActivePolicies.js] skip-if = os != 'mac' diff --git a/browser/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings.js b/browser/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings.js index dbdd5c2f3710..1736bf4a2be5 100644 --- a/browser/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings.js +++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings.js @@ -2,22 +2,206 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; -add_task(async function test_extensionsettings() { +const BASE_URL = "http://mochi.test:8888/browser/browser/components/enterprisepolicies/tests/browser/"; + +/** + * Wait for the given PopupNotification to display + * + * @param {string} name + * The name of the notification to wait for. + * + * @returns {Promise} + * Resolves with the notification window. + */ +function promisePopupNotificationShown(name) { + return new Promise(resolve => { + function popupshown() { + let notification = PopupNotifications.getNotification(name); + if (!notification) { return; } + + ok(notification, `${name} notification shown`); + ok(PopupNotifications.isPanelOpen, "notification panel open"); + + PopupNotifications.panel.removeEventListener("popupshown", popupshown); + resolve(PopupNotifications.panel.firstElementChild); + } + + PopupNotifications.panel.addEventListener("popupshown", popupshown); + }); +} + +add_task(async function test_install_source_blocked_link() { await setupPolicyEngineWithJson({ "policies": { "ExtensionSettings": { - "extension1@mozilla.com": { - "blocked_install_message": "Extension1 error message.", - }, "*": { - "blocked_install_message": "Generic error message.", + "install_sources": ["http://blocks.other.install.sources/*"], }, }, }, }); + let popupPromise = promisePopupNotificationShown("addon-install-origin-blocked"); + let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser, + opening: BASE_URL + "extensionsettings.html", + waitForStateStop: true}); - let extensionSettings = Services.policies.getExtensionSettings("extension1@mozilla.com"); - is(extensionSettings.blocked_install_message, "Extension1 error message.", "Should have extension specific message."); - extensionSettings = Services.policies.getExtensionSettings("extension2@mozilla.com"); - is(extensionSettings.blocked_install_message, "Generic error message.", "Should have generic message."); + await ContentTask.spawn(tab.linkedBrowser, {}, () => { + content.document.getElementById("policytest").click(); + }); + await popupPromise; + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_install_source_blocked_installtrigger() { + await setupPolicyEngineWithJson({ + "policies": { + "ExtensionSettings": { + "*": { + "install_sources": ["http://blocks.other.install.sources/*"], + }, + }, + }, + }); + let popupPromise = promisePopupNotificationShown("addon-install-origin-blocked"); + let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser, + opening: BASE_URL + "extensionsettings.html", + waitForStateStop: true}); + + await ContentTask.spawn(tab.linkedBrowser, {}, () => { + content.document.getElementById("policytest_installtrigger").click(); + }); + await popupPromise; + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_install_source_blocked_otherdomain() { + await setupPolicyEngineWithJson({ + "policies": { + "ExtensionSettings": { + "*": { + "install_sources": ["http://mochi.test/*"], + }, + }, + }, + }); + let popupPromise = promisePopupNotificationShown("addon-install-origin-blocked"); + let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser, + opening: BASE_URL + "extensionsettings.html", + waitForStateStop: true}); + + await ContentTask.spawn(tab.linkedBrowser, {}, () => { + content.document.getElementById("policytest_otherdomain").click(); + }); + await popupPromise; + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_install_source_blocked_direct() { + await setupPolicyEngineWithJson({ + "policies": { + "ExtensionSettings": { + "*": { + "install_sources": ["http://blocks.other.install.sources/*"], + }, + }, + }, + }); + let popupPromise = promisePopupNotificationShown("addon-install-origin-blocked"); + let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser, + opening: BASE_URL + "extensionsettings.html", + waitForStateStop: true}); + + await ContentTask.spawn(tab.linkedBrowser, {baseUrl: BASE_URL}, async function({baseUrl}) { + content.document.location.href = baseUrl + "policytest_v0.1.xpi"; + }); + await popupPromise; + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_install_source_allowed_link() { + await setupPolicyEngineWithJson({ + "policies": { + "ExtensionSettings": { + "*": { + "install_sources": ["http://mochi.test/*"], + }, + }, + }, + }); + let popupPromise = promisePopupNotificationShown("addon-webext-permissions"); + let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser, + opening: BASE_URL + "extensionsettings.html", + waitForStateStop: true}); + + await ContentTask.spawn(tab.linkedBrowser, {}, () => { + content.document.getElementById("policytest").click(); + }); + await popupPromise; + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_install_source_allowed_installtrigger() { + await setupPolicyEngineWithJson({ + "policies": { + "ExtensionSettings": { + "*": { + "install_sources": ["http://mochi.test/*"], + }, + }, + }, + }); + let popupPromise = promisePopupNotificationShown("addon-webext-permissions"); + let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser, + opening: BASE_URL + "extensionsettings.html", + waitForStateStop: true}); + + await ContentTask.spawn(tab.linkedBrowser, {}, () => { + content.document.getElementById("policytest_installtrigger").click(); + }); + await popupPromise; + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_install_source_allowed_otherdomain() { + await setupPolicyEngineWithJson({ + "policies": { + "ExtensionSettings": { + "*": { + "install_sources": ["http://mochi.test/*", "http://example.org/*"], + }, + }, + }, + }); + let popupPromise = promisePopupNotificationShown("addon-webext-permissions"); + let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser, + opening: BASE_URL + "extensionsettings.html", + waitForStateStop: true}); + + await ContentTask.spawn(tab.linkedBrowser, {}, () => { + content.document.getElementById("policytest_otherdomain").click(); + }); + await popupPromise; + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_install_source_allowed_direct() { + await setupPolicyEngineWithJson({ + "policies": { + "ExtensionSettings": { + "*": { + "install_sources": ["http://mochi.test/*"], + }, + }, + }, + }); + let popupPromise = promisePopupNotificationShown("addon-webext-permissions"); + let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser, + opening: BASE_URL + "extensionsettings.html", + waitForStateStop: true}); + + await ContentTask.spawn(tab.linkedBrowser, {baseUrl: BASE_URL}, async function({baseUrl}) { + content.document.location.href = baseUrl + "policytest_v0.1.xpi"; + }); + await popupPromise; + BrowserTestUtils.removeTab(tab); }); diff --git a/browser/components/enterprisepolicies/tests/browser/extensionsettings.html b/browser/components/enterprisepolicies/tests/browser/extensionsettings.html new file mode 100644 index 000000000000..1370a2b5c473 --- /dev/null +++ b/browser/components/enterprisepolicies/tests/browser/extensionsettings.html @@ -0,0 +1,23 @@ + + + + + + + + +

+policytest@mozilla.com +

+

+policytest@mozilla.com +

+

+policytest@mozilla.com +

+ + diff --git a/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js b/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js new file mode 100644 index 000000000000..47e72bff6c01 --- /dev/null +++ b/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js @@ -0,0 +1,155 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const {AddonTestUtils} = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm"); +const {AddonManager} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm"); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "48", "48"); + +const server = AddonTestUtils.createHttpServer({hosts: ["example.com"]}); +const BASE_URL = `http://example.com/data`; + +let addonID = "policytest2@mozilla.com"; + +add_task(async function setup() { + await AddonTestUtils.promiseStartupManager(); + + let webExtensionFile = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + applications: { + gecko: { + id: addonID, + }, + }, + }, + }); + + server.registerFile("/data/policy_test.xpi", webExtensionFile); +}); + +add_task(async function test_extensionsettings() { + await setupPolicyEngineWithJson({ + "policies": { + "ExtensionSettings": { + "extension1@mozilla.com": { + "blocked_install_message": "Extension1 error message.", + }, + "*": { + "blocked_install_message": "Generic error message.", + }, + }, + }, + }); + + let extensionSettings = Services.policies.getExtensionSettings("extension1@mozilla.com"); + equal(extensionSettings.blocked_install_message, "Extension1 error message.", "Should have extension specific message."); + extensionSettings = Services.policies.getExtensionSettings("extension2@mozilla.com"); + equal(extensionSettings.blocked_install_message, "Generic error message.", "Should have generic message."); +}); + +add_task(async function test_addon_blocked() { + await setupPolicyEngineWithJson({ + "policies": { + "ExtensionSettings": { + "policytest2@mozilla.com": { + "installation_mode": "blocked", + }, + }, + }, + }); + + let install = await AddonManager.getInstallForURL(BASE_URL + "/policy_test.xpi"); + await install.install(); + notEqual(install.addon, null, "Addon should not be null"); + equal(install.addon.appDisabled, true, "Addon should be disabled"); + await install.addon.uninstall(); +}); + +add_task(async function test_addon_allowed() { + await setupPolicyEngineWithJson({ + "policies": { + "ExtensionSettings": { + "policytest2@mozilla.com": { + "installation_mode": "allowed", + }, + "*": { + "installation_mode": "blocked", + }, + }, + }, + }); + + let install = await AddonManager.getInstallForURL(BASE_URL + "/policy_test.xpi"); + await install.install(); + notEqual(install.addon, null, "Addon should not be null"); + equal(install.addon.appDisabled, false, "Addon should not be disabled"); + await install.addon.uninstall(); +}); + +add_task(async function test_addon_uninstalled() { + let install = await AddonManager.getInstallForURL(BASE_URL + "/policy_test.xpi"); + await install.install(); + notEqual(install.addon, null, "Addon should not be null"); + + await Promise.all([ + AddonTestUtils.promiseAddonEvent("onUninstalled"), + setupPolicyEngineWithJson({ + "policies": { + "ExtensionSettings": { + "*": { + "installation_mode": "blocked", + }, + }, + }, + }), + ]); + let addon = await AddonManager.getAddonByID(addonID); + equal(addon, null, "Addon should be null"); +}); + +add_task(async function test_addon_forceinstalled() { + await Promise.all([ + AddonTestUtils.promiseInstallEvent("onInstallEnded"), + setupPolicyEngineWithJson({ + "policies": { + "ExtensionSettings": { + "policytest2@mozilla.com": { + "installation_mode": "force_installed", + "install_url": BASE_URL + "/policy_test.xpi", + }, + }, + }, + }), + ]); + let addon = await AddonManager.getAddonByID(addonID); + notEqual(addon, null, "Addon should not be null"); + equal(addon.appDisabled, false, "Addon should not be disabled"); + equal(addon.permissions & AddonManager.PERM_CAN_UNINSTALL, 0, "Addon should not be able to be uninstalled."); + equal(addon.permissions & AddonManager.PERM_CAN_DISABLE, 0, "Addon should not be able to be disabled."); + await addon.uninstall(); +}); + +add_task(async function test_addon_normalinstalled() { + await Promise.all([ + AddonTestUtils.promiseInstallEvent("onInstallEnded"), + setupPolicyEngineWithJson({ + "policies": { + "ExtensionSettings": { + "policytest2@mozilla.com": { + "installation_mode": "normal_installed", + "install_url": BASE_URL + "/policy_test.xpi", + }, + }, + }, + }), + ]); + let addon = await AddonManager.getAddonByID(addonID); + notEqual(addon, null, "Addon should not be null"); + equal(addon.appDisabled, false, "Addon should not be disabled"); + equal(addon.permissions & AddonManager.PERM_CAN_UNINSTALL, 0, "Addon should not be able to be uninstalled."); + notEqual(addon.permissions & AddonManager.PERM_CAN_DISABLE, 0, "Addon should be able to be disabled."); + await addon.uninstall(); +}); diff --git a/browser/components/enterprisepolicies/tests/xpcshell/xpcshell.ini b/browser/components/enterprisepolicies/tests/xpcshell/xpcshell.ini index bbd46f1f7cad..107020f4c2cd 100644 --- a/browser/components/enterprisepolicies/tests/xpcshell/xpcshell.ini +++ b/browser/components/enterprisepolicies/tests/xpcshell/xpcshell.ini @@ -6,6 +6,7 @@ head = head.js [test_appupdateurl.js] [test_clear_blocked_cookies.js] [test_defaultbrowsercheck.js] +[test_extensionsettings.js] [test_macosparser_unflatten.js] skip-if = os != 'mac' [test_permissions.js] diff --git a/browser/locales/en-US/browser/policies/policies-descriptions.ftl b/browser/locales/en-US/browser/policies/policies-descriptions.ftl index 55b1937c5518..b649c64b328d 100644 --- a/browser/locales/en-US/browser/policies/policies-descriptions.ftl +++ b/browser/locales/en-US/browser/policies/policies-descriptions.ftl @@ -90,6 +90,8 @@ policy-EnableTrackingProtection = Enable or disable Content Blocking and optiona # English or translate them as verbs. policy-Extensions = Install, uninstall or lock extensions. The Install option takes URLs or paths as parameters. The Uninstall and Locked options take extension IDs. +policy-ExtensionSettings = Manage all aspects of extension installation. + policy-ExtensionUpdate = Enable or disable automatic extension updates. policy-FirefoxHome = Configure Firefox Home. diff --git a/browser/locales/en-US/chrome/browser/browser.properties b/browser/locales/en-US/chrome/browser/browser.properties index 161b442d06f1..dbb66b3a5eca 100644 --- a/browser/locales/en-US/chrome/browser/browser.properties +++ b/browser/locales/en-US/chrome/browser/browser.properties @@ -41,6 +41,13 @@ xpinstallDisabledMessage=Software installation is currently disabled. Click Enab xpinstallDisabledButton=Enable xpinstallDisabledButton.accesskey=n +# LOCALIZATION NOTE (addonInstallBlockedByPolicy) +# This message is shown when the installation of an add-on is blocked by +# enterprise policy. %1$S is replaced by the name of the add-on. +# %2$S is replaced by the ID of add-on. %3$S is a custom message that +# the administration can add to the message. +addonInstallBlockedByPolicy=%1$S (%2$S) is blocked by your system administrator.%3$S + # LOCALIZATION NOTE (webextPerms.header) # This string is used as a header in the webextension permissions dialog, # %S is replaced with the localized name of the extension being installed. diff --git a/toolkit/components/enterprisepolicies/EnterprisePolicies.js b/toolkit/components/enterprisepolicies/EnterprisePolicies.js index 0f5e590364ba..c219d4edf670 100644 --- a/toolkit/components/enterprisepolicies/EnterprisePolicies.js +++ b/toolkit/components/enterprisepolicies/EnterprisePolicies.js @@ -300,23 +300,61 @@ EnterprisePoliciesManager.prototype = { setExtensionSettings(extensionSettings) { ExtensionSettings = extensionSettings; + if ("*" in extensionSettings && + "install_sources" in extensionSettings["*"]) { + InstallSources = new MatchPatternSet(extensionSettings["*"].install_sources); + } }, getExtensionSettings(extensionID) { let settings = null; - if (extensionID in ExtensionSettings) { - settings = ExtensionSettings[extensionID]; - } else if ("*" in ExtensionSettings) { - settings = ExtensionSettings["*"]; + if (ExtensionSettings) { + if (extensionID in ExtensionSettings) { + settings = ExtensionSettings[extensionID]; + } else if ("*" in ExtensionSettings) { + settings = ExtensionSettings["*"]; + } } return settings; }, + + mayInstallAddon(addon) { + // See https://dev.chromium.org/administrators/policy-list-3/extension-settings-full + if (!ExtensionSettings) { + return true; + } + if (addon.id in ExtensionSettings) { + if ("installation_mode" in ExtensionSettings[addon.id]) { + switch (ExtensionSettings[addon.id].installation_mode) { + case "blocked": + return false; + default: + return true; + } + } + } + if ("*" in ExtensionSettings) { + if (ExtensionSettings["*"].installation_mode && + ExtensionSettings["*"].installation_mode == "blocked") { + return false; + } + if ("allowed_types" in ExtensionSettings["*"]) { + return ExtensionSettings["*"].allowed_types.includes(addon.type); + } + } + return true; + }, + + allowedInstallSource(uri) { + return InstallSources ? InstallSources.matches(uri) : true; + }, }; let DisallowedFeatures = {}; let SupportMenu = null; let ExtensionPolicies = null; let ExtensionSettings = null; +let InstallSources = null; /** * areEnterpriseOnlyPoliciesAllowed diff --git a/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl b/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl index 812304a70c55..fd81f5a9da0a 100644 --- a/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl +++ b/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsISupports.idl" +#include "nsIURI.idl" [scriptable, uuid(6a568972-cc91-4bf5-963e-3768f3319b8a)] interface nsIEnterprisePolicies : nsISupports @@ -48,4 +49,20 @@ interface nsIEnterprisePolicies : nsISupports * @returns A JS object that settings or null if unavailable. */ jsval getExtensionSettings(in ACString extensionID); + + /** + * Uses the whitelist, blacklist and settings to determine if an extension + * may be installed. + * + * @returns A boolean - true of the extension may be installed. + */ + bool mayInstallAddon(in jsval addon); + + /** + * Uses install_sources to determine if an extension can be installed + * from the given URI. + * + * @returns A boolean - true of the extension may be installed. + */ + bool allowedInstallSource(in nsIURI uri); }; diff --git a/toolkit/mozapps/extensions/AddonManager.jsm b/toolkit/mozapps/extensions/AddonManager.jsm index 79dd227f783b..21ac1b84b01e 100644 --- a/toolkit/mozapps/extensions/AddonManager.jsm +++ b/toolkit/mozapps/extensions/AddonManager.jsm @@ -1663,6 +1663,10 @@ var AddonManagerInternal = { throw Components.Exception("aInstallingPrincipal must be a nsIPrincipal", Cr.NS_ERROR_INVALID_ARG); + if (this.isInstallAllowedByPolicy(aInstallingPrincipal, null, true /* explicit */)) { + return true; + } + let providers = [...this.providers]; for (let provider of providers) { if (callProvider(provider, "supportsMimetype", false, aMimetype) && @@ -1672,6 +1676,40 @@ var AddonManagerInternal = { return false; }, + /** + * Checks whether a particular source is allowed to install add-ons based + * on policy. + * + * @param aInstallingPrincipal + * The nsIPrincipal that initiated the install + * @param aInstall + * The AddonInstall to be installed + * @param explicit + * If this is set, we only return true if the source is explicitly + * blocked via policy. + * + * @return boolean + * By default, returns true if the source is blocked by policy + * or there is no policy. + * If explicit is set, only returns true of the source is + * blocked by policy, false otherwise. This is needed for + * handling inverse cases. + */ + isInstallAllowedByPolicy(aInstallingPrincipal, aInstall, explicit) { + if (Services.policies) { + let extensionSettings = Services.policies.getExtensionSettings("*"); + if (extensionSettings && extensionSettings.install_sources) { + if ((!aInstall || Services.policies.allowedInstallSource(aInstall.sourceURI)) && + (!aInstallingPrincipal || !aInstallingPrincipal.URI || + Services.policies.allowedInstallSource(aInstallingPrincipal.URI))) { + return true; + } + return false; + } + } + return !explicit; + }, + installNotifyObservers(aTopic, aBrowser, aUri, aInstall, aInstallFn) { let info = { wrappedJSObject: { @@ -1809,7 +1847,9 @@ var AddonManagerInternal = { this.installNotifyObservers("addon-install-disabled", topBrowser, aInstallingPrincipal.URI, aInstall); return; - } else if (aInstallingPrincipal.isNullPrincipal || !aBrowser.contentPrincipal || !aInstallingPrincipal.subsumes(aBrowser.contentPrincipal)) { + } else if (aInstallingPrincipal.isNullPrincipal || !aBrowser.contentPrincipal || + !aInstallingPrincipal.subsumes(aBrowser.contentPrincipal) || + !this.isInstallAllowedByPolicy(aInstallingPrincipal, aInstall, false /* explicit */)) { aInstall.cancel(); this.installNotifyObservers("addon-install-origin-blocked", topBrowser, @@ -1855,6 +1895,13 @@ var AddonManagerInternal = { * The AddonInstall to be installed */ installAddonFromAOM(browser, uri, install) { + if (!this.isInstallAllowedByPolicy(null, install)) { + install.cancel(); + + this.installNotifyObservers("addon-install-origin-blocked", browser, + install.sourceURI, install); + return; + } if (!gStarted) throw Components.Exception("AddonManager is not initialized", Cr.NS_ERROR_NOT_INITIALIZED); diff --git a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm index 0d6be5a04558..a9a55de12013 100644 --- a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm +++ b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm @@ -659,10 +659,13 @@ class AddonInternal { permissions |= AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS; } - if (Services.policies && - !Services.policies.isAllowed(`modify-extension:${this.id}`)) { - permissions &= ~AddonManager.PERM_CAN_UNINSTALL; - permissions &= ~AddonManager.PERM_CAN_DISABLE; + if (Services.policies) { + if (!Services.policies.isAllowed(`uninstall-extension:${this.id}`)) { + permissions &= ~AddonManager.PERM_CAN_UNINSTALL; + } + if (!Services.policies.isAllowed(`disable-extension:${this.id}`)) { + permissions &= ~AddonManager.PERM_CAN_DISABLE; + } } return permissions; @@ -1945,6 +1948,14 @@ this.XPIDatabase = { } } + if (aAddon.location.isSystem || aAddon.location.isBuiltin) { + return true; + } + + if (Services.policies && !Services.policies.mayInstallAddon(aAddon)) { + return false; + } + return true; },