Bug 1449021 - Policy for install/uninstalling add-ons. r?felipe draft
authorMichael Kaply <mozilla@kaply.com>
Wed, 28 Mar 2018 09:29:17 -0500
changeset 773798 c62922db69f1173c2b96145e5416c0c8f2606484
parent 773797 a456475502b80a1264642d9eaee9394a8fad8315
child 774382 1492f5a8eedaeb9d613c2cfb69fb2b25239dfa74
push id104309
push usermozilla@kaply.com
push dateWed, 28 Mar 2018 14:29:34 +0000
reviewersfelipe
bugs1449021
milestone61.0a1
Bug 1449021 - Policy for install/uninstalling add-ons. r?felipe MozReview-Commit-ID: HBcbs4hWfEA
browser/components/enterprisepolicies/Policies.jsm
browser/components/enterprisepolicies/schemas/policies-schema.json
browser/components/enterprisepolicies/tests/browser/browser.ini
browser/components/enterprisepolicies/tests/browser/browser_policy_extensions.js
browser/components/enterprisepolicies/tests/browser/policytest.xpi
toolkit/mozapps/extensions/internal/XPIProvider.jsm
--- a/browser/components/enterprisepolicies/Policies.jsm
+++ b/browser/components/enterprisepolicies/Policies.jsm
@@ -8,16 +8,17 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "gXulStore",
                                    "@mozilla.org/xul/xulstore;1",
                                    "nsIXULStore");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   BookmarksPolicies: "resource:///modules/policies/BookmarksPolicies.jsm",
   ProxyPolicies: "resource:///modules/policies/ProxyPolicies.jsm",
+  AddonManager: "resource://gre/modules/AddonManager.jsm",
 });
 
 const PREF_LOGLEVEL           = "browser.policies.loglevel";
 const BROWSER_DOCUMENT_URL    = "chrome://browser/content/browser.xul";
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
   let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm", {});
   return new ConsoleAPI({
@@ -274,16 +275,73 @@ var Policies = {
         }
       } else {
         setAndLockPref("privacy.trackingprotection.enabled", false);
         setAndLockPref("privacy.trackingprotection.pbmode.enabled", false);
       }
     }
   },
 
+  "Extensions": {
+    onBeforeUIStartup(manager, param) {
+      if ("Install" in param) {
+        runOncePerModification("extensionsInstall", JSON.stringify(param.Install), () => {
+          for (let location of param.Install) {
+            let url;
+            if (location.includes("://")) {
+              // Assume location is an URI
+              url = location;
+            } else {
+              // Assume location is a file path
+              let xpiFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+              try {
+                xpiFile.initWithPath(location);
+              } catch (e) {
+                log.error(`Invalid extension path location - ${location}`);
+                continue;
+              }
+              url = Services.io.newFileURI(xpiFile).spec;
+            }
+            AddonManager.getInstallForURL(url, (install) => {
+              let listener = {
+                onDownloadFailed: () => {
+                  log.error(`Download failed - ${location}`);
+                },
+                onInstallFailed: () => {
+                  log.error(`Installation failed - ${location}`);
+                },
+                onInstallEnded: () => {
+                  log.debug(`Installation succeeded - ${location}`);
+                }
+              };
+              install.addListener(listener);
+              install.install();
+            }, "application/x-xpinstall");
+          }
+        });
+      }
+      if ("Uninstall" in param) {
+        runOncePerModification("extensionsUninstall", JSON.stringify(param.Uninstall), () => {
+          AddonManager.getAddonsByIDs(param.Uninstall, (addons) => {
+            for (let addon of addons) {
+              if (addon) {
+                addon.uninstall();
+              }
+            }
+          });
+        });
+      }
+      if ("Locked" in param) {
+        for (let ID of param.Locked) {
+          manager.disallowFeature(`modify-extension:${ID}`);
+        }
+      }
+    }
+  },
+
   "FlashPlugin": {
     onBeforeUIStartup(manager, param) {
       addAllowDenyPermissions("plugin:flash", param.Allow, param.Block);
     }
   },
 
   "Homepage": {
     onBeforeUIStartup(manager, param) {
--- a/browser/components/enterprisepolicies/schemas/policies-schema.json
+++ b/browser/components/enterprisepolicies/schemas/policies-schema.json
@@ -217,16 +217,44 @@
         },
         "Locked": {
           "type": "boolean"
         }
       },
       "required": ["Value"]
     },
 
