Bug 1402850 Don't include runtime permissions in prompts for webextension updates draft
authorAndrew Swan <aswan@mozilla.com>
Wed, 08 Nov 2017 17:14:11 -0800
changeset 760493 6f79b872b6102cc7dd1d24bfaaa36d95166c2671
parent 760185 580d833df9c44acec686a9fb88b5f27e9d29f68d
push id100669
push useraswan@mozilla.com
push dateTue, 27 Feb 2018 19:25:11 +0000
bugs1402850
milestone60.0a1
Bug 1402850 Don't include runtime permissions in prompts for webextension updates MozReview-Commit-ID: 1cnNsWLVGmg
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ext-permissions.js
toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
toolkit/mozapps/extensions/internal/XPIInstall.jsm
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -418,36 +418,70 @@ class ExtensionData {
           resolve(JSON.parse(text));
         } catch (e) {
           reject(e);
         }
       });
     });
   }
 
-  // This method should return a structured representation of any
-  // capabilities this extension has access to, as derived from the
-  // manifest.  The current implementation just returns the contents
-  // of the permissions attribute, if we add things like url_overrides,
-  // they should also be added here.
-  get userPermissions() {
+  /**
+   * Returns an object representing any capabilities that the extension
+   * has access to based on fixed properties in the manifest.  The result
+   * includes the contents of the "permissions" property as well as other
+   * capabilities that are derived from manifest fields that users should
+   * be informed of (e.g., origins where content scripts are injected).
+   */
+  get manifestPermissions() {
+    if (this.type !== "extension") {
+      return null;
+    }
+
+    let permissions = new Set();
+    let origins = new Set();
+    for (let perm of this.manifest.permissions || []) {
+      let type = classifyPermission(perm);
+      if (type.origin) {
+        origins.add(perm);
+      } else if (!type.api) {
+        permissions.add(perm);
+      }
+    }
+
+    if (this.manifest.devtools_page) {
+      permissions.add("devtools");
+    }
+
+    for (let entry of this.manifest.content_scripts || []) {
+      for (let origin of entry.matches) {
+        origins.add(origin);
+      }
+    }
+
+    return {
+      permissions: Array.from(permissions),
+      origins: Array.from(origins),
+    };
+  }
+
+  /**
+   * Returns an object representing all capabilities this extension has
+   * access to, including fixed ones from the manifest as well as dynamically
+   * granted permissions.
+   */
+  get activePermissions() {
     if (this.type !== "extension") {
       return null;
     }
 
     let result = {
       origins: this.whiteListedHosts.patterns.map(matcher => matcher.pattern),
       apis: [...this.apiNames],
     };
 
-    if (Array.isArray(this.manifest.content_scripts)) {
-      for (let entry of this.manifest.content_scripts) {
-        result.origins.push(...entry.matches);
-      }
-    }
     const EXP_PATTERN = /^experiments\.\w+/;
     result.permissions = [...this.permissions]
       .filter(p => !result.origins.includes(p) && !EXP_PATTERN.test(p));
     return result;
   }
 
   // Compute the difference between two sets of permissions, suitable
   // for presenting to the user.
@@ -537,20 +571,16 @@ class ExtensionData {
       originPermissions,
       permissions,
       schemaURLs: null,
       type: this.type,
       webAccessibleResources,
     };
 
     if (this.type === "extension") {
-      if (this.manifest.devtools_page) {
-        permissions.add("devtools");
-      }
-
       for (let perm of manifest.permissions) {
         if (perm === "geckoProfiler" && !this.isPrivileged) {
           const acceptedExtensions = Services.prefs.getStringPref("extensions.geckoProfiler.acceptedExtensionIds", "");
           if (!acceptedExtensions.split(",").includes(id)) {
             this.manifestError("Only whitelisted extensions are allowed to access the geckoProfiler.");
             continue;
           }
         }
--- a/toolkit/components/extensions/ext-permissions.js
+++ b/toolkit/components/extensions/ext-permissions.js
@@ -59,17 +59,17 @@ this.permissions = class extends Extensi
             }
           }
 
           await ExtensionPermissions.add(context.extension, perms);
           return true;
         },
 
         async getAll() {
-          let perms = context.extension.userPermissions;
+          let perms = context.extension.activePermissions;
           delete perms.apis;
           return perms;
         },
 
         async contains(permissions) {
           for (let perm of permissions.permissions) {
             if (!context.extension.hasPermission(perm)) {
               return false;
--- a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
@@ -1,12 +1,14 @@
 "use strict";
 
+ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionPermissions.jsm");
 ChromeUtils.import("resource://gre/modules/MessageChannel.jsm");
+ChromeUtils.import("resource://gre/modules/osfile.jsm");
 
 const BROWSER_PROPERTIES = "chrome://browser/locale/browser.properties";
 
 AddonTestUtils.init(this);
 AddonTestUtils.overrideCertDB();
 AddonTestUtils.createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
 
 let extensionHandlers = new WeakSet();
@@ -430,8 +432,93 @@ add_task(function test_permissions_have_
         ok(str.length, `Found localization string for '${perm}' permission`);
       } catch (e) {
         ok(GRANTED_WITHOUT_USER_PROMPT.includes(perm),
            `Permission '${perm}' intentionally granted without prompting the user`);
       }
     }
   }
 });
+
+// Check that optional permissions are not included in update prompts
+add_task(async function test_permissions_prompt() {
+  function background() {
+    browser.test.onMessage.addListener(async (msg, arg) => {
+      if (msg == "request") {
+        let result = await browser.permissions.request(arg);
+        browser.test.sendMessage("result", result);
+      }
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      name: "permissions test",
+      description: "permissions test",
+      manifest_version: 2,
+      version: "1.0",
+
+      permissions: ["tabs", "https://test1.example.com/*"],
+      optional_permissions: ["clipboardWrite", "<all_urls>"],
+
+      content_scripts: [
+        {
+          matches: ["https://test2.example.com/*"],
+          js: [],
+        },
+      ],
+    },
+    useAddonManager: "permanent",
+  });
+
+  await extension.startup();
+
+  await withHandlingUserInput(extension, async () => {
+    extension.sendMessage("request", {
+      permissions: ["clipboardWrite"],
+      origins: ["https://test2.example.com/*"],
+    });
+    let result = await extension.awaitMessage("result");
+    equal(result, true, "request() for optional permissions succeeded");
+  });
+
+  const PERMS = ["history", "tabs"];
+  const ORIGINS = ["https://test1.example.com/*", "https://test3.example.com/"];
+  let xpi = Extension.generateXPI({
+    background,
+    manifest: {
+      name: "permissions test",
+      description: "permissions test",
+      manifest_version: 2,
+      version: "2.0",
+
+      applications: {gecko: {id: extension.id}},
+
+      permissions: [...PERMS, ...ORIGINS],
+      optional_permissions: ["clipboardWrite", "<all_urls>"],
+    },
+  });
+
+  let install = await AddonManager.getInstallForFile(xpi);
+
+  Services.prefs.setBoolPref("extensions.webextPermissionPrompts", true);
+  registerCleanupFunction(() => {
+    Services.prefs.clearUserPref("extensions.webextPermissionPrompts");
+  });
+
+  let perminfo;
+  install.promptHandler = info => {
+    perminfo = info;
+    return Promise.resolve();
+  };
+
+  await AddonTestUtils.promiseCompleteInstall(install);
+  await extension.awaitStartup();
+
+  notEqual(perminfo, undefined, "Permission handler was invoked");
+  let perms = perminfo.addon.userPermissions;
+  deepEqual(perms.permissions, PERMS, "Update details includes only manifest api permissions");
+  deepEqual(perms.origins, ORIGINS, "Update details includes only manifest origin permissions");
+
+  await extension.unload();
+  await OS.File.remove(xpi.path);
+});
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -370,17 +370,17 @@ async function loadManifestFromWebManife
     else
       addon.optionsBrowserStyle = manifest.options_ui.browser_style;
   }
 
   // WebExtensions don't use iconURLs
   addon.iconURL = null;
   addon.icon64URL = null;
   addon.icons = manifest.icons || {};
-  addon.userPermissions = extension.userPermissions;
+  addon.userPermissions = extension.manifestPermissions;
 
   addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
 
   function getLocale(aLocale) {
     // Use the raw manifest, here, since we need values with their
     // localization placeholders still in place.
     let rawManifest = extension.rawManifest;