Commit a98b9389 authored by Michael Kaply's avatar Michael Kaply
Browse files

Bug 1522823 - Policy for whitelist/blacklist addons by ID. r=aswan,flod

Differential Revision: https://phabricator.services.mozilla.com/D27902

--HG--
extra : moz-landing-system : lando
parent 67fb859d
......@@ -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";
......
......@@ -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
......
......@@ -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"
}
......
......@@ -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'
......
......@@ -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": {
"*": {
"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").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": {
"extension1@mozilla.com": {
"blocked_install_message": "Extension1 error message.",
"*": {
"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": {
"*": {
"blocked_install_message": "Generic error message.",
"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);
});
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.");
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);
});
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script type="text/javascript">
function installTrigger(url) {
InstallTrigger.install({extension: url});
}
</script>
</head>
<body>
<p>
<a id="policytest" href="policytest_v0.1.xpi">policytest@mozilla.com</a>
</p>
<p>
<a id="policytest_installtrigger" onclick="installTrigger(this.href);return false;" href="policytest_v0.1.xpi">policytest@mozilla.com</a>
</p>
<p>
<a id="policytest_otherdomain" href="http://example.org:80/browser/browser/components/enterprisepolicies/tests/browser/policytest_v0.1.xpi">policytest@mozilla.com</a>
</p>
</body>
</html>
/* 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.",
},
"*": {