+    "Extensions": {
+      "description": "Install, uninstall or lock extensions. The Install option takes URLs or paths as parameters. The Uninstall and Locked options take extension IDs.",
+      "first_available": "60.0",
+      "enterprise_only": true,
+
+      "type": "object",
+      "properties": {
+        "Install" : {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "Uninstall" : {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "Locked" : {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        }
+      }
+    },
+
     "FlashPlugin": {
       "description": "Allow or deny flash plugin usage.",
       "first_available": "60.0",
 
       "type": "object",
       "properties": {
         "Allow": {
           "type": "array",
--- a/browser/components/enterprisepolicies/tests/browser/browser.ini
+++ b/browser/components/enterprisepolicies/tests/browser/browser.ini
@@ -1,15 +1,16 @@
 [DEFAULT]
 support-files =
   head.js
   config_popups_cookies_addons_flash.json
   config_broken_json.json
   opensearch.html
   opensearchEngine.xml
+  policytest.xpi
 
 [browser_policies_basic_tests.js]
 [browser_policies_broken_json.js]
 [browser_policies_enterprise_only.js]
 [browser_policies_notice_in_aboutpreferences.js]
 [browser_policies_popups_cookies_addons_flash.js]
 [browser_policies_runOnce_helper.js]
 [browser_policies_setAndLockPref_API.js]
@@ -30,11 +31,12 @@ support-files =
 [browser_policy_disable_masterpassword.js]
 [browser_policy_disable_pdfjs.js]
 [browser_policy_disable_pocket.js]
 [browser_policy_disable_privatebrowsing.js]
 [browser_policy_disable_safemode.js]
 [browser_policy_disable_shield.js]
 [browser_policy_display_bookmarks.js]
 [browser_policy_display_menu.js]
+[browser_policy_extensions.js]
 [browser_policy_proxy.js]
 [browser_policy_search_engine.js]
 [browser_policy_set_homepage.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_extensions.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const addonID = "policytest@mozilla.com";
+
+add_task(async function test_addon_install() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Extensions": {
+        "Install": [
+          "http://mochi.test:8888/browser/browser/components/enterprisepolicies/tests/browser/policytest.xpi"
+        ],
+        "Locked": [
+          addonID
+        ]
+      }
+    }
+  });
+  await wait_for_addon_install();
+  let addon = await AddonManager.getAddonByID(addonID);
+  isnot(addon, null, "Addon not installed.");
+});
+
+add_task(async function test_addon_locked() {
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+  await BrowserOpenAddonsMgr("addons://list/extension");
+  // eslint-disable-next-line no-shadow
+  await ContentTask.spawn(tab.linkedBrowser, {addonID}, async function({addonID}) {
+    let list = content.document.getElementById("addon-list");
+    let flashEntry = list.getElementsByAttribute("value", addonID)[0];
+    let disableBtn = content.document.getAnonymousElementByAttribute(flashEntry, "anonid", "disable-btn");
+    let removeBtn = content.document.getAnonymousElementByAttribute(flashEntry, "anonid", "remove-btn");
+    is(removeBtn.hidden, true, "Remove button should be hidden");
+    is(disableBtn.hidden, true, "Disable button should be hidden");
+  });
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_addon_uninstall() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Extensions": {
+        "Uninstall": [
+          addonID
+        ]
+      }
+    }
+  });
+  let addon = await AddonManager.getAddonByID(addonID);
+  is(addon, null, "Addon should be uninstalled.");
+});
+
+function wait_for_addon_install() {
+  return new Promise((resolve, reject) => {
+      AddonManager.addInstallListener({
+        onInstallEnded(install, addon) {
+          if (addon.id == addonID)
+          resolve();
+        },
+        onDownloadFailed: (install) => {
+          reject();
+        },
+        onInstallFailed: (install) => {
+          reject();
+        },
+      });
+  });
+}
new file mode 100644
index 0000000000000000000000000000000000000000..ee2a6289ee5c2abd373f4aefdae1356110c18596
GIT binary patch
literal 305
zc$^FHW@Zs#U|`^2cwSKLv+`YjW+RZd5r}yiWEgT2^D@&?i%ayfiu3bALpT|j6Q}Kp
z0^!mMZU#n{uZ#=~VC}Abd-)C-@U(olX`Lz=df>#7OLH4#BM-WY{R~c8=ybJwy|9Xe
z&7Jb{Yr1R)*m`UNr{3=NdMfgkvu}5r3(t~6eSs^U9Gd!IlFe@Om`pK=^o2LLZU5%q
z{#)?F=uG5ijZ>?ox{a3d7oGnynd`^BsGa}CVvG1*9=`YZLC^-<Ewjp2-((2zW@M6M
n#^n<U1`q%`mf;0R5Zzy_5PzZhDZrbR4Wf;aAs9%X02=}T?uTd9
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -4721,16 +4721,22 @@ AddonInternal.prototype = {
       if (this.type != "experiment" &&
           !this._installLocation.isLinkedAddon(this.id) && !isSystem) {
         permissions |= AddonManager.PERM_CAN_UPGRADE;
       }
 
       permissions |= AddonManager.PERM_CAN_UNINSTALL;
     }
 
+    if (Services.policies &&
+        !Services.policies.isAllowed(`modify-extension:${this.id}`)) {
+      permissions &= ~AddonManager.PERM_CAN_UNINSTALL;
+      permissions &= ~AddonManager.PERM_CAN_DISABLE;
+    }
+
     return permissions;
   },
 };
 
 /**
  * The AddonWrapper wraps an Addon to provide the data visible to consumers of
  * the public API.
  